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/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
- // When installed as package, look for tickets.json in the project root (cwd)
12
- // When run standalone, look in the same directory as the script
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
- app.use(express.static(projectRoot)); // Serve from project root first
23
- app.use(express.static(packageDir)); // Fall back to package directory
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
- const net = require('net');
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
- // Read tickets
61
- async function readTickets() {
62
- try {
63
- const data = await fs.readFile(TICKETS_FILE, 'utf8');
64
- return JSON.parse(data);
65
- } catch (error) {
66
- return { tickets: [] };
67
- }
68
- }
69
-
70
- // Create backup before writing
71
- async function createBackup() {
72
- try {
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
- // Write tickets (with automatic backup)
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
- try {
109
- const data = await readTickets();
110
- res.json(data.tickets);
111
- } catch (error) {
112
- res.status(500).json({ error: error.message });
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
- try {
119
- const data = await readTickets();
120
- const ticket = data.tickets.find(t => t.id === req.params.id);
121
- if (ticket) {
122
- res.json(ticket);
123
- } else {
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
- try {
134
- const data = await readTickets();
135
-
136
- const ticket = {
137
- id: 'TKT-' + Date.now(),
138
- route: req.body.route,
139
- f12Errors: req.body.f12Errors || '',
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
- try {
163
- const data = await readTickets();
164
- const ticketIndex = data.tickets.findIndex(t => t.id === req.params.id);
165
-
166
- if (ticketIndex === -1) {
167
- return res.status(404).json({ error: 'Ticket not found' });
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
- try {
194
- const data = await readTickets();
195
- const ticket = data.tickets.find(t => t.id === req.params.id);
196
-
197
- if (!ticket) {
198
- return res.status(404).json({ error: 'Ticket not found' });
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 with swarm (placeholder for swarm integration)
185
+ // POST analyze ticket (simple auto-analysis)
218
186
  app.post('/api/tickets/:id/analyze', async (req, res) => {
219
- try {
220
- const data = await readTickets();
221
- const ticket = data.tickets.find(t => t.id === req.params.id);
222
-
223
- if (!ticket) {
224
- return res.status(404).json({ error: 'Ticket not found' });
225
- }
226
-
227
- // This is where you'd integrate with your swarm to analyze the ticket
228
- // For now, we'll do a simple analysis
229
-
230
- // Determine priority based on errors
231
- let priority = 'low';
232
- if (ticket.f12Errors.toLowerCase().includes('error') ||
233
- ticket.serverErrors.toLowerCase().includes('error')) {
234
- priority = 'medium';
235
- }
236
- if (ticket.f12Errors.toLowerCase().includes('uncaught') ||
237
- ticket.serverErrors.toLowerCase().includes('fatal') ||
238
- ticket.serverErrors.toLowerCase().includes('crash')) {
239
- priority = 'high';
240
- }
241
- if (ticket.route.includes('auth') || ticket.route.includes('payment')) {
242
- priority = 'critical';
243
- }
244
-
245
- // Find related tickets (simple route-based matching)
246
- const relatedTickets = data.tickets
247
- .filter(t => t.id !== ticket.id && t.route === ticket.route)
248
- .map(t => t.id)
249
- .slice(0, 3);
250
-
251
- ticket.priority = priority;
252
- ticket.relatedTickets = relatedTickets;
253
- ticket.swarmActions.push({
254
- timestamp: new Date().toISOString(),
255
- action: 'auto-analysis',
256
- result: `Priority set to ${priority}, found ${relatedTickets.length} related tickets`
257
- });
258
- ticket.updatedAt = new Date().toISOString();
259
-
260
- await writeTickets(data);
261
- res.json(ticket);
262
- } catch (error) {
263
- res.status(500).json({ error: error.message });
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
- // DELETE ticket
268
- app.delete('/api/tickets/:id', async (req, res) => {
269
- try {
270
- const data = await readTickets();
271
- const ticketIndex = data.tickets.findIndex(t => t.id === req.params.id);
272
-
273
- if (ticketIndex === -1) {
274
- return res.status(404).json({ error: 'Ticket not found' });
275
- }
276
-
277
- data.tickets.splice(ticketIndex, 1);
278
- await writeTickets(data);
279
-
280
- res.json({ message: 'Ticket deleted' });
281
- } catch (error) {
282
- res.status(500).json({ error: error.message });
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
- try {
289
- const data = await readTickets();
290
- const tickets = data.tickets;
291
-
292
- const stats = {
293
- total: tickets.length,
294
- byStatus: {
295
- open: tickets.filter(t => t.status === 'open').length,
296
- inProgress: tickets.filter(t => t.status === 'in-progress').length,
297
- fixed: tickets.filter(t => t.status === 'fixed').length,
298
- closed: tickets.filter(t => t.status === 'closed').length
299
- },
300
- byPriority: {
301
- critical: tickets.filter(t => t.priority === 'critical').length,
302
- high: tickets.filter(t => t.priority === 'high').length,
303
- medium: tickets.filter(t => t.priority === 'medium').length,
304
- low: tickets.filter(t => t.priority === 'low').length
305
- }
306
- };
307
-
308
- res.json(stats);
309
- } catch (error) {
310
- res.status(500).json({ error: error.message });
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
- // Initialize and start server
315
- initTicketsFile().then(async () => {
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
- console.log(`🎫 Ticket Tracker API running on http://localhost:${availablePort}`);
320
- console.log(`📊 Open http://localhost:${availablePort}/ticket-tracker.html to view the UI`);
321
- console.log(`📁 Tickets stored at: ${TICKETS_FILE}`);
322
-
323
- if (availablePort !== PORT) {
324
- console.log(`⚠️ Port ${PORT} was busy, using port ${availablePort} instead`);
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();