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
package/ticket-server.js
CHANGED
|
@@ -1,327 +1,483 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Swarm Tickets Server
|
|
5
|
+
* REST API for ticket management with SQL and JSON storage backends
|
|
6
|
+
*/
|
|
7
|
+
|
|
3
8
|
const express = require('express');
|
|
4
|
-
const fs = require('fs').promises;
|
|
5
9
|
const path = require('path');
|
|
6
10
|
const cors = require('cors');
|
|
11
|
+
const { createStorageAdapter, getStorageConfig } = require('./lib/storage');
|
|
7
12
|
|
|
8
13
|
const app = express();
|
|
9
14
|
const PORT = process.env.PORT || 3456;
|
|
10
15
|
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
const projectRoot = process.cwd();
|
|
14
|
-
const TICKETS_FILE = path.join(projectRoot, 'tickets.json');
|
|
15
|
-
const BACKUP_DIR = path.join(projectRoot, 'ticket-backups');
|
|
16
|
-
|
|
17
|
-
// Serve static files from the package directory (where ticket-tracker.html is)
|
|
18
|
-
const packageDir = __dirname;
|
|
16
|
+
// Storage adapter instance
|
|
17
|
+
let storage = null;
|
|
19
18
|
|
|
19
|
+
// Middleware
|
|
20
20
|
app.use(cors());
|
|
21
21
|
app.use(express.json());
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
|
|
23
|
+
// Serve static files from the project root and package directory
|
|
24
|
+
const projectRoot = process.cwd();
|
|
25
|
+
const packageDir = __dirname;
|
|
26
|
+
app.use(express.static(projectRoot));
|
|
27
|
+
app.use(express.static(packageDir));
|
|
24
28
|
|
|
25
29
|
// Auto-find available port
|
|
26
30
|
async function findAvailablePort(startPort) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return new Promise((resolve) => {
|
|
30
|
-
const server = net.createServer();
|
|
31
|
-
server.unref();
|
|
32
|
-
server.on('error', () => {
|
|
33
|
-
resolve(findAvailablePort(startPort + 1));
|
|
34
|
-
});
|
|
35
|
-
server.listen(startPort, () => {
|
|
36
|
-
const { port } = server.address();
|
|
37
|
-
server.close(() => {
|
|
38
|
-
resolve(port);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Initialize tickets file if it doesn't exist
|
|
45
|
-
async function initTicketsFile() {
|
|
46
|
-
try {
|
|
47
|
-
await fs.access(TICKETS_FILE);
|
|
48
|
-
} catch {
|
|
49
|
-
await fs.writeFile(TICKETS_FILE, JSON.stringify({ tickets: [] }, null, 2));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Create backup directory
|
|
53
|
-
try {
|
|
54
|
-
await fs.mkdir(BACKUP_DIR, { recursive: true });
|
|
55
|
-
} catch (error) {
|
|
56
|
-
// Directory already exists, that's fine
|
|
57
|
-
}
|
|
58
|
-
}
|
|
31
|
+
const net = require('net');
|
|
59
32
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Check if tickets.json exists
|
|
74
|
-
await fs.access(TICKETS_FILE);
|
|
75
|
-
|
|
76
|
-
// Create timestamped backup in folder
|
|
77
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
78
|
-
const timestampedBackup = path.join(BACKUP_DIR, `tickets-${timestamp}.json`);
|
|
79
|
-
await fs.copyFile(TICKETS_FILE, timestampedBackup);
|
|
80
|
-
|
|
81
|
-
// Rotate backups - keep last 10
|
|
82
|
-
const files = await fs.readdir(BACKUP_DIR);
|
|
83
|
-
const backupFiles = files
|
|
84
|
-
.filter(f => f.startsWith('tickets-') && f.endsWith('.json'))
|
|
85
|
-
.sort()
|
|
86
|
-
.reverse();
|
|
87
|
-
|
|
88
|
-
if (backupFiles.length > 10) {
|
|
89
|
-
for (let i = 10; i < backupFiles.length; i++) {
|
|
90
|
-
await fs.unlink(path.join(BACKUP_DIR, backupFiles[i]));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
console.log(`✅ Backup created: ${timestampedBackup}`);
|
|
95
|
-
} catch (error) {
|
|
96
|
-
console.warn('⚠️ Backup failed:', error.message);
|
|
97
|
-
}
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const server = net.createServer();
|
|
35
|
+
server.unref();
|
|
36
|
+
server.on('error', () => {
|
|
37
|
+
resolve(findAvailablePort(startPort + 1));
|
|
38
|
+
});
|
|
39
|
+
server.listen(startPort, () => {
|
|
40
|
+
const { port } = server.address();
|
|
41
|
+
server.close(() => {
|
|
42
|
+
resolve(port);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
98
46
|
}
|
|
99
47
|
|
|
100
|
-
//
|
|
101
|
-
async function writeTickets(data) {
|
|
102
|
-
await createBackup();
|
|
103
|
-
await fs.writeFile(TICKETS_FILE, JSON.stringify(data, null, 2));
|
|
104
|
-
}
|
|
48
|
+
// ==================== TICKET ENDPOINTS ====================
|
|
105
49
|
|
|
106
50
|
// GET all tickets
|
|
107
51
|
app.get('/api/tickets', async (req, res) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
52
|
+
try {
|
|
53
|
+
const filters = {};
|
|
54
|
+
if (req.query.status) filters.status = req.query.status;
|
|
55
|
+
if (req.query.priority) filters.priority = req.query.priority;
|
|
56
|
+
if (req.query.route) filters.route = req.query.route;
|
|
57
|
+
|
|
58
|
+
const tickets = await storage.getAllTickets(filters);
|
|
59
|
+
res.json(tickets);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Error getting tickets:', error);
|
|
62
|
+
res.status(500).json({ error: error.message });
|
|
63
|
+
}
|
|
114
64
|
});
|
|
115
65
|
|
|
116
66
|
// GET single ticket
|
|
117
67
|
app.get('/api/tickets/:id', async (req, res) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
res.status(404).json({ error: 'Ticket not found' });
|
|
125
|
-
}
|
|
126
|
-
} catch (error) {
|
|
127
|
-
res.status(500).json({ error: error.message });
|
|
68
|
+
try {
|
|
69
|
+
const ticket = await storage.getTicket(req.params.id);
|
|
70
|
+
if (ticket) {
|
|
71
|
+
res.json(ticket);
|
|
72
|
+
} else {
|
|
73
|
+
res.status(404).json({ error: 'Ticket not found' });
|
|
128
74
|
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Error getting ticket:', error);
|
|
77
|
+
res.status(500).json({ error: error.message });
|
|
78
|
+
}
|
|
129
79
|
});
|
|
130
80
|
|
|
131
81
|
// POST new ticket
|
|
132
82
|
app.post('/api/tickets', async (req, res) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
serverErrors: req.body.serverErrors || '',
|
|
141
|
-
description: req.body.description || '',
|
|
142
|
-
status: req.body.status || 'open',
|
|
143
|
-
priority: req.body.priority || null,
|
|
144
|
-
relatedTickets: req.body.relatedTickets || [],
|
|
145
|
-
swarmActions: req.body.swarmActions || [],
|
|
146
|
-
namespace: req.body.namespace || null,
|
|
147
|
-
createdAt: new Date().toISOString(),
|
|
148
|
-
updatedAt: new Date().toISOString()
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
data.tickets.push(ticket);
|
|
152
|
-
await writeTickets(data);
|
|
153
|
-
|
|
154
|
-
res.status(201).json(ticket);
|
|
155
|
-
} catch (error) {
|
|
156
|
-
res.status(500).json({ error: error.message });
|
|
157
|
-
}
|
|
83
|
+
try {
|
|
84
|
+
const ticket = await storage.createTicket(req.body);
|
|
85
|
+
res.status(201).json(ticket);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('Error creating ticket:', error);
|
|
88
|
+
res.status(500).json({ error: error.message });
|
|
89
|
+
}
|
|
158
90
|
});
|
|
159
91
|
|
|
160
92
|
// PATCH update ticket
|
|
161
93
|
app.patch('/api/tickets/:id', async (req, res) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Update allowed fields
|
|
171
|
-
const allowedFields = [
|
|
172
|
-
'status', 'priority', 'relatedTickets', 'swarmActions',
|
|
173
|
-
'namespace', 'description', 'f12Errors', 'serverErrors'
|
|
174
|
-
];
|
|
175
|
-
|
|
176
|
-
allowedFields.forEach(field => {
|
|
177
|
-
if (req.body[field] !== undefined) {
|
|
178
|
-
data.tickets[ticketIndex][field] = req.body[field];
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
data.tickets[ticketIndex].updatedAt = new Date().toISOString();
|
|
183
|
-
|
|
184
|
-
await writeTickets(data);
|
|
185
|
-
res.json(data.tickets[ticketIndex]);
|
|
186
|
-
} catch (error) {
|
|
187
|
-
res.status(500).json({ error: error.message });
|
|
94
|
+
try {
|
|
95
|
+
const ticket = await storage.updateTicket(req.params.id, req.body);
|
|
96
|
+
if (ticket) {
|
|
97
|
+
res.json(ticket);
|
|
98
|
+
} else {
|
|
99
|
+
res.status(404).json({ error: 'Ticket not found' });
|
|
188
100
|
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('Error updating ticket:', error);
|
|
103
|
+
res.status(500).json({ error: error.message });
|
|
104
|
+
}
|
|
189
105
|
});
|
|
190
106
|
|
|
107
|
+
// DELETE ticket
|
|
108
|
+
app.delete('/api/tickets/:id', async (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const deleted = await storage.deleteTicket(req.params.id);
|
|
111
|
+
if (deleted) {
|
|
112
|
+
res.json({ message: 'Ticket deleted' });
|
|
113
|
+
} else {
|
|
114
|
+
res.status(404).json({ error: 'Ticket not found' });
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Error deleting ticket:', error);
|
|
118
|
+
res.status(500).json({ error: error.message });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ==================== TICKET STATUS SHORTCUTS ====================
|
|
123
|
+
|
|
124
|
+
// POST close ticket
|
|
125
|
+
app.post('/api/tickets/:id/close', async (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const ticket = await storage.getTicket(req.params.id);
|
|
128
|
+
if (!ticket) {
|
|
129
|
+
return res.status(404).json({ error: 'Ticket not found' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Add a swarm action documenting the close
|
|
133
|
+
await storage.addSwarmAction(req.params.id, {
|
|
134
|
+
action: 'status-change',
|
|
135
|
+
result: `Status changed from "${ticket.status}" to "closed"${req.body.reason ? `. Reason: ${req.body.reason}` : ''}`
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Update status
|
|
139
|
+
const updated = await storage.updateTicket(req.params.id, { status: 'closed' });
|
|
140
|
+
res.json(updated);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Error closing ticket:', error);
|
|
143
|
+
res.status(500).json({ error: error.message });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// POST reopen ticket
|
|
148
|
+
app.post('/api/tickets/:id/reopen', async (req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
const ticket = await storage.getTicket(req.params.id);
|
|
151
|
+
if (!ticket) {
|
|
152
|
+
return res.status(404).json({ error: 'Ticket not found' });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await storage.addSwarmAction(req.params.id, {
|
|
156
|
+
action: 'status-change',
|
|
157
|
+
result: `Status changed from "${ticket.status}" to "open"${req.body.reason ? `. Reason: ${req.body.reason}` : ''}`
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const updated = await storage.updateTicket(req.params.id, { status: 'open' });
|
|
161
|
+
res.json(updated);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('Error reopening ticket:', error);
|
|
164
|
+
res.status(500).json({ error: error.message });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ==================== SWARM ACTION ENDPOINTS ====================
|
|
169
|
+
|
|
191
170
|
// POST add swarm action to ticket
|
|
192
171
|
app.post('/api/tickets/:id/swarm-action', async (req, res) => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const action = {
|
|
202
|
-
timestamp: new Date().toISOString(),
|
|
203
|
-
action: req.body.action,
|
|
204
|
-
result: req.body.result || null
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
ticket.swarmActions.push(action);
|
|
208
|
-
ticket.updatedAt = new Date().toISOString();
|
|
209
|
-
|
|
210
|
-
await writeTickets(data);
|
|
211
|
-
res.json(ticket);
|
|
212
|
-
} catch (error) {
|
|
213
|
-
res.status(500).json({ error: error.message });
|
|
172
|
+
try {
|
|
173
|
+
const ticket = await storage.addSwarmAction(req.params.id, req.body);
|
|
174
|
+
if (ticket) {
|
|
175
|
+
res.json(ticket);
|
|
176
|
+
} else {
|
|
177
|
+
res.status(404).json({ error: 'Ticket not found' });
|
|
214
178
|
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error('Error adding swarm action:', error);
|
|
181
|
+
res.status(500).json({ error: error.message });
|
|
182
|
+
}
|
|
215
183
|
});
|
|
216
184
|
|
|
217
|
-
// POST analyze ticket
|
|
185
|
+
// POST analyze ticket (simple auto-analysis)
|
|
218
186
|
app.post('/api/tickets/:id/analyze', async (req, res) => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
187
|
+
try {
|
|
188
|
+
const ticket = await storage.getTicket(req.params.id);
|
|
189
|
+
if (!ticket) {
|
|
190
|
+
return res.status(404).json({ error: 'Ticket not found' });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Determine priority based on errors
|
|
194
|
+
let priority = 'low';
|
|
195
|
+
if (ticket.f12Errors.toLowerCase().includes('error') ||
|
|
196
|
+
ticket.serverErrors.toLowerCase().includes('error')) {
|
|
197
|
+
priority = 'medium';
|
|
198
|
+
}
|
|
199
|
+
if (ticket.f12Errors.toLowerCase().includes('uncaught') ||
|
|
200
|
+
ticket.serverErrors.toLowerCase().includes('fatal') ||
|
|
201
|
+
ticket.serverErrors.toLowerCase().includes('crash')) {
|
|
202
|
+
priority = 'high';
|
|
203
|
+
}
|
|
204
|
+
if (ticket.route.includes('auth') || ticket.route.includes('payment')) {
|
|
205
|
+
priority = 'critical';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Find related tickets
|
|
209
|
+
const allTickets = await storage.getAllTickets();
|
|
210
|
+
const relatedTickets = allTickets
|
|
211
|
+
.filter(t => t.id !== ticket.id && t.route === ticket.route)
|
|
212
|
+
.map(t => t.id)
|
|
213
|
+
.slice(0, 3);
|
|
214
|
+
|
|
215
|
+
// Update ticket
|
|
216
|
+
await storage.addSwarmAction(req.params.id, {
|
|
217
|
+
action: 'auto-analysis',
|
|
218
|
+
result: `Priority set to ${priority}, found ${relatedTickets.length} related tickets`
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const updated = await storage.updateTicket(req.params.id, {
|
|
222
|
+
priority,
|
|
223
|
+
relatedTickets
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
res.json(updated);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error('Error analyzing ticket:', error);
|
|
229
|
+
res.status(500).json({ error: error.message });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ==================== COMMENT ENDPOINTS ====================
|
|
234
|
+
|
|
235
|
+
// GET all comments for a ticket
|
|
236
|
+
app.get('/api/tickets/:id/comments', async (req, res) => {
|
|
237
|
+
try {
|
|
238
|
+
const comments = await storage.getComments(req.params.id);
|
|
239
|
+
res.json(comments);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error('Error getting comments:', error);
|
|
242
|
+
res.status(500).json({ error: error.message });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// POST add comment to ticket
|
|
247
|
+
app.post('/api/tickets/:id/comments', async (req, res) => {
|
|
248
|
+
try {
|
|
249
|
+
const comment = await storage.addComment(req.params.id, {
|
|
250
|
+
type: req.body.type || 'human',
|
|
251
|
+
author: req.body.author || 'anonymous',
|
|
252
|
+
content: req.body.content || '',
|
|
253
|
+
metadata: req.body.metadata || {}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (comment) {
|
|
257
|
+
res.status(201).json(comment);
|
|
258
|
+
} else {
|
|
259
|
+
res.status(404).json({ error: 'Ticket not found' });
|
|
264
260
|
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('Error adding comment:', error);
|
|
263
|
+
res.status(500).json({ error: error.message });
|
|
264
|
+
}
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
-
//
|
|
268
|
-
app.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
267
|
+
// PATCH update comment
|
|
268
|
+
app.patch('/api/tickets/:ticketId/comments/:commentId', async (req, res) => {
|
|
269
|
+
try {
|
|
270
|
+
const comment = await storage.updateComment(
|
|
271
|
+
req.params.ticketId,
|
|
272
|
+
req.params.commentId,
|
|
273
|
+
{
|
|
274
|
+
content: req.body.content,
|
|
275
|
+
metadata: req.body.metadata
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (comment) {
|
|
280
|
+
res.json(comment);
|
|
281
|
+
} else {
|
|
282
|
+
res.status(404).json({ error: 'Comment not found' });
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error('Error updating comment:', error);
|
|
286
|
+
res.status(500).json({ error: error.message });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// DELETE comment
|
|
291
|
+
app.delete('/api/tickets/:ticketId/comments/:commentId', async (req, res) => {
|
|
292
|
+
try {
|
|
293
|
+
const deleted = await storage.deleteComment(
|
|
294
|
+
req.params.ticketId,
|
|
295
|
+
req.params.commentId
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
if (deleted) {
|
|
299
|
+
res.json({ message: 'Comment deleted' });
|
|
300
|
+
} else {
|
|
301
|
+
res.status(404).json({ error: 'Comment not found' });
|
|
283
302
|
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error('Error deleting comment:', error);
|
|
305
|
+
res.status(500).json({ error: error.message });
|
|
306
|
+
}
|
|
284
307
|
});
|
|
285
308
|
|
|
309
|
+
// ==================== STATS ENDPOINTS ====================
|
|
310
|
+
|
|
286
311
|
// GET stats
|
|
287
312
|
app.get('/api/stats', async (req, res) => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
313
|
+
try {
|
|
314
|
+
const stats = await storage.getStats();
|
|
315
|
+
res.json(stats);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error('Error getting stats:', error);
|
|
318
|
+
res.status(500).json({ error: error.message });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ==================== BUG REPORT ENDPOINTS ====================
|
|
323
|
+
|
|
324
|
+
// POST bug report (limited access - for end users)
|
|
325
|
+
app.post('/api/bug-report', async (req, res) => {
|
|
326
|
+
try {
|
|
327
|
+
// Extract API key from header or body
|
|
328
|
+
const apiKey = req.headers['x-api-key'] || req.body.apiKey;
|
|
329
|
+
|
|
330
|
+
// Get client IP for rate limiting
|
|
331
|
+
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
332
|
+
|
|
333
|
+
const result = await storage.createBugReport({
|
|
334
|
+
...req.body,
|
|
335
|
+
ip,
|
|
336
|
+
userAgent: req.headers['user-agent']
|
|
337
|
+
}, apiKey);
|
|
338
|
+
|
|
339
|
+
res.status(201).json(result);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error.message.includes('Rate limit')) {
|
|
342
|
+
res.status(429).json({ error: error.message });
|
|
343
|
+
} else if (error.message.includes('Invalid API key')) {
|
|
344
|
+
res.status(401).json({ error: error.message });
|
|
345
|
+
} else {
|
|
346
|
+
console.error('Error creating bug report:', error);
|
|
347
|
+
res.status(500).json({ error: 'Failed to submit bug report' });
|
|
311
348
|
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ==================== ADMIN ENDPOINTS ====================
|
|
353
|
+
|
|
354
|
+
// GET storage info
|
|
355
|
+
app.get('/api/admin/storage', (req, res) => {
|
|
356
|
+
const config = getStorageConfig();
|
|
357
|
+
res.json({
|
|
358
|
+
type: config.type,
|
|
359
|
+
// Don't expose sensitive info
|
|
360
|
+
configured: true
|
|
361
|
+
});
|
|
312
362
|
});
|
|
313
363
|
|
|
314
|
-
//
|
|
315
|
-
|
|
364
|
+
// POST create API key (for bug report widget)
|
|
365
|
+
app.post('/api/admin/api-keys', async (req, res) => {
|
|
366
|
+
try {
|
|
367
|
+
if (typeof storage.createApiKey !== 'function') {
|
|
368
|
+
return res.status(501).json({ error: 'API key management not supported for this storage backend' });
|
|
369
|
+
}
|
|
370
|
+
const apiKey = await storage.createApiKey(req.body.name);
|
|
371
|
+
res.status(201).json(apiKey);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error('Error creating API key:', error);
|
|
374
|
+
res.status(500).json({ error: error.message });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// GET list API keys
|
|
379
|
+
app.get('/api/admin/api-keys', async (req, res) => {
|
|
380
|
+
try {
|
|
381
|
+
if (typeof storage.listApiKeys !== 'function') {
|
|
382
|
+
return res.status(501).json({ error: 'API key management not supported for this storage backend' });
|
|
383
|
+
}
|
|
384
|
+
const keys = await storage.listApiKeys();
|
|
385
|
+
res.json(keys);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error('Error listing API keys:', error);
|
|
388
|
+
res.status(500).json({ error: error.message });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// DELETE revoke API key
|
|
393
|
+
app.delete('/api/admin/api-keys/:key', async (req, res) => {
|
|
394
|
+
try {
|
|
395
|
+
if (typeof storage.revokeApiKey !== 'function') {
|
|
396
|
+
return res.status(501).json({ error: 'API key management not supported for this storage backend' });
|
|
397
|
+
}
|
|
398
|
+
const revoked = await storage.revokeApiKey(req.params.key);
|
|
399
|
+
if (revoked) {
|
|
400
|
+
res.json({ message: 'API key revoked' });
|
|
401
|
+
} else {
|
|
402
|
+
res.status(404).json({ error: 'API key not found' });
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error('Error revoking API key:', error);
|
|
406
|
+
res.status(500).json({ error: error.message });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// ==================== HEALTH CHECK ====================
|
|
411
|
+
|
|
412
|
+
app.get('/api/health', (req, res) => {
|
|
413
|
+
res.json({
|
|
414
|
+
status: 'ok',
|
|
415
|
+
storage: getStorageConfig().type,
|
|
416
|
+
version: require('./package.json').version
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ==================== INITIALIZE AND START ====================
|
|
421
|
+
|
|
422
|
+
async function start() {
|
|
423
|
+
try {
|
|
424
|
+
// Initialize storage adapter
|
|
425
|
+
const config = getStorageConfig();
|
|
426
|
+
console.log(`📦 Initializing ${config.type} storage...`);
|
|
427
|
+
|
|
428
|
+
storage = await createStorageAdapter();
|
|
429
|
+
console.log(`✅ Storage initialized: ${config.type}`);
|
|
430
|
+
|
|
431
|
+
// Find available port and start server
|
|
316
432
|
const availablePort = await findAvailablePort(PORT);
|
|
317
|
-
|
|
433
|
+
|
|
318
434
|
app.listen(availablePort, () => {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
435
|
+
console.log(`\n🎫 Swarm Tickets API running on http://localhost:${availablePort}`);
|
|
436
|
+
console.log(`📊 Open http://localhost:${availablePort}/ticket-tracker.html to view the UI`);
|
|
437
|
+
|
|
438
|
+
if (config.type === 'json') {
|
|
439
|
+
console.log(`📁 Tickets stored at: ${config.jsonPath}`);
|
|
440
|
+
} else if (config.type === 'sqlite') {
|
|
441
|
+
console.log(`📁 Database: ${config.sqlitePath}`);
|
|
442
|
+
} else if (config.type === 'supabase') {
|
|
443
|
+
console.log(`☁️ Connected to Supabase`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (availablePort !== PORT) {
|
|
447
|
+
console.log(`⚠️ Port ${PORT} was busy, using port ${availablePort} instead`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
console.log(`\n📡 API Endpoints:`);
|
|
451
|
+
console.log(` GET /api/tickets - List all tickets`);
|
|
452
|
+
console.log(` POST /api/tickets - Create ticket`);
|
|
453
|
+
console.log(` GET /api/tickets/:id - Get ticket`);
|
|
454
|
+
console.log(` PATCH /api/tickets/:id - Update ticket`);
|
|
455
|
+
console.log(` DELETE /api/tickets/:id - Delete ticket`);
|
|
456
|
+
console.log(` POST /api/tickets/:id/close - Close ticket`);
|
|
457
|
+
console.log(` POST /api/tickets/:id/comments - Add comment`);
|
|
458
|
+
console.log(` POST /api/bug-report - Submit bug report (rate limited)`);
|
|
459
|
+
console.log(` GET /api/stats - Get statistics\n`);
|
|
326
460
|
});
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error('❌ Failed to start server:', error.message);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Handle graceful shutdown
|
|
468
|
+
process.on('SIGINT', async () => {
|
|
469
|
+
console.log('\nShutting down...');
|
|
470
|
+
if (storage) {
|
|
471
|
+
await storage.close();
|
|
472
|
+
}
|
|
473
|
+
process.exit(0);
|
|
327
474
|
});
|
|
475
|
+
|
|
476
|
+
process.on('SIGTERM', async () => {
|
|
477
|
+
if (storage) {
|
|
478
|
+
await storage.close();
|
|
479
|
+
}
|
|
480
|
+
process.exit(0);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
start();
|