swarm-tickets 1.0.1 → 2.0.2
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.txt +0 -0
- package/README.md +307 -61
- package/SKILL.md +222 -46
- package/backup-tickets.sh +0 -0
- package/bug-report-widget.js +536 -0
- package/lib/storage/base-adapter.js +200 -0
- package/lib/storage/index.js +87 -0
- package/lib/storage/json-adapter.js +293 -0
- package/lib/storage/sqlite-adapter.js +552 -0
- package/lib/storage/supabase-adapter.js +614 -0
- package/package.json +21 -12
- package/setup.js +7 -8
- package/ticket-cli.js +0 -0
- package/ticket-server.js +425 -269
- package/ticket-tracker.html +567 -132
- package/tickets.example.json +0 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Storage Adapter
|
|
3
|
+
* Uses better-sqlite3 for synchronous, high-performance SQLite operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const BaseAdapter = require('./base-adapter');
|
|
8
|
+
|
|
9
|
+
class SqliteAdapter extends BaseAdapter {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
super(config);
|
|
12
|
+
this.dbPath = path.resolve(config.sqlitePath || './tickets.db');
|
|
13
|
+
this.db = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async initialize() {
|
|
17
|
+
// Dynamic import to make sqlite optional
|
|
18
|
+
let Database;
|
|
19
|
+
try {
|
|
20
|
+
Database = require('better-sqlite3');
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'SQLite adapter requires better-sqlite3. Install it with: npm install better-sqlite3'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.db = new Database(this.dbPath);
|
|
28
|
+
|
|
29
|
+
// Enable foreign keys
|
|
30
|
+
this.db.pragma('foreign_keys = ON');
|
|
31
|
+
|
|
32
|
+
// Create tables
|
|
33
|
+
this.db.exec(`
|
|
34
|
+
-- Main tickets table
|
|
35
|
+
CREATE TABLE IF NOT EXISTS tickets (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
route TEXT NOT NULL,
|
|
38
|
+
f12Errors TEXT DEFAULT '',
|
|
39
|
+
serverErrors TEXT DEFAULT '',
|
|
40
|
+
description TEXT DEFAULT '',
|
|
41
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'in-progress', 'fixed', 'closed')),
|
|
42
|
+
priority TEXT CHECK (priority IS NULL OR priority IN ('critical', 'high', 'medium', 'low')),
|
|
43
|
+
namespace TEXT,
|
|
44
|
+
createdAt TEXT NOT NULL,
|
|
45
|
+
updatedAt TEXT NOT NULL
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
-- Related tickets junction table
|
|
49
|
+
CREATE TABLE IF NOT EXISTS ticket_relations (
|
|
50
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
+
ticket_id TEXT NOT NULL,
|
|
52
|
+
related_ticket_id TEXT NOT NULL,
|
|
53
|
+
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
|
|
54
|
+
FOREIGN KEY (related_ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
|
|
55
|
+
UNIQUE(ticket_id, related_ticket_id)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
-- Swarm actions log
|
|
59
|
+
CREATE TABLE IF NOT EXISTS swarm_actions (
|
|
60
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
61
|
+
ticket_id TEXT NOT NULL,
|
|
62
|
+
timestamp TEXT NOT NULL,
|
|
63
|
+
action TEXT NOT NULL,
|
|
64
|
+
result TEXT,
|
|
65
|
+
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
-- Comments table
|
|
69
|
+
CREATE TABLE IF NOT EXISTS comments (
|
|
70
|
+
id TEXT PRIMARY KEY,
|
|
71
|
+
ticket_id TEXT NOT NULL,
|
|
72
|
+
timestamp TEXT NOT NULL,
|
|
73
|
+
type TEXT NOT NULL DEFAULT 'human' CHECK (type IN ('human', 'ai')),
|
|
74
|
+
author TEXT DEFAULT 'anonymous',
|
|
75
|
+
content TEXT DEFAULT '',
|
|
76
|
+
metadata TEXT DEFAULT '{}',
|
|
77
|
+
editedAt TEXT,
|
|
78
|
+
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
-- API Keys for bug report widget
|
|
82
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
83
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
|
+
key TEXT UNIQUE NOT NULL,
|
|
85
|
+
name TEXT,
|
|
86
|
+
created_at TEXT NOT NULL,
|
|
87
|
+
last_used TEXT,
|
|
88
|
+
rate_limit INTEGER DEFAULT 100,
|
|
89
|
+
enabled INTEGER DEFAULT 1
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
-- Rate limiting table
|
|
93
|
+
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
94
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
95
|
+
identifier TEXT NOT NULL,
|
|
96
|
+
window_start TEXT NOT NULL,
|
|
97
|
+
request_count INTEGER DEFAULT 1,
|
|
98
|
+
UNIQUE(identifier, window_start)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
-- Create indexes
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_tickets_priority ON tickets(priority);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_tickets_route ON tickets(route);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_tickets_createdAt ON tickets(createdAt);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_swarm_actions_ticket_id ON swarm_actions(ticket_id);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_comments_ticket_id ON comments(ticket_id);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_rate_limits_identifier ON rate_limits(identifier);
|
|
109
|
+
`);
|
|
110
|
+
|
|
111
|
+
// Prepare commonly used statements
|
|
112
|
+
this._prepareStatements();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_prepareStatements() {
|
|
116
|
+
this.stmts = {
|
|
117
|
+
getAllTickets: this.db.prepare('SELECT * FROM tickets ORDER BY createdAt DESC'),
|
|
118
|
+
getTicket: this.db.prepare('SELECT * FROM tickets WHERE id = ?'),
|
|
119
|
+
insertTicket: this.db.prepare(`
|
|
120
|
+
INSERT INTO tickets (id, route, f12Errors, serverErrors, description, status, priority, namespace, createdAt, updatedAt)
|
|
121
|
+
VALUES (@id, @route, @f12Errors, @serverErrors, @description, @status, @priority, @namespace, @createdAt, @updatedAt)
|
|
122
|
+
`),
|
|
123
|
+
updateTicket: this.db.prepare(`
|
|
124
|
+
UPDATE tickets SET
|
|
125
|
+
route = COALESCE(@route, route),
|
|
126
|
+
f12Errors = COALESCE(@f12Errors, f12Errors),
|
|
127
|
+
serverErrors = COALESCE(@serverErrors, serverErrors),
|
|
128
|
+
description = COALESCE(@description, description),
|
|
129
|
+
status = COALESCE(@status, status),
|
|
130
|
+
priority = @priority,
|
|
131
|
+
namespace = @namespace,
|
|
132
|
+
updatedAt = @updatedAt
|
|
133
|
+
WHERE id = @id
|
|
134
|
+
`),
|
|
135
|
+
deleteTicket: this.db.prepare('DELETE FROM tickets WHERE id = ?'),
|
|
136
|
+
|
|
137
|
+
// Relations
|
|
138
|
+
getRelatedTickets: this.db.prepare('SELECT related_ticket_id FROM ticket_relations WHERE ticket_id = ?'),
|
|
139
|
+
insertRelation: this.db.prepare('INSERT OR IGNORE INTO ticket_relations (ticket_id, related_ticket_id) VALUES (?, ?)'),
|
|
140
|
+
deleteRelations: this.db.prepare('DELETE FROM ticket_relations WHERE ticket_id = ?'),
|
|
141
|
+
|
|
142
|
+
// Swarm actions
|
|
143
|
+
getSwarmActions: this.db.prepare('SELECT * FROM swarm_actions WHERE ticket_id = ? ORDER BY timestamp ASC'),
|
|
144
|
+
insertSwarmAction: this.db.prepare(`
|
|
145
|
+
INSERT INTO swarm_actions (ticket_id, timestamp, action, result)
|
|
146
|
+
VALUES (@ticket_id, @timestamp, @action, @result)
|
|
147
|
+
`),
|
|
148
|
+
|
|
149
|
+
// Comments
|
|
150
|
+
getComments: this.db.prepare('SELECT * FROM comments WHERE ticket_id = ? ORDER BY timestamp ASC'),
|
|
151
|
+
getComment: this.db.prepare('SELECT * FROM comments WHERE id = ? AND ticket_id = ?'),
|
|
152
|
+
insertComment: this.db.prepare(`
|
|
153
|
+
INSERT INTO comments (id, ticket_id, timestamp, type, author, content, metadata)
|
|
154
|
+
VALUES (@id, @ticket_id, @timestamp, @type, @author, @content, @metadata)
|
|
155
|
+
`),
|
|
156
|
+
updateComment: this.db.prepare(`
|
|
157
|
+
UPDATE comments SET content = @content, metadata = @metadata, editedAt = @editedAt
|
|
158
|
+
WHERE id = @id AND ticket_id = @ticket_id
|
|
159
|
+
`),
|
|
160
|
+
deleteComment: this.db.prepare('DELETE FROM comments WHERE id = ? AND ticket_id = ?'),
|
|
161
|
+
|
|
162
|
+
// Stats
|
|
163
|
+
countByStatus: this.db.prepare('SELECT status, COUNT(*) as count FROM tickets GROUP BY status'),
|
|
164
|
+
countByPriority: this.db.prepare('SELECT priority, COUNT(*) as count FROM tickets GROUP BY priority'),
|
|
165
|
+
|
|
166
|
+
// API Keys
|
|
167
|
+
getApiKey: this.db.prepare('SELECT * FROM api_keys WHERE key = ? AND enabled = 1'),
|
|
168
|
+
updateApiKeyUsage: this.db.prepare('UPDATE api_keys SET last_used = ? WHERE key = ?'),
|
|
169
|
+
|
|
170
|
+
// Rate limiting
|
|
171
|
+
getRateLimit: this.db.prepare('SELECT * FROM rate_limits WHERE identifier = ? AND window_start = ?'),
|
|
172
|
+
upsertRateLimit: this.db.prepare(`
|
|
173
|
+
INSERT INTO rate_limits (identifier, window_start, request_count)
|
|
174
|
+
VALUES (@identifier, @window_start, 1)
|
|
175
|
+
ON CONFLICT(identifier, window_start) DO UPDATE SET request_count = request_count + 1
|
|
176
|
+
`),
|
|
177
|
+
cleanOldRateLimits: this.db.prepare('DELETE FROM rate_limits WHERE window_start < ?')
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async close() {
|
|
182
|
+
if (this.db) {
|
|
183
|
+
this.db.close();
|
|
184
|
+
this.db = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Helper to build full ticket object with relations and actions
|
|
189
|
+
_buildFullTicket(row) {
|
|
190
|
+
if (!row) return null;
|
|
191
|
+
|
|
192
|
+
const relatedRows = this.stmts.getRelatedTickets.all(row.id);
|
|
193
|
+
const swarmActions = this.stmts.getSwarmActions.all(row.id);
|
|
194
|
+
const comments = this.stmts.getComments.all(row.id);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
...row,
|
|
198
|
+
relatedTickets: relatedRows.map(r => r.related_ticket_id),
|
|
199
|
+
swarmActions: swarmActions.map(a => ({
|
|
200
|
+
timestamp: a.timestamp,
|
|
201
|
+
action: a.action,
|
|
202
|
+
result: a.result
|
|
203
|
+
})),
|
|
204
|
+
comments: comments.map(c => ({
|
|
205
|
+
id: c.id,
|
|
206
|
+
timestamp: c.timestamp,
|
|
207
|
+
type: c.type,
|
|
208
|
+
author: c.author,
|
|
209
|
+
content: c.content,
|
|
210
|
+
metadata: JSON.parse(c.metadata || '{}'),
|
|
211
|
+
editedAt: c.editedAt
|
|
212
|
+
}))
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ==================== TICKET OPERATIONS ====================
|
|
217
|
+
|
|
218
|
+
async getAllTickets(filters = {}) {
|
|
219
|
+
let query = 'SELECT * FROM tickets WHERE 1=1';
|
|
220
|
+
const params = [];
|
|
221
|
+
|
|
222
|
+
if (filters.status) {
|
|
223
|
+
query += ' AND status = ?';
|
|
224
|
+
params.push(filters.status);
|
|
225
|
+
}
|
|
226
|
+
if (filters.priority) {
|
|
227
|
+
query += ' AND priority = ?';
|
|
228
|
+
params.push(filters.priority);
|
|
229
|
+
}
|
|
230
|
+
if (filters.route) {
|
|
231
|
+
query += ' AND route LIKE ?';
|
|
232
|
+
params.push(`%${filters.route}%`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
query += ' ORDER BY createdAt DESC';
|
|
236
|
+
|
|
237
|
+
const rows = this.db.prepare(query).all(...params);
|
|
238
|
+
return rows.map(row => this._buildFullTicket(row));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async getTicket(id) {
|
|
242
|
+
const row = this.stmts.getTicket.get(id);
|
|
243
|
+
return this._buildFullTicket(row);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async createTicket(ticketData) {
|
|
247
|
+
const id = this.generateTicketId();
|
|
248
|
+
const now = new Date().toISOString();
|
|
249
|
+
|
|
250
|
+
const ticket = {
|
|
251
|
+
id,
|
|
252
|
+
route: ticketData.route || '',
|
|
253
|
+
f12Errors: ticketData.f12Errors || '',
|
|
254
|
+
serverErrors: ticketData.serverErrors || '',
|
|
255
|
+
description: ticketData.description || '',
|
|
256
|
+
status: ticketData.status || 'open',
|
|
257
|
+
priority: ticketData.priority || null,
|
|
258
|
+
namespace: ticketData.namespace || null,
|
|
259
|
+
createdAt: now,
|
|
260
|
+
updatedAt: now
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const transaction = this.db.transaction(() => {
|
|
264
|
+
this.stmts.insertTicket.run(ticket);
|
|
265
|
+
|
|
266
|
+
// Insert related tickets
|
|
267
|
+
if (ticketData.relatedTickets && ticketData.relatedTickets.length > 0) {
|
|
268
|
+
for (const relatedId of ticketData.relatedTickets) {
|
|
269
|
+
this.stmts.insertRelation.run(id, relatedId);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Insert swarm actions
|
|
274
|
+
if (ticketData.swarmActions && ticketData.swarmActions.length > 0) {
|
|
275
|
+
for (const action of ticketData.swarmActions) {
|
|
276
|
+
this.stmts.insertSwarmAction.run({
|
|
277
|
+
ticket_id: id,
|
|
278
|
+
timestamp: action.timestamp || now,
|
|
279
|
+
action: action.action,
|
|
280
|
+
result: action.result || null
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Insert comments
|
|
286
|
+
if (ticketData.comments && ticketData.comments.length > 0) {
|
|
287
|
+
for (const comment of ticketData.comments) {
|
|
288
|
+
this.stmts.insertComment.run({
|
|
289
|
+
id: comment.id || this.generateCommentId(),
|
|
290
|
+
ticket_id: id,
|
|
291
|
+
timestamp: comment.timestamp || now,
|
|
292
|
+
type: comment.type || 'human',
|
|
293
|
+
author: comment.author || 'anonymous',
|
|
294
|
+
content: comment.content || '',
|
|
295
|
+
metadata: JSON.stringify(comment.metadata || {})
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
transaction();
|
|
302
|
+
return this.getTicket(id);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async updateTicket(id, updates) {
|
|
306
|
+
const existing = await this.getTicket(id);
|
|
307
|
+
if (!existing) return null;
|
|
308
|
+
|
|
309
|
+
const now = new Date().toISOString();
|
|
310
|
+
|
|
311
|
+
const transaction = this.db.transaction(() => {
|
|
312
|
+
this.stmts.updateTicket.run({
|
|
313
|
+
id,
|
|
314
|
+
route: updates.route,
|
|
315
|
+
f12Errors: updates.f12Errors,
|
|
316
|
+
serverErrors: updates.serverErrors,
|
|
317
|
+
description: updates.description,
|
|
318
|
+
status: updates.status,
|
|
319
|
+
priority: updates.priority !== undefined ? updates.priority : existing.priority,
|
|
320
|
+
namespace: updates.namespace !== undefined ? updates.namespace : existing.namespace,
|
|
321
|
+
updatedAt: now
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Update related tickets if provided
|
|
325
|
+
if (updates.relatedTickets !== undefined) {
|
|
326
|
+
this.stmts.deleteRelations.run(id);
|
|
327
|
+
for (const relatedId of updates.relatedTickets) {
|
|
328
|
+
this.stmts.insertRelation.run(id, relatedId);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
transaction();
|
|
334
|
+
return this.getTicket(id);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async deleteTicket(id) {
|
|
338
|
+
const result = this.stmts.deleteTicket.run(id);
|
|
339
|
+
return result.changes > 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ==================== SWARM ACTION OPERATIONS ====================
|
|
343
|
+
|
|
344
|
+
async addSwarmAction(ticketId, action) {
|
|
345
|
+
const ticket = await this.getTicket(ticketId);
|
|
346
|
+
if (!ticket) return null;
|
|
347
|
+
|
|
348
|
+
const now = new Date().toISOString();
|
|
349
|
+
|
|
350
|
+
this.stmts.insertSwarmAction.run({
|
|
351
|
+
ticket_id: ticketId,
|
|
352
|
+
timestamp: now,
|
|
353
|
+
action: action.action,
|
|
354
|
+
result: action.result || null
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
this.db.prepare('UPDATE tickets SET updatedAt = ? WHERE id = ?').run(now, ticketId);
|
|
358
|
+
|
|
359
|
+
return this.getTicket(ticketId);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ==================== COMMENT OPERATIONS ====================
|
|
363
|
+
|
|
364
|
+
async addComment(ticketId, commentData) {
|
|
365
|
+
const ticket = await this.getTicket(ticketId);
|
|
366
|
+
if (!ticket) return null;
|
|
367
|
+
|
|
368
|
+
const now = new Date().toISOString();
|
|
369
|
+
const commentId = this.generateCommentId();
|
|
370
|
+
|
|
371
|
+
const comment = {
|
|
372
|
+
id: commentId,
|
|
373
|
+
ticket_id: ticketId,
|
|
374
|
+
timestamp: now,
|
|
375
|
+
type: commentData.type || 'human',
|
|
376
|
+
author: commentData.author || 'anonymous',
|
|
377
|
+
content: commentData.content || '',
|
|
378
|
+
metadata: JSON.stringify(commentData.metadata || {})
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
this.stmts.insertComment.run(comment);
|
|
382
|
+
this.db.prepare('UPDATE tickets SET updatedAt = ? WHERE id = ?').run(now, ticketId);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
id: commentId,
|
|
386
|
+
timestamp: now,
|
|
387
|
+
type: comment.type,
|
|
388
|
+
author: comment.author,
|
|
389
|
+
content: comment.content,
|
|
390
|
+
metadata: commentData.metadata || {}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async getComments(ticketId) {
|
|
395
|
+
const rows = this.stmts.getComments.all(ticketId);
|
|
396
|
+
return rows.map(c => ({
|
|
397
|
+
id: c.id,
|
|
398
|
+
timestamp: c.timestamp,
|
|
399
|
+
type: c.type,
|
|
400
|
+
author: c.author,
|
|
401
|
+
content: c.content,
|
|
402
|
+
metadata: JSON.parse(c.metadata || '{}'),
|
|
403
|
+
editedAt: c.editedAt
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async updateComment(ticketId, commentId, updates) {
|
|
408
|
+
const existing = this.stmts.getComment.get(commentId, ticketId);
|
|
409
|
+
if (!existing) return null;
|
|
410
|
+
|
|
411
|
+
const now = new Date().toISOString();
|
|
412
|
+
|
|
413
|
+
this.stmts.updateComment.run({
|
|
414
|
+
id: commentId,
|
|
415
|
+
ticket_id: ticketId,
|
|
416
|
+
content: updates.content !== undefined ? updates.content : existing.content,
|
|
417
|
+
metadata: JSON.stringify(updates.metadata || JSON.parse(existing.metadata || '{}')),
|
|
418
|
+
editedAt: now
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
this.db.prepare('UPDATE tickets SET updatedAt = ? WHERE id = ?').run(now, ticketId);
|
|
422
|
+
|
|
423
|
+
const updated = this.stmts.getComment.get(commentId, ticketId);
|
|
424
|
+
return {
|
|
425
|
+
id: updated.id,
|
|
426
|
+
timestamp: updated.timestamp,
|
|
427
|
+
type: updated.type,
|
|
428
|
+
author: updated.author,
|
|
429
|
+
content: updated.content,
|
|
430
|
+
metadata: JSON.parse(updated.metadata || '{}'),
|
|
431
|
+
editedAt: updated.editedAt
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async deleteComment(ticketId, commentId) {
|
|
436
|
+
const result = this.stmts.deleteComment.run(commentId, ticketId);
|
|
437
|
+
if (result.changes > 0) {
|
|
438
|
+
const now = new Date().toISOString();
|
|
439
|
+
this.db.prepare('UPDATE tickets SET updatedAt = ? WHERE id = ?').run(now, ticketId);
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ==================== STATS OPERATIONS ====================
|
|
446
|
+
|
|
447
|
+
async getStats() {
|
|
448
|
+
const totalRow = this.db.prepare('SELECT COUNT(*) as total FROM tickets').get();
|
|
449
|
+
const statusRows = this.stmts.countByStatus.all();
|
|
450
|
+
const priorityRows = this.stmts.countByPriority.all();
|
|
451
|
+
|
|
452
|
+
const byStatus = { open: 0, inProgress: 0, fixed: 0, closed: 0 };
|
|
453
|
+
const byPriority = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
454
|
+
|
|
455
|
+
statusRows.forEach(row => {
|
|
456
|
+
if (row.status === 'in-progress') byStatus.inProgress = row.count;
|
|
457
|
+
else if (byStatus[row.status] !== undefined) byStatus[row.status] = row.count;
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
priorityRows.forEach(row => {
|
|
461
|
+
if (row.priority && byPriority[row.priority] !== undefined) {
|
|
462
|
+
byPriority[row.priority] = row.count;
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
total: totalRow.total,
|
|
468
|
+
byStatus,
|
|
469
|
+
byPriority
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ==================== BUG REPORT OPERATIONS ====================
|
|
474
|
+
|
|
475
|
+
async createBugReport(reportData, apiKey = null) {
|
|
476
|
+
// Validate API key if provided
|
|
477
|
+
if (apiKey) {
|
|
478
|
+
const keyRecord = this.stmts.getApiKey.get(apiKey);
|
|
479
|
+
if (!keyRecord) {
|
|
480
|
+
throw new Error('Invalid API key');
|
|
481
|
+
}
|
|
482
|
+
// Update last used
|
|
483
|
+
this.stmts.updateApiKeyUsage.run(new Date().toISOString(), apiKey);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Check rate limit (IP-based if no API key)
|
|
487
|
+
const identifier = apiKey || reportData.ip || 'anonymous';
|
|
488
|
+
const windowStart = new Date();
|
|
489
|
+
windowStart.setMinutes(0, 0, 0); // Hour window
|
|
490
|
+
const windowKey = windowStart.toISOString();
|
|
491
|
+
|
|
492
|
+
// Clean old rate limit records
|
|
493
|
+
const oldWindow = new Date(Date.now() - 3600000).toISOString();
|
|
494
|
+
this.stmts.cleanOldRateLimits.run(oldWindow);
|
|
495
|
+
|
|
496
|
+
// Check current count
|
|
497
|
+
const rateLimit = this.stmts.getRateLimit.get(identifier, windowKey);
|
|
498
|
+
const limit = apiKey ? 1000 : 10; // Higher limit for API key users
|
|
499
|
+
|
|
500
|
+
if (rateLimit && rateLimit.request_count >= limit) {
|
|
501
|
+
throw new Error('Rate limit exceeded. Please try again later.');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Increment rate limit
|
|
505
|
+
this.stmts.upsertRateLimit.run({ identifier, window_start: windowKey });
|
|
506
|
+
|
|
507
|
+
// Create the ticket
|
|
508
|
+
const ticket = await this.createTicket({
|
|
509
|
+
route: reportData.location || reportData.route || 'unknown',
|
|
510
|
+
f12Errors: reportData.clientError || reportData.f12Errors || '',
|
|
511
|
+
serverErrors: '',
|
|
512
|
+
description: reportData.description || '',
|
|
513
|
+
status: 'open',
|
|
514
|
+
priority: null,
|
|
515
|
+
swarmActions: [{
|
|
516
|
+
timestamp: new Date().toISOString(),
|
|
517
|
+
action: 'bug-report-submitted',
|
|
518
|
+
result: `Bug report submitted via widget. User agent: ${reportData.userAgent || 'unknown'}`
|
|
519
|
+
}]
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
id: ticket.id,
|
|
524
|
+
status: 'submitted',
|
|
525
|
+
message: 'Bug report received. Thank you!'
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ==================== API KEY MANAGEMENT ====================
|
|
530
|
+
|
|
531
|
+
async createApiKey(name = null) {
|
|
532
|
+
const key = 'stk_' + require('crypto').randomBytes(24).toString('hex');
|
|
533
|
+
const now = new Date().toISOString();
|
|
534
|
+
|
|
535
|
+
this.db.prepare(`
|
|
536
|
+
INSERT INTO api_keys (key, name, created_at) VALUES (?, ?, ?)
|
|
537
|
+
`).run(key, name, now);
|
|
538
|
+
|
|
539
|
+
return { key, name, createdAt: now };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async listApiKeys() {
|
|
543
|
+
return this.db.prepare('SELECT id, name, created_at, last_used, enabled FROM api_keys').all();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async revokeApiKey(key) {
|
|
547
|
+
const result = this.db.prepare('UPDATE api_keys SET enabled = 0 WHERE key = ?').run(key);
|
|
548
|
+
return result.changes > 0;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
module.exports = SqliteAdapter;
|