swarm-tickets 1.0.1 → 2.0.0
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 +0 -0
- package/ticket-cli.js +0 -0
- package/ticket-server.js +425 -269
- package/ticket-tracker.html +459 -132
- package/tickets.example.json +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Storage Adapter
|
|
3
|
+
* Abstract class defining the interface for all storage backends
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class BaseAdapter {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
if (new.target === BaseAdapter) {
|
|
10
|
+
throw new Error('BaseAdapter is abstract and cannot be instantiated directly');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize the storage (create tables, files, etc.)
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
async initialize() {
|
|
19
|
+
throw new Error('initialize() must be implemented');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Close the storage connection
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
*/
|
|
26
|
+
async close() {
|
|
27
|
+
throw new Error('close() must be implemented');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ==================== TICKET OPERATIONS ====================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get all tickets
|
|
34
|
+
* @param {Object} filters - Optional filters (status, priority, route)
|
|
35
|
+
* @returns {Promise<Array>} Array of tickets
|
|
36
|
+
*/
|
|
37
|
+
async getAllTickets(filters = {}) {
|
|
38
|
+
throw new Error('getAllTickets() must be implemented');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get a single ticket by ID
|
|
43
|
+
* @param {string} id - Ticket ID
|
|
44
|
+
* @returns {Promise<Object|null>} Ticket or null if not found
|
|
45
|
+
*/
|
|
46
|
+
async getTicket(id) {
|
|
47
|
+
throw new Error('getTicket() must be implemented');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a new ticket
|
|
52
|
+
* @param {Object} ticketData - Ticket data
|
|
53
|
+
* @returns {Promise<Object>} Created ticket with ID
|
|
54
|
+
*/
|
|
55
|
+
async createTicket(ticketData) {
|
|
56
|
+
throw new Error('createTicket() must be implemented');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Update a ticket
|
|
61
|
+
* @param {string} id - Ticket ID
|
|
62
|
+
* @param {Object} updates - Fields to update
|
|
63
|
+
* @returns {Promise<Object|null>} Updated ticket or null if not found
|
|
64
|
+
*/
|
|
65
|
+
async updateTicket(id, updates) {
|
|
66
|
+
throw new Error('updateTicket() must be implemented');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Delete a ticket
|
|
71
|
+
* @param {string} id - Ticket ID
|
|
72
|
+
* @returns {Promise<boolean>} True if deleted
|
|
73
|
+
*/
|
|
74
|
+
async deleteTicket(id) {
|
|
75
|
+
throw new Error('deleteTicket() must be implemented');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ==================== SWARM ACTION OPERATIONS ====================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Add a swarm action to a ticket
|
|
82
|
+
* @param {string} ticketId - Ticket ID
|
|
83
|
+
* @param {Object} action - Action data (action, result)
|
|
84
|
+
* @returns {Promise<Object>} Updated ticket
|
|
85
|
+
*/
|
|
86
|
+
async addSwarmAction(ticketId, action) {
|
|
87
|
+
throw new Error('addSwarmAction() must be implemented');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ==================== COMMENT OPERATIONS ====================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Add a comment to a ticket
|
|
94
|
+
* @param {string} ticketId - Ticket ID
|
|
95
|
+
* @param {Object} comment - Comment data (author, content, type)
|
|
96
|
+
* @returns {Promise<Object>} Created comment
|
|
97
|
+
*/
|
|
98
|
+
async addComment(ticketId, comment) {
|
|
99
|
+
throw new Error('addComment() must be implemented');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get all comments for a ticket
|
|
104
|
+
* @param {string} ticketId - Ticket ID
|
|
105
|
+
* @returns {Promise<Array>} Array of comments
|
|
106
|
+
*/
|
|
107
|
+
async getComments(ticketId) {
|
|
108
|
+
throw new Error('getComments() must be implemented');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Update a comment
|
|
113
|
+
* @param {string} ticketId - Ticket ID
|
|
114
|
+
* @param {string} commentId - Comment ID
|
|
115
|
+
* @param {Object} updates - Fields to update
|
|
116
|
+
* @returns {Promise<Object|null>} Updated comment or null
|
|
117
|
+
*/
|
|
118
|
+
async updateComment(ticketId, commentId, updates) {
|
|
119
|
+
throw new Error('updateComment() must be implemented');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Delete a comment
|
|
124
|
+
* @param {string} ticketId - Ticket ID
|
|
125
|
+
* @param {string} commentId - Comment ID
|
|
126
|
+
* @returns {Promise<boolean>} True if deleted
|
|
127
|
+
*/
|
|
128
|
+
async deleteComment(ticketId, commentId) {
|
|
129
|
+
throw new Error('deleteComment() must be implemented');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ==================== STATS OPERATIONS ====================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get ticket statistics
|
|
136
|
+
* @returns {Promise<Object>} Statistics object
|
|
137
|
+
*/
|
|
138
|
+
async getStats() {
|
|
139
|
+
throw new Error('getStats() must be implemented');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ==================== BUG REPORT OPERATIONS ====================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create a bug report (limited access endpoint)
|
|
146
|
+
* @param {Object} reportData - Bug report data
|
|
147
|
+
* @param {string} apiKey - Optional API key for validation
|
|
148
|
+
* @returns {Promise<Object>} Created ticket (limited info)
|
|
149
|
+
*/
|
|
150
|
+
async createBugReport(reportData, apiKey = null) {
|
|
151
|
+
throw new Error('createBugReport() must be implemented');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ==================== UTILITY METHODS ====================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate a unique ticket ID
|
|
158
|
+
* @returns {string} Ticket ID
|
|
159
|
+
*/
|
|
160
|
+
generateTicketId() {
|
|
161
|
+
return 'TKT-' + Date.now();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Generate a unique comment ID
|
|
166
|
+
* @returns {string} Comment ID
|
|
167
|
+
*/
|
|
168
|
+
generateCommentId() {
|
|
169
|
+
return 'CMT-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Validate ticket status
|
|
174
|
+
* @param {string} status - Status to validate
|
|
175
|
+
* @returns {boolean} True if valid
|
|
176
|
+
*/
|
|
177
|
+
isValidStatus(status) {
|
|
178
|
+
return ['open', 'in-progress', 'fixed', 'closed'].includes(status);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validate ticket priority
|
|
183
|
+
* @param {string} priority - Priority to validate
|
|
184
|
+
* @returns {boolean} True if valid
|
|
185
|
+
*/
|
|
186
|
+
isValidPriority(priority) {
|
|
187
|
+
return ['critical', 'high', 'medium', 'low'].includes(priority);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Create a backup (if supported)
|
|
192
|
+
* @returns {Promise<string|null>} Backup path or null
|
|
193
|
+
*/
|
|
194
|
+
async createBackup() {
|
|
195
|
+
// Default: no backup support
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = BaseAdapter;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Adapter Factory
|
|
3
|
+
* Supports JSON, SQLite, and Supabase backends
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const JsonAdapter = require('./json-adapter');
|
|
7
|
+
const SqliteAdapter = require('./sqlite-adapter');
|
|
8
|
+
const SupabaseAdapter = require('./supabase-adapter');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get storage configuration from environment or config file
|
|
12
|
+
*/
|
|
13
|
+
function getStorageConfig() {
|
|
14
|
+
// Check environment variables first
|
|
15
|
+
const storageType = process.env.SWARM_TICKETS_STORAGE || 'json';
|
|
16
|
+
|
|
17
|
+
const config = {
|
|
18
|
+
type: storageType,
|
|
19
|
+
// JSON options
|
|
20
|
+
jsonPath: process.env.SWARM_TICKETS_JSON_PATH || './tickets.json',
|
|
21
|
+
backupDir: process.env.SWARM_TICKETS_BACKUP_DIR || './ticket-backups',
|
|
22
|
+
|
|
23
|
+
// SQLite options
|
|
24
|
+
sqlitePath: process.env.SWARM_TICKETS_SQLITE_PATH || './tickets.db',
|
|
25
|
+
|
|
26
|
+
// Supabase options
|
|
27
|
+
supabaseUrl: process.env.SUPABASE_URL || process.env.SWARM_TICKETS_SUPABASE_URL,
|
|
28
|
+
supabaseKey: process.env.SUPABASE_ANON_KEY || process.env.SWARM_TICKETS_SUPABASE_KEY,
|
|
29
|
+
supabaseServiceKey: process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SWARM_TICKETS_SUPABASE_SERVICE_KEY,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return config;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create storage adapter based on configuration
|
|
37
|
+
* @param {Object} overrideConfig - Optional config override
|
|
38
|
+
* @returns {Promise<BaseAdapter>} Storage adapter instance
|
|
39
|
+
*/
|
|
40
|
+
async function createStorageAdapter(overrideConfig = null) {
|
|
41
|
+
const config = overrideConfig || getStorageConfig();
|
|
42
|
+
|
|
43
|
+
let adapter;
|
|
44
|
+
|
|
45
|
+
switch (config.type.toLowerCase()) {
|
|
46
|
+
case 'sqlite':
|
|
47
|
+
adapter = new SqliteAdapter(config);
|
|
48
|
+
break;
|
|
49
|
+
|
|
50
|
+
case 'supabase':
|
|
51
|
+
if (!config.supabaseUrl || !config.supabaseKey) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'Supabase configuration missing. Set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
adapter = new SupabaseAdapter(config);
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case 'json':
|
|
60
|
+
default:
|
|
61
|
+
adapter = new JsonAdapter(config);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Initialize the adapter (create tables, etc.)
|
|
66
|
+
await adapter.initialize();
|
|
67
|
+
|
|
68
|
+
return adapter;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Storage type constants
|
|
73
|
+
*/
|
|
74
|
+
const StorageType = {
|
|
75
|
+
JSON: 'json',
|
|
76
|
+
SQLITE: 'sqlite',
|
|
77
|
+
SUPABASE: 'supabase'
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
createStorageAdapter,
|
|
82
|
+
getStorageConfig,
|
|
83
|
+
StorageType,
|
|
84
|
+
JsonAdapter,
|
|
85
|
+
SqliteAdapter,
|
|
86
|
+
SupabaseAdapter
|
|
87
|
+
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON File Storage Adapter
|
|
3
|
+
* Backwards-compatible with existing tickets.json format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const BaseAdapter = require('./base-adapter');
|
|
9
|
+
|
|
10
|
+
class JsonAdapter extends BaseAdapter {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
super(config);
|
|
13
|
+
this.ticketsPath = path.resolve(config.jsonPath || './tickets.json');
|
|
14
|
+
this.backupDir = path.resolve(config.backupDir || './ticket-backups');
|
|
15
|
+
this.data = { tickets: [] };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async initialize() {
|
|
19
|
+
// Create backup directory
|
|
20
|
+
try {
|
|
21
|
+
await fs.mkdir(this.backupDir, { recursive: true });
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// Directory may already exist
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Load existing data or create new file
|
|
27
|
+
try {
|
|
28
|
+
const content = await fs.readFile(this.ticketsPath, 'utf8');
|
|
29
|
+
this.data = JSON.parse(content);
|
|
30
|
+
// Ensure tickets array exists
|
|
31
|
+
if (!this.data.tickets) {
|
|
32
|
+
this.data.tickets = [];
|
|
33
|
+
}
|
|
34
|
+
// Migrate: add comments array to existing tickets
|
|
35
|
+
this.data.tickets = this.data.tickets.map(ticket => ({
|
|
36
|
+
...ticket,
|
|
37
|
+
comments: ticket.comments || []
|
|
38
|
+
}));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// File doesn't exist, create empty structure
|
|
41
|
+
this.data = { tickets: [] };
|
|
42
|
+
await this._save();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async close() {
|
|
47
|
+
// No connection to close for file-based storage
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async _save() {
|
|
51
|
+
await this.createBackup();
|
|
52
|
+
await fs.writeFile(this.ticketsPath, JSON.stringify(this.data, null, 2));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async createBackup() {
|
|
56
|
+
try {
|
|
57
|
+
await fs.access(this.ticketsPath);
|
|
58
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
59
|
+
const backupPath = path.join(this.backupDir, `tickets-${timestamp}.json`);
|
|
60
|
+
await fs.copyFile(this.ticketsPath, backupPath);
|
|
61
|
+
|
|
62
|
+
// Rotate backups - keep last 10
|
|
63
|
+
const files = await fs.readdir(this.backupDir);
|
|
64
|
+
const backupFiles = files
|
|
65
|
+
.filter(f => f.startsWith('tickets-') && f.endsWith('.json'))
|
|
66
|
+
.sort()
|
|
67
|
+
.reverse();
|
|
68
|
+
|
|
69
|
+
if (backupFiles.length > 10) {
|
|
70
|
+
for (let i = 10; i < backupFiles.length; i++) {
|
|
71
|
+
await fs.unlink(path.join(this.backupDir, backupFiles[i]));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return backupPath;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ==================== TICKET OPERATIONS ====================
|
|
82
|
+
|
|
83
|
+
async getAllTickets(filters = {}) {
|
|
84
|
+
let tickets = [...this.data.tickets];
|
|
85
|
+
|
|
86
|
+
if (filters.status) {
|
|
87
|
+
tickets = tickets.filter(t => t.status === filters.status);
|
|
88
|
+
}
|
|
89
|
+
if (filters.priority) {
|
|
90
|
+
tickets = tickets.filter(t => t.priority === filters.priority);
|
|
91
|
+
}
|
|
92
|
+
if (filters.route) {
|
|
93
|
+
tickets = tickets.filter(t => t.route && t.route.includes(filters.route));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return tickets;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getTicket(id) {
|
|
100
|
+
return this.data.tickets.find(t => t.id === id) || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async createTicket(ticketData) {
|
|
104
|
+
const ticket = {
|
|
105
|
+
id: this.generateTicketId(),
|
|
106
|
+
route: ticketData.route || '',
|
|
107
|
+
f12Errors: ticketData.f12Errors || '',
|
|
108
|
+
serverErrors: ticketData.serverErrors || '',
|
|
109
|
+
description: ticketData.description || '',
|
|
110
|
+
status: ticketData.status || 'open',
|
|
111
|
+
priority: ticketData.priority || null,
|
|
112
|
+
relatedTickets: ticketData.relatedTickets || [],
|
|
113
|
+
swarmActions: ticketData.swarmActions || [],
|
|
114
|
+
comments: ticketData.comments || [],
|
|
115
|
+
namespace: ticketData.namespace || null,
|
|
116
|
+
createdAt: new Date().toISOString(),
|
|
117
|
+
updatedAt: new Date().toISOString()
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
this.data.tickets.push(ticket);
|
|
121
|
+
await this._save();
|
|
122
|
+
|
|
123
|
+
return ticket;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async updateTicket(id, updates) {
|
|
127
|
+
const ticketIndex = this.data.tickets.findIndex(t => t.id === id);
|
|
128
|
+
if (ticketIndex === -1) return null;
|
|
129
|
+
|
|
130
|
+
const allowedFields = [
|
|
131
|
+
'status', 'priority', 'relatedTickets', 'swarmActions',
|
|
132
|
+
'namespace', 'description', 'f12Errors', 'serverErrors', 'route', 'comments'
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const ticket = this.data.tickets[ticketIndex];
|
|
136
|
+
|
|
137
|
+
allowedFields.forEach(field => {
|
|
138
|
+
if (updates[field] !== undefined) {
|
|
139
|
+
ticket[field] = updates[field];
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
ticket.updatedAt = new Date().toISOString();
|
|
144
|
+
await this._save();
|
|
145
|
+
|
|
146
|
+
return ticket;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async deleteTicket(id) {
|
|
150
|
+
const ticketIndex = this.data.tickets.findIndex(t => t.id === id);
|
|
151
|
+
if (ticketIndex === -1) return false;
|
|
152
|
+
|
|
153
|
+
this.data.tickets.splice(ticketIndex, 1);
|
|
154
|
+
await this._save();
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ==================== SWARM ACTION OPERATIONS ====================
|
|
160
|
+
|
|
161
|
+
async addSwarmAction(ticketId, action) {
|
|
162
|
+
const ticket = await this.getTicket(ticketId);
|
|
163
|
+
if (!ticket) return null;
|
|
164
|
+
|
|
165
|
+
const swarmAction = {
|
|
166
|
+
timestamp: new Date().toISOString(),
|
|
167
|
+
action: action.action,
|
|
168
|
+
result: action.result || null
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
ticket.swarmActions.push(swarmAction);
|
|
172
|
+
ticket.updatedAt = new Date().toISOString();
|
|
173
|
+
|
|
174
|
+
await this._save();
|
|
175
|
+
return ticket;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ==================== COMMENT OPERATIONS ====================
|
|
179
|
+
|
|
180
|
+
async addComment(ticketId, commentData) {
|
|
181
|
+
const ticket = await this.getTicket(ticketId);
|
|
182
|
+
if (!ticket) return null;
|
|
183
|
+
|
|
184
|
+
// Ensure comments array exists
|
|
185
|
+
if (!ticket.comments) {
|
|
186
|
+
ticket.comments = [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const comment = {
|
|
190
|
+
id: this.generateCommentId(),
|
|
191
|
+
timestamp: new Date().toISOString(),
|
|
192
|
+
type: commentData.type || 'human', // 'human' or 'ai'
|
|
193
|
+
author: commentData.author || 'anonymous',
|
|
194
|
+
content: commentData.content || '',
|
|
195
|
+
metadata: commentData.metadata || {}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
ticket.comments.push(comment);
|
|
199
|
+
ticket.updatedAt = new Date().toISOString();
|
|
200
|
+
|
|
201
|
+
await this._save();
|
|
202
|
+
return comment;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async getComments(ticketId) {
|
|
206
|
+
const ticket = await this.getTicket(ticketId);
|
|
207
|
+
if (!ticket) return [];
|
|
208
|
+
return ticket.comments || [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async updateComment(ticketId, commentId, updates) {
|
|
212
|
+
const ticket = await this.getTicket(ticketId);
|
|
213
|
+
if (!ticket || !ticket.comments) return null;
|
|
214
|
+
|
|
215
|
+
const commentIndex = ticket.comments.findIndex(c => c.id === commentId);
|
|
216
|
+
if (commentIndex === -1) return null;
|
|
217
|
+
|
|
218
|
+
const comment = ticket.comments[commentIndex];
|
|
219
|
+
if (updates.content !== undefined) comment.content = updates.content;
|
|
220
|
+
if (updates.metadata !== undefined) comment.metadata = { ...comment.metadata, ...updates.metadata };
|
|
221
|
+
comment.editedAt = new Date().toISOString();
|
|
222
|
+
|
|
223
|
+
ticket.updatedAt = new Date().toISOString();
|
|
224
|
+
await this._save();
|
|
225
|
+
|
|
226
|
+
return comment;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async deleteComment(ticketId, commentId) {
|
|
230
|
+
const ticket = await this.getTicket(ticketId);
|
|
231
|
+
if (!ticket || !ticket.comments) return false;
|
|
232
|
+
|
|
233
|
+
const commentIndex = ticket.comments.findIndex(c => c.id === commentId);
|
|
234
|
+
if (commentIndex === -1) return false;
|
|
235
|
+
|
|
236
|
+
ticket.comments.splice(commentIndex, 1);
|
|
237
|
+
ticket.updatedAt = new Date().toISOString();
|
|
238
|
+
await this._save();
|
|
239
|
+
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ==================== STATS OPERATIONS ====================
|
|
244
|
+
|
|
245
|
+
async getStats() {
|
|
246
|
+
const tickets = this.data.tickets;
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
total: tickets.length,
|
|
250
|
+
byStatus: {
|
|
251
|
+
open: tickets.filter(t => t.status === 'open').length,
|
|
252
|
+
inProgress: tickets.filter(t => t.status === 'in-progress').length,
|
|
253
|
+
fixed: tickets.filter(t => t.status === 'fixed').length,
|
|
254
|
+
closed: tickets.filter(t => t.status === 'closed').length
|
|
255
|
+
},
|
|
256
|
+
byPriority: {
|
|
257
|
+
critical: tickets.filter(t => t.priority === 'critical').length,
|
|
258
|
+
high: tickets.filter(t => t.priority === 'high').length,
|
|
259
|
+
medium: tickets.filter(t => t.priority === 'medium').length,
|
|
260
|
+
low: tickets.filter(t => t.priority === 'low').length
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ==================== BUG REPORT OPERATIONS ====================
|
|
266
|
+
|
|
267
|
+
async createBugReport(reportData, apiKey = null) {
|
|
268
|
+
// For JSON adapter, we don't validate API keys (simple mode)
|
|
269
|
+
// Create a minimal ticket from the bug report
|
|
270
|
+
const ticket = await this.createTicket({
|
|
271
|
+
route: reportData.location || reportData.route || 'unknown',
|
|
272
|
+
f12Errors: reportData.clientError || reportData.f12Errors || '',
|
|
273
|
+
serverErrors: '', // Bug reports don't include server errors
|
|
274
|
+
description: reportData.description || '',
|
|
275
|
+
status: 'open',
|
|
276
|
+
priority: null, // Will be set by triage
|
|
277
|
+
swarmActions: [{
|
|
278
|
+
timestamp: new Date().toISOString(),
|
|
279
|
+
action: 'bug-report-submitted',
|
|
280
|
+
result: `Bug report submitted via widget. User agent: ${reportData.userAgent || 'unknown'}`
|
|
281
|
+
}]
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Return limited info (don't expose internal ticket structure)
|
|
285
|
+
return {
|
|
286
|
+
id: ticket.id,
|
|
287
|
+
status: 'submitted',
|
|
288
|
+
message: 'Bug report received. Thank you!'
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = JsonAdapter;
|