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-tracker.html
CHANGED
|
@@ -3,43 +3,54 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>Swarm Tickets - Ticket Tracker</title>
|
|
7
7
|
<style>
|
|
8
8
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
body {
|
|
11
11
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
12
12
|
background: #1a1a1a;
|
|
13
13
|
color: #e0e0e0;
|
|
14
14
|
padding: 20px;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
.container { max-width: 1200px; margin: 0 auto; }
|
|
18
18
|
h1 { margin-bottom: 10px; color: #00d4aa; }
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
.project-name {
|
|
21
21
|
color: #999;
|
|
22
22
|
font-size: 0.9em;
|
|
23
23
|
margin-bottom: 30px;
|
|
24
24
|
font-weight: normal;
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
.server-status {
|
|
28
28
|
padding: 10px 15px;
|
|
29
29
|
border-radius: 5px;
|
|
30
30
|
margin-bottom: 20px;
|
|
31
31
|
font-size: 0.9em;
|
|
32
|
+
display: flex;
|
|
33
|
+
justify-content: space-between;
|
|
34
|
+
align-items: center;
|
|
32
35
|
}
|
|
33
36
|
.server-online { background: #2d4a2d; border-left: 4px solid #6bcf7f; }
|
|
34
37
|
.server-offline { background: #4a2d2d; border-left: 4px solid #ff6b6b; }
|
|
35
|
-
|
|
38
|
+
|
|
39
|
+
.storage-badge {
|
|
40
|
+
background: #444;
|
|
41
|
+
padding: 4px 10px;
|
|
42
|
+
border-radius: 12px;
|
|
43
|
+
font-size: 0.8em;
|
|
44
|
+
text-transform: uppercase;
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
.tabs {
|
|
37
48
|
display: flex;
|
|
38
49
|
gap: 10px;
|
|
39
50
|
margin-bottom: 20px;
|
|
40
51
|
border-bottom: 2px solid #333;
|
|
41
52
|
}
|
|
42
|
-
|
|
53
|
+
|
|
43
54
|
.tab {
|
|
44
55
|
padding: 10px 20px;
|
|
45
56
|
background: #2a2a2a;
|
|
@@ -49,22 +60,22 @@
|
|
|
49
60
|
border-radius: 5px 5px 0 0;
|
|
50
61
|
transition: all 0.3s;
|
|
51
62
|
}
|
|
52
|
-
|
|
63
|
+
|
|
53
64
|
.tab:hover { background: #333; }
|
|
54
65
|
.tab.active { background: #00d4aa; color: #1a1a1a; }
|
|
55
|
-
|
|
66
|
+
|
|
56
67
|
.tab-content { display: none; }
|
|
57
68
|
.tab-content.active { display: block; }
|
|
58
|
-
|
|
69
|
+
|
|
59
70
|
.form-group { margin-bottom: 20px; }
|
|
60
|
-
|
|
71
|
+
|
|
61
72
|
label {
|
|
62
73
|
display: block;
|
|
63
74
|
margin-bottom: 5px;
|
|
64
75
|
color: #00d4aa;
|
|
65
76
|
font-weight: 500;
|
|
66
77
|
}
|
|
67
|
-
|
|
78
|
+
|
|
68
79
|
input, textarea, select {
|
|
69
80
|
width: 100%;
|
|
70
81
|
padding: 10px;
|
|
@@ -74,13 +85,13 @@
|
|
|
74
85
|
border-radius: 5px;
|
|
75
86
|
font-family: inherit;
|
|
76
87
|
}
|
|
77
|
-
|
|
88
|
+
|
|
78
89
|
textarea {
|
|
79
90
|
resize: vertical;
|
|
80
91
|
font-family: 'Courier New', monospace;
|
|
81
92
|
min-height: 100px;
|
|
82
93
|
}
|
|
83
|
-
|
|
94
|
+
|
|
84
95
|
button {
|
|
85
96
|
background: #00d4aa;
|
|
86
97
|
color: #1a1a1a;
|
|
@@ -91,14 +102,37 @@
|
|
|
91
102
|
font-weight: 600;
|
|
92
103
|
transition: all 0.3s;
|
|
93
104
|
}
|
|
94
|
-
|
|
105
|
+
|
|
95
106
|
button:hover {
|
|
96
107
|
background: #00ffcc;
|
|
97
108
|
transform: translateY(-2px);
|
|
98
109
|
}
|
|
99
|
-
|
|
110
|
+
|
|
111
|
+
.btn-secondary {
|
|
112
|
+
background: #444;
|
|
113
|
+
color: #e0e0e0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.btn-secondary:hover {
|
|
117
|
+
background: #555;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.btn-danger {
|
|
121
|
+
background: #ff6b6b;
|
|
122
|
+
color: white;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.btn-danger:hover {
|
|
126
|
+
background: #ff8888;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.btn-small {
|
|
130
|
+
padding: 6px 12px;
|
|
131
|
+
font-size: 0.85em;
|
|
132
|
+
}
|
|
133
|
+
|
|
100
134
|
.ticket-list { display: grid; gap: 15px; }
|
|
101
|
-
|
|
135
|
+
|
|
102
136
|
.ticket-card {
|
|
103
137
|
background: #2a2a2a;
|
|
104
138
|
border: 1px solid #444;
|
|
@@ -106,58 +140,58 @@
|
|
|
106
140
|
padding: 20px;
|
|
107
141
|
transition: all 0.3s;
|
|
108
142
|
}
|
|
109
|
-
|
|
143
|
+
|
|
110
144
|
.ticket-card:hover {
|
|
111
145
|
border-color: #00d4aa;
|
|
112
146
|
box-shadow: 0 0 20px rgba(0, 212, 170, 0.1);
|
|
113
147
|
}
|
|
114
|
-
|
|
148
|
+
|
|
115
149
|
.ticket-header {
|
|
116
150
|
display: flex;
|
|
117
151
|
justify-content: space-between;
|
|
118
152
|
align-items: start;
|
|
119
153
|
margin-bottom: 15px;
|
|
120
154
|
}
|
|
121
|
-
|
|
155
|
+
|
|
122
156
|
.ticket-id {
|
|
123
157
|
font-weight: 700;
|
|
124
158
|
color: #00d4aa;
|
|
125
159
|
font-size: 1.1em;
|
|
126
160
|
}
|
|
127
|
-
|
|
161
|
+
|
|
128
162
|
.ticket-meta { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
129
|
-
|
|
163
|
+
|
|
130
164
|
.badge {
|
|
131
165
|
padding: 4px 12px;
|
|
132
166
|
border-radius: 12px;
|
|
133
167
|
font-size: 0.85em;
|
|
134
168
|
font-weight: 600;
|
|
135
169
|
}
|
|
136
|
-
|
|
170
|
+
|
|
137
171
|
.status-open { background: #ff6b6b; color: white; }
|
|
138
172
|
.status-in-progress { background: #ffd93d; color: #1a1a1a; }
|
|
139
173
|
.status-fixed { background: #6bcf7f; color: white; }
|
|
140
174
|
.status-closed { background: #666; color: white; }
|
|
141
|
-
|
|
175
|
+
|
|
142
176
|
.priority-critical { background: #ff3838; color: white; }
|
|
143
177
|
.priority-high { background: #ff9f43; color: white; }
|
|
144
178
|
.priority-medium { background: #ffd93d; color: #1a1a1a; }
|
|
145
179
|
.priority-low { background: #6bcf7f; color: white; }
|
|
146
|
-
|
|
180
|
+
|
|
147
181
|
.ticket-route {
|
|
148
182
|
color: #00d4aa;
|
|
149
183
|
font-family: 'Courier New', monospace;
|
|
150
184
|
margin-bottom: 10px;
|
|
151
185
|
}
|
|
152
|
-
|
|
186
|
+
|
|
153
187
|
.error-section { margin: 10px 0; }
|
|
154
|
-
|
|
188
|
+
|
|
155
189
|
.error-section h4 {
|
|
156
190
|
color: #ff6b6b;
|
|
157
191
|
margin-bottom: 5px;
|
|
158
192
|
font-size: 0.9em;
|
|
159
193
|
}
|
|
160
|
-
|
|
194
|
+
|
|
161
195
|
.error-content {
|
|
162
196
|
background: #1a1a1a;
|
|
163
197
|
padding: 10px;
|
|
@@ -167,7 +201,7 @@
|
|
|
167
201
|
overflow-x: auto;
|
|
168
202
|
white-space: pre-wrap;
|
|
169
203
|
}
|
|
170
|
-
|
|
204
|
+
|
|
171
205
|
.ticket-footer {
|
|
172
206
|
margin-top: 15px;
|
|
173
207
|
padding-top: 15px;
|
|
@@ -175,26 +209,33 @@
|
|
|
175
209
|
font-size: 0.85em;
|
|
176
210
|
color: #999;
|
|
177
211
|
}
|
|
178
|
-
|
|
212
|
+
|
|
213
|
+
.ticket-actions {
|
|
214
|
+
display: flex;
|
|
215
|
+
gap: 10px;
|
|
216
|
+
margin-top: 15px;
|
|
217
|
+
flex-wrap: wrap;
|
|
218
|
+
}
|
|
219
|
+
|
|
179
220
|
.filters {
|
|
180
221
|
display: flex;
|
|
181
222
|
gap: 15px;
|
|
182
223
|
margin-bottom: 20px;
|
|
183
224
|
flex-wrap: wrap;
|
|
184
225
|
}
|
|
185
|
-
|
|
226
|
+
|
|
186
227
|
.filter-group {
|
|
187
228
|
flex: 1;
|
|
188
229
|
min-width: 200px;
|
|
189
230
|
}
|
|
190
|
-
|
|
231
|
+
|
|
191
232
|
.stats {
|
|
192
233
|
display: flex;
|
|
193
234
|
gap: 15px;
|
|
194
235
|
margin-bottom: 20px;
|
|
195
236
|
flex-wrap: wrap;
|
|
196
237
|
}
|
|
197
|
-
|
|
238
|
+
|
|
198
239
|
.stat-card {
|
|
199
240
|
flex: 1;
|
|
200
241
|
min-width: 150px;
|
|
@@ -203,31 +244,29 @@
|
|
|
203
244
|
border-radius: 8px;
|
|
204
245
|
border: 1px solid #444;
|
|
205
246
|
}
|
|
206
|
-
|
|
247
|
+
|
|
207
248
|
.stat-value {
|
|
208
249
|
font-size: 2em;
|
|
209
250
|
font-weight: 700;
|
|
210
251
|
color: #00d4aa;
|
|
211
252
|
}
|
|
212
|
-
|
|
253
|
+
|
|
213
254
|
.stat-label {
|
|
214
255
|
color: #999;
|
|
215
256
|
font-size: 0.9em;
|
|
216
257
|
}
|
|
217
|
-
|
|
258
|
+
|
|
218
259
|
.quick-prompt-btn {
|
|
219
260
|
background: #6c5ce7;
|
|
220
261
|
color: white;
|
|
221
262
|
padding: 8px 16px;
|
|
222
263
|
font-size: 0.9em;
|
|
223
|
-
margin-top: 10px;
|
|
224
|
-
display: inline-block;
|
|
225
264
|
}
|
|
226
|
-
|
|
265
|
+
|
|
227
266
|
.quick-prompt-btn:hover {
|
|
228
267
|
background: #5f4fd1;
|
|
229
268
|
}
|
|
230
|
-
|
|
269
|
+
|
|
231
270
|
.settings-section {
|
|
232
271
|
background: #2a2a2a;
|
|
233
272
|
padding: 20px;
|
|
@@ -235,12 +274,12 @@
|
|
|
235
274
|
margin-bottom: 20px;
|
|
236
275
|
border: 1px solid #444;
|
|
237
276
|
}
|
|
238
|
-
|
|
277
|
+
|
|
239
278
|
.settings-section h3 {
|
|
240
279
|
color: #00d4aa;
|
|
241
280
|
margin-bottom: 15px;
|
|
242
281
|
}
|
|
243
|
-
|
|
282
|
+
|
|
244
283
|
.template-info {
|
|
245
284
|
background: #1a1a1a;
|
|
246
285
|
padding: 10px;
|
|
@@ -249,50 +288,157 @@
|
|
|
249
288
|
font-size: 0.9em;
|
|
250
289
|
color: #999;
|
|
251
290
|
}
|
|
252
|
-
|
|
291
|
+
|
|
253
292
|
.template-info code {
|
|
254
293
|
color: #00d4aa;
|
|
255
294
|
background: #2a2a2a;
|
|
256
295
|
padding: 2px 6px;
|
|
257
296
|
border-radius: 3px;
|
|
258
297
|
}
|
|
298
|
+
|
|
299
|
+
/* Comments section */
|
|
300
|
+
.comments-section {
|
|
301
|
+
margin-top: 15px;
|
|
302
|
+
padding-top: 15px;
|
|
303
|
+
border-top: 1px solid #444;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.comments-section h4 {
|
|
307
|
+
color: #00d4aa;
|
|
308
|
+
margin-bottom: 10px;
|
|
309
|
+
font-size: 0.9em;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.comment {
|
|
313
|
+
background: #1a1a1a;
|
|
314
|
+
padding: 10px;
|
|
315
|
+
border-radius: 5px;
|
|
316
|
+
margin-bottom: 10px;
|
|
317
|
+
border-left: 3px solid #444;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.comment.human { border-left-color: #6c5ce7; }
|
|
321
|
+
.comment.ai { border-left-color: #00d4aa; }
|
|
322
|
+
|
|
323
|
+
.comment-header {
|
|
324
|
+
display: flex;
|
|
325
|
+
justify-content: space-between;
|
|
326
|
+
font-size: 0.8em;
|
|
327
|
+
color: #999;
|
|
328
|
+
margin-bottom: 5px;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.comment-author {
|
|
332
|
+
font-weight: 600;
|
|
333
|
+
color: #e0e0e0;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.comment-type {
|
|
337
|
+
padding: 2px 6px;
|
|
338
|
+
border-radius: 3px;
|
|
339
|
+
font-size: 0.75em;
|
|
340
|
+
text-transform: uppercase;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.comment-type.human { background: #6c5ce7; color: white; }
|
|
344
|
+
.comment-type.ai { background: #00d4aa; color: #1a1a1a; }
|
|
345
|
+
|
|
346
|
+
.comment-content {
|
|
347
|
+
font-size: 0.9em;
|
|
348
|
+
line-height: 1.4;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.add-comment {
|
|
352
|
+
display: flex;
|
|
353
|
+
gap: 10px;
|
|
354
|
+
margin-top: 10px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.add-comment input {
|
|
358
|
+
flex: 1;
|
|
359
|
+
padding: 8px 12px;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.add-comment button {
|
|
363
|
+
padding: 8px 16px;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* Modal */
|
|
367
|
+
.modal {
|
|
368
|
+
display: none;
|
|
369
|
+
position: fixed;
|
|
370
|
+
inset: 0;
|
|
371
|
+
background: rgba(0, 0, 0, 0.7);
|
|
372
|
+
z-index: 1000;
|
|
373
|
+
align-items: center;
|
|
374
|
+
justify-content: center;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.modal.open { display: flex; }
|
|
378
|
+
|
|
379
|
+
.modal-content {
|
|
380
|
+
background: #2a2a2a;
|
|
381
|
+
padding: 25px;
|
|
382
|
+
border-radius: 10px;
|
|
383
|
+
max-width: 500px;
|
|
384
|
+
width: 90%;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.modal-header {
|
|
388
|
+
display: flex;
|
|
389
|
+
justify-content: space-between;
|
|
390
|
+
align-items: center;
|
|
391
|
+
margin-bottom: 20px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.modal-header h3 { color: #00d4aa; }
|
|
395
|
+
|
|
396
|
+
.modal-close {
|
|
397
|
+
background: none;
|
|
398
|
+
border: none;
|
|
399
|
+
color: #999;
|
|
400
|
+
font-size: 1.5em;
|
|
401
|
+
cursor: pointer;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.modal-close:hover { color: #fff; }
|
|
259
405
|
</style>
|
|
260
406
|
</head>
|
|
261
407
|
<body>
|
|
262
408
|
<div class="container">
|
|
263
|
-
<h1>🎫
|
|
409
|
+
<h1>🎫 Swarm Tickets</h1>
|
|
264
410
|
<div class="project-name" id="project-name">📁 Loading project...</div>
|
|
265
|
-
|
|
411
|
+
|
|
266
412
|
<div id="server-status" class="server-status"></div>
|
|
267
|
-
|
|
413
|
+
|
|
268
414
|
<div class="tabs">
|
|
269
415
|
<button class="tab active" onclick="switchTab('submit')">Submit Ticket</button>
|
|
270
416
|
<button class="tab" onclick="switchTab('view')">View Tickets</button>
|
|
271
417
|
<button class="tab" onclick="switchTab('settings')">Settings</button>
|
|
272
418
|
</div>
|
|
273
|
-
|
|
419
|
+
|
|
274
420
|
<div id="submit-tab" class="tab-content active">
|
|
275
421
|
<form id="ticket-form">
|
|
276
422
|
<div class="form-group">
|
|
277
423
|
<label for="route" id="route-form-label">Route/Webpage</label>
|
|
278
424
|
<input type="text" id="route" required placeholder="/dashboard/users">
|
|
279
425
|
</div>
|
|
280
|
-
|
|
426
|
+
|
|
281
427
|
<div class="form-group">
|
|
282
428
|
<label for="f12-errors" id="f12-form-label">F12 Console Errors</label>
|
|
283
429
|
<textarea id="f12-errors" placeholder="Paste console errors from browser DevTools..."></textarea>
|
|
284
430
|
</div>
|
|
285
|
-
|
|
431
|
+
|
|
286
432
|
<div class="form-group">
|
|
287
433
|
<label for="server-errors" id="server-form-label">Server Console Errors</label>
|
|
288
434
|
<textarea id="server-errors" placeholder="Paste server-side console errors..."></textarea>
|
|
289
435
|
</div>
|
|
290
|
-
|
|
436
|
+
|
|
291
437
|
<div class="form-group">
|
|
292
438
|
<label for="description">Description (Optional)</label>
|
|
293
439
|
<textarea id="description" rows="3" placeholder="Additional context or steps to reproduce..."></textarea>
|
|
294
440
|
</div>
|
|
295
|
-
|
|
441
|
+
|
|
296
442
|
<div class="form-group">
|
|
297
443
|
<label for="status">Status</label>
|
|
298
444
|
<select id="status">
|
|
@@ -302,14 +448,14 @@
|
|
|
302
448
|
<option value="closed">Closed</option>
|
|
303
449
|
</select>
|
|
304
450
|
</div>
|
|
305
|
-
|
|
451
|
+
|
|
306
452
|
<button type="submit">Create Ticket</button>
|
|
307
453
|
</form>
|
|
308
454
|
</div>
|
|
309
|
-
|
|
455
|
+
|
|
310
456
|
<div id="view-tab" class="tab-content">
|
|
311
457
|
<div class="stats" id="stats"></div>
|
|
312
|
-
|
|
458
|
+
|
|
313
459
|
<div class="filters">
|
|
314
460
|
<div class="filter-group">
|
|
315
461
|
<label for="filter-status">Filter by Status</label>
|
|
@@ -321,7 +467,7 @@
|
|
|
321
467
|
<option value="closed">Closed</option>
|
|
322
468
|
</select>
|
|
323
469
|
</div>
|
|
324
|
-
|
|
470
|
+
|
|
325
471
|
<div class="filter-group">
|
|
326
472
|
<label for="filter-priority">Filter by Priority</label>
|
|
327
473
|
<select id="filter-priority" onchange="filterTickets()">
|
|
@@ -332,21 +478,21 @@
|
|
|
332
478
|
<option value="low">Low</option>
|
|
333
479
|
</select>
|
|
334
480
|
</div>
|
|
335
|
-
|
|
481
|
+
|
|
336
482
|
<div class="filter-group">
|
|
337
483
|
<label for="search">Search</label>
|
|
338
484
|
<input type="text" id="search" placeholder="Search tickets..." oninput="filterTickets()">
|
|
339
485
|
</div>
|
|
340
486
|
</div>
|
|
341
|
-
|
|
487
|
+
|
|
342
488
|
<div class="ticket-list" id="ticket-list"></div>
|
|
343
489
|
</div>
|
|
344
|
-
|
|
490
|
+
|
|
345
491
|
<div id="settings-tab" class="tab-content">
|
|
346
492
|
<div class="settings-section">
|
|
347
493
|
<h3>Quick Prompt Template</h3>
|
|
348
494
|
<p style="margin-bottom: 15px; color: #999;">Customize the prompt that gets copied when you click the Quick Prompt button on a ticket.</p>
|
|
349
|
-
|
|
495
|
+
|
|
350
496
|
<div class="form-group">
|
|
351
497
|
<label for="prompt-template">Template</label>
|
|
352
498
|
<textarea id="prompt-template" rows="5" placeholder="Please investigate and fix ticket {TICKET_ID}."></textarea>
|
|
@@ -355,48 +501,111 @@
|
|
|
355
501
|
<ul style="margin-top: 5px; margin-left: 20px;">
|
|
356
502
|
<li><code>{TICKET_ID}</code> - The ticket's unique identifier</li>
|
|
357
503
|
</ul>
|
|
358
|
-
<p style="margin-top: 10px;">💡 Tip: Keep it simple and let Claude-flow ask clarifying questions as needed.</p>
|
|
359
504
|
</div>
|
|
360
505
|
</div>
|
|
361
|
-
|
|
506
|
+
|
|
362
507
|
<button onclick="savePromptTemplate()">Save Template</button>
|
|
363
|
-
<button onclick="resetPromptTemplate()" style="
|
|
508
|
+
<button onclick="resetPromptTemplate()" class="btn-secondary" style="margin-left: 10px;">Reset to Default</button>
|
|
364
509
|
</div>
|
|
365
|
-
|
|
510
|
+
|
|
366
511
|
<div class="settings-section">
|
|
367
512
|
<h3>Project & Field Labels</h3>
|
|
368
513
|
<p style="margin-bottom: 15px; color: #999;">Customize how fields are displayed in your ticket tracker.</p>
|
|
369
|
-
|
|
514
|
+
|
|
370
515
|
<div class="form-group">
|
|
371
516
|
<label for="project-name-input">Project Name</label>
|
|
372
517
|
<input type="text" id="project-name-input" placeholder="My Awesome Project">
|
|
373
518
|
</div>
|
|
374
|
-
|
|
519
|
+
|
|
375
520
|
<div class="form-group">
|
|
376
521
|
<label for="route-label">Location Field Label</label>
|
|
377
522
|
<input type="text" id="route-label" placeholder="Route/Webpage">
|
|
378
523
|
</div>
|
|
379
|
-
|
|
524
|
+
|
|
380
525
|
<div class="form-group">
|
|
381
526
|
<label for="f12-label">Client Errors Field Label</label>
|
|
382
527
|
<input type="text" id="f12-label" placeholder="F12 Console Errors">
|
|
383
528
|
</div>
|
|
384
|
-
|
|
529
|
+
|
|
385
530
|
<div class="form-group">
|
|
386
531
|
<label for="server-label">Server Errors Field Label</label>
|
|
387
532
|
<input type="text" id="server-label" placeholder="Server Console Errors">
|
|
388
533
|
</div>
|
|
389
|
-
|
|
534
|
+
|
|
390
535
|
<button onclick="saveFieldLabels()">Save Labels</button>
|
|
391
|
-
<button onclick="resetFieldLabels()" style="
|
|
536
|
+
<button onclick="resetFieldLabels()" class="btn-secondary" style="margin-left: 10px;">Reset to Default</button>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
<!-- Comment Modal -->
|
|
542
|
+
<div id="comment-modal" class="modal">
|
|
543
|
+
<div class="modal-content">
|
|
544
|
+
<div class="modal-header">
|
|
545
|
+
<h3>💬 Add Comment</h3>
|
|
546
|
+
<button class="modal-close" onclick="closeCommentModal()">×</button>
|
|
392
547
|
</div>
|
|
548
|
+
<form id="comment-form">
|
|
549
|
+
<input type="hidden" id="comment-ticket-id">
|
|
550
|
+
<div class="form-group">
|
|
551
|
+
<label for="comment-author">Your Name</label>
|
|
552
|
+
<input type="text" id="comment-author" placeholder="Enter your name" required>
|
|
553
|
+
</div>
|
|
554
|
+
<div class="form-group">
|
|
555
|
+
<label for="comment-content">Comment</label>
|
|
556
|
+
<textarea id="comment-content" rows="4" placeholder="Write your comment..." required></textarea>
|
|
557
|
+
</div>
|
|
558
|
+
<button type="submit">Add Comment</button>
|
|
559
|
+
</form>
|
|
393
560
|
</div>
|
|
394
561
|
</div>
|
|
395
|
-
|
|
562
|
+
|
|
563
|
+
<!-- Edit Ticket Modal -->
|
|
564
|
+
<div id="edit-modal" class="modal">
|
|
565
|
+
<div class="modal-content" style="max-width: 600px;">
|
|
566
|
+
<div class="modal-header">
|
|
567
|
+
<h3>✏️ Edit Ticket</h3>
|
|
568
|
+
<button class="modal-close" onclick="closeEditModal()">×</button>
|
|
569
|
+
</div>
|
|
570
|
+
<form id="edit-form">
|
|
571
|
+
<input type="hidden" id="edit-ticket-id">
|
|
572
|
+
<div class="form-group">
|
|
573
|
+
<label for="edit-status">Status</label>
|
|
574
|
+
<select id="edit-status">
|
|
575
|
+
<option value="open">Open</option>
|
|
576
|
+
<option value="in-progress">In Progress</option>
|
|
577
|
+
<option value="fixed">Fixed</option>
|
|
578
|
+
<option value="closed">Closed</option>
|
|
579
|
+
</select>
|
|
580
|
+
</div>
|
|
581
|
+
<div class="form-group">
|
|
582
|
+
<label for="edit-priority">Priority</label>
|
|
583
|
+
<select id="edit-priority">
|
|
584
|
+
<option value="">Not Set</option>
|
|
585
|
+
<option value="critical">Critical</option>
|
|
586
|
+
<option value="high">High</option>
|
|
587
|
+
<option value="medium">Medium</option>
|
|
588
|
+
<option value="low">Low</option>
|
|
589
|
+
</select>
|
|
590
|
+
</div>
|
|
591
|
+
<div class="form-group">
|
|
592
|
+
<label for="edit-description">Description</label>
|
|
593
|
+
<textarea id="edit-description" rows="3" placeholder="Description..."></textarea>
|
|
594
|
+
</div>
|
|
595
|
+
<div class="form-group">
|
|
596
|
+
<label for="edit-namespace">Namespace (where fix applied)</label>
|
|
597
|
+
<input type="text" id="edit-namespace" placeholder="e.g., auth/login, database/connection">
|
|
598
|
+
</div>
|
|
599
|
+
<button type="submit">Save Changes</button>
|
|
600
|
+
</form>
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
396
604
|
<script>
|
|
397
605
|
let tickets = [];
|
|
398
606
|
let useServer = false;
|
|
399
|
-
|
|
607
|
+
let storageType = 'json';
|
|
608
|
+
const API_BASE = window.location.origin + '/api';
|
|
400
609
|
const DEFAULT_PROMPT_TEMPLATE = 'Please investigate and fix ticket {TICKET_ID}.';
|
|
401
610
|
const DEFAULT_FIELD_LABELS = {
|
|
402
611
|
projectName: 'Ticket Tracker',
|
|
@@ -404,19 +613,19 @@
|
|
|
404
613
|
f12: 'F12 Console Errors',
|
|
405
614
|
server: 'Server Console Errors'
|
|
406
615
|
};
|
|
407
|
-
|
|
616
|
+
|
|
408
617
|
// Load field labels from localStorage or use defaults
|
|
409
618
|
function getFieldLabels() {
|
|
410
619
|
const stored = localStorage.getItem('claudeflow-field-labels');
|
|
411
620
|
return stored ? JSON.parse(stored) : DEFAULT_FIELD_LABELS;
|
|
412
621
|
}
|
|
413
|
-
|
|
622
|
+
|
|
414
623
|
// Update project name display
|
|
415
624
|
function updateProjectName() {
|
|
416
625
|
const labels = getFieldLabels();
|
|
417
626
|
document.getElementById('project-name').textContent = '📁 ' + labels.projectName;
|
|
418
627
|
}
|
|
419
|
-
|
|
628
|
+
|
|
420
629
|
// Update all form labels
|
|
421
630
|
function updateFormLabels() {
|
|
422
631
|
const labels = getFieldLabels();
|
|
@@ -424,12 +633,12 @@
|
|
|
424
633
|
document.getElementById('f12-form-label').textContent = labels.f12;
|
|
425
634
|
document.getElementById('server-form-label').textContent = labels.server;
|
|
426
635
|
}
|
|
427
|
-
|
|
636
|
+
|
|
428
637
|
// Load prompt template from localStorage or use default
|
|
429
638
|
function getPromptTemplate() {
|
|
430
639
|
return localStorage.getItem('claudeflow-prompt-template') || DEFAULT_PROMPT_TEMPLATE;
|
|
431
640
|
}
|
|
432
|
-
|
|
641
|
+
|
|
433
642
|
// Save prompt template to localStorage
|
|
434
643
|
function savePromptTemplate() {
|
|
435
644
|
const template = document.getElementById('prompt-template').value.trim();
|
|
@@ -440,7 +649,7 @@
|
|
|
440
649
|
localStorage.setItem('claudeflow-prompt-template', template);
|
|
441
650
|
alert('✅ Prompt template saved!');
|
|
442
651
|
}
|
|
443
|
-
|
|
652
|
+
|
|
444
653
|
// Reset prompt template to default
|
|
445
654
|
function resetPromptTemplate() {
|
|
446
655
|
if (confirm('Reset to default template?')) {
|
|
@@ -449,7 +658,7 @@
|
|
|
449
658
|
alert('✅ Template reset to default!');
|
|
450
659
|
}
|
|
451
660
|
}
|
|
452
|
-
|
|
661
|
+
|
|
453
662
|
// Save field labels to localStorage
|
|
454
663
|
function saveFieldLabels() {
|
|
455
664
|
const labels = {
|
|
@@ -463,7 +672,7 @@
|
|
|
463
672
|
updateFormLabels();
|
|
464
673
|
alert('✅ Field labels saved!');
|
|
465
674
|
}
|
|
466
|
-
|
|
675
|
+
|
|
467
676
|
// Reset field labels to defaults
|
|
468
677
|
function resetFieldLabels() {
|
|
469
678
|
if (confirm('Reset all field labels to defaults?')) {
|
|
@@ -477,12 +686,12 @@
|
|
|
477
686
|
alert('✅ Labels reset to defaults!');
|
|
478
687
|
}
|
|
479
688
|
}
|
|
480
|
-
|
|
689
|
+
|
|
481
690
|
// Copy quick prompt for a ticket
|
|
482
691
|
function copyQuickPrompt(ticketId) {
|
|
483
692
|
const template = getPromptTemplate();
|
|
484
693
|
const prompt = template.replace(/{TICKET_ID}/g, ticketId);
|
|
485
|
-
|
|
694
|
+
|
|
486
695
|
navigator.clipboard.writeText(prompt).then(() => {
|
|
487
696
|
alert('✅ Prompt copied to clipboard!\n\n' + prompt);
|
|
488
697
|
}).catch(err => {
|
|
@@ -490,12 +699,18 @@
|
|
|
490
699
|
alert('❌ Failed to copy to clipboard');
|
|
491
700
|
});
|
|
492
701
|
}
|
|
493
|
-
|
|
702
|
+
|
|
494
703
|
// Check if server is available
|
|
495
704
|
async function checkServer() {
|
|
496
705
|
try {
|
|
497
|
-
const response = await fetch(`${API_BASE}/
|
|
498
|
-
|
|
706
|
+
const response = await fetch(`${API_BASE}/health`);
|
|
707
|
+
if (response.ok) {
|
|
708
|
+
const data = await response.json();
|
|
709
|
+
useServer = true;
|
|
710
|
+
storageType = data.storage || 'json';
|
|
711
|
+
} else {
|
|
712
|
+
useServer = false;
|
|
713
|
+
}
|
|
499
714
|
updateServerStatus();
|
|
500
715
|
return useServer;
|
|
501
716
|
} catch {
|
|
@@ -504,23 +719,29 @@
|
|
|
504
719
|
return false;
|
|
505
720
|
}
|
|
506
721
|
}
|
|
507
|
-
|
|
722
|
+
|
|
508
723
|
// Update server status indicator
|
|
509
724
|
function updateServerStatus() {
|
|
510
725
|
const statusEl = document.getElementById('server-status');
|
|
511
726
|
if (useServer) {
|
|
512
727
|
statusEl.className = 'server-status server-online';
|
|
513
|
-
statusEl.
|
|
728
|
+
statusEl.innerHTML = `
|
|
729
|
+
<span>✅ Server connected</span>
|
|
730
|
+
<span class="storage-badge">${storageType}</span>
|
|
731
|
+
`;
|
|
514
732
|
} else {
|
|
515
733
|
statusEl.className = 'server-status server-offline';
|
|
516
|
-
statusEl.
|
|
734
|
+
statusEl.innerHTML = `
|
|
735
|
+
<span>⚠️ Server offline - using localStorage</span>
|
|
736
|
+
<span class="storage-badge">local</span>
|
|
737
|
+
`;
|
|
517
738
|
}
|
|
518
739
|
}
|
|
519
|
-
|
|
740
|
+
|
|
520
741
|
// Load tickets from server or localStorage
|
|
521
742
|
async function loadTickets() {
|
|
522
743
|
await checkServer();
|
|
523
|
-
|
|
744
|
+
|
|
524
745
|
if (useServer) {
|
|
525
746
|
try {
|
|
526
747
|
const response = await fetch(`${API_BASE}/tickets`);
|
|
@@ -534,7 +755,7 @@
|
|
|
534
755
|
loadFromLocalStorage();
|
|
535
756
|
}
|
|
536
757
|
}
|
|
537
|
-
|
|
758
|
+
|
|
538
759
|
// Load from localStorage
|
|
539
760
|
function loadFromLocalStorage() {
|
|
540
761
|
const stored = localStorage.getItem('claudeflow-tickets');
|
|
@@ -543,7 +764,7 @@
|
|
|
543
764
|
console.log('✅ Loaded', tickets.length, 'tickets from localStorage');
|
|
544
765
|
}
|
|
545
766
|
}
|
|
546
|
-
|
|
767
|
+
|
|
547
768
|
// Save ticket to server or localStorage
|
|
548
769
|
async function saveTicket(ticket) {
|
|
549
770
|
if (useServer) {
|
|
@@ -561,21 +782,167 @@
|
|
|
561
782
|
return ticket;
|
|
562
783
|
}
|
|
563
784
|
} else {
|
|
785
|
+
ticket.id = 'TKT-' + Date.now();
|
|
786
|
+
ticket.createdAt = new Date().toISOString();
|
|
787
|
+
ticket.updatedAt = new Date().toISOString();
|
|
788
|
+
ticket.swarmActions = [];
|
|
789
|
+
ticket.comments = [];
|
|
790
|
+
ticket.relatedTickets = [];
|
|
564
791
|
tickets.push(ticket);
|
|
565
792
|
localStorage.setItem('claudeflow-tickets', JSON.stringify(tickets));
|
|
566
793
|
console.log('✅ Ticket saved to localStorage');
|
|
567
794
|
return ticket;
|
|
568
795
|
}
|
|
569
796
|
}
|
|
570
|
-
|
|
797
|
+
|
|
798
|
+
// Close ticket
|
|
799
|
+
async function closeTicket(ticketId) {
|
|
800
|
+
if (!confirm('Close this ticket?')) return;
|
|
801
|
+
|
|
802
|
+
if (useServer) {
|
|
803
|
+
try {
|
|
804
|
+
await fetch(`${API_BASE}/tickets/${ticketId}/close`, { method: 'POST' });
|
|
805
|
+
await loadTickets();
|
|
806
|
+
renderTickets();
|
|
807
|
+
renderStats();
|
|
808
|
+
} catch (error) {
|
|
809
|
+
alert('Failed to close ticket');
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Reopen ticket
|
|
815
|
+
async function reopenTicket(ticketId) {
|
|
816
|
+
if (!confirm('Reopen this ticket?')) return;
|
|
817
|
+
|
|
818
|
+
if (useServer) {
|
|
819
|
+
try {
|
|
820
|
+
await fetch(`${API_BASE}/tickets/${ticketId}/reopen`, { method: 'POST' });
|
|
821
|
+
await loadTickets();
|
|
822
|
+
renderTickets();
|
|
823
|
+
renderStats();
|
|
824
|
+
} catch (error) {
|
|
825
|
+
alert('Failed to reopen ticket');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Open comment modal
|
|
831
|
+
function openCommentModal(ticketId) {
|
|
832
|
+
document.getElementById('comment-ticket-id').value = ticketId;
|
|
833
|
+
document.getElementById('comment-content').value = '';
|
|
834
|
+
document.getElementById('comment-modal').classList.add('open');
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Close comment modal
|
|
838
|
+
function closeCommentModal() {
|
|
839
|
+
document.getElementById('comment-modal').classList.remove('open');
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Open edit modal
|
|
843
|
+
function openEditModal(ticketId) {
|
|
844
|
+
const ticket = tickets.find(t => t.id === ticketId);
|
|
845
|
+
if (!ticket) return;
|
|
846
|
+
|
|
847
|
+
document.getElementById('edit-ticket-id').value = ticketId;
|
|
848
|
+
document.getElementById('edit-status').value = ticket.status || 'open';
|
|
849
|
+
document.getElementById('edit-priority').value = ticket.priority || '';
|
|
850
|
+
document.getElementById('edit-description').value = ticket.description || '';
|
|
851
|
+
document.getElementById('edit-namespace').value = ticket.namespace || '';
|
|
852
|
+
document.getElementById('edit-modal').classList.add('open');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Close edit modal
|
|
856
|
+
function closeEditModal() {
|
|
857
|
+
document.getElementById('edit-modal').classList.remove('open');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Handle edit form
|
|
861
|
+
document.getElementById('edit-form').addEventListener('submit', async function(e) {
|
|
862
|
+
e.preventDefault();
|
|
863
|
+
const ticketId = document.getElementById('edit-ticket-id').value;
|
|
864
|
+
const updates = {
|
|
865
|
+
status: document.getElementById('edit-status').value,
|
|
866
|
+
priority: document.getElementById('edit-priority').value || null,
|
|
867
|
+
description: document.getElementById('edit-description').value,
|
|
868
|
+
namespace: document.getElementById('edit-namespace').value || null
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
if (useServer) {
|
|
872
|
+
try {
|
|
873
|
+
await fetch(`${API_BASE}/tickets/${ticketId}`, {
|
|
874
|
+
method: 'PATCH',
|
|
875
|
+
headers: { 'Content-Type': 'application/json' },
|
|
876
|
+
body: JSON.stringify(updates)
|
|
877
|
+
});
|
|
878
|
+
await loadTickets();
|
|
879
|
+
renderTickets();
|
|
880
|
+
renderStats();
|
|
881
|
+
closeEditModal();
|
|
882
|
+
} catch (error) {
|
|
883
|
+
alert('Failed to update ticket');
|
|
884
|
+
}
|
|
885
|
+
} else {
|
|
886
|
+
// Update in localStorage
|
|
887
|
+
const ticket = tickets.find(t => t.id === ticketId);
|
|
888
|
+
if (ticket) {
|
|
889
|
+
Object.assign(ticket, updates);
|
|
890
|
+
ticket.updatedAt = new Date().toISOString();
|
|
891
|
+
localStorage.setItem('claudeflow-tickets', JSON.stringify(tickets));
|
|
892
|
+
renderTickets();
|
|
893
|
+
renderStats();
|
|
894
|
+
closeEditModal();
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
// Add comment
|
|
900
|
+
async function addComment(ticketId, author, content) {
|
|
901
|
+
if (useServer) {
|
|
902
|
+
try {
|
|
903
|
+
await fetch(`${API_BASE}/tickets/${ticketId}/comments`, {
|
|
904
|
+
method: 'POST',
|
|
905
|
+
headers: { 'Content-Type': 'application/json' },
|
|
906
|
+
body: JSON.stringify({ type: 'human', author, content })
|
|
907
|
+
});
|
|
908
|
+
await loadTickets();
|
|
909
|
+
renderTickets();
|
|
910
|
+
} catch (error) {
|
|
911
|
+
alert('Failed to add comment');
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Handle comment form
|
|
917
|
+
document.getElementById('comment-form').addEventListener('submit', async function(e) {
|
|
918
|
+
e.preventDefault();
|
|
919
|
+
const ticketId = document.getElementById('comment-ticket-id').value;
|
|
920
|
+
const author = document.getElementById('comment-author').value;
|
|
921
|
+
const content = document.getElementById('comment-content').value;
|
|
922
|
+
|
|
923
|
+
// Save author name for next time
|
|
924
|
+
localStorage.setItem('claudeflow-author', author);
|
|
925
|
+
|
|
926
|
+
await addComment(ticketId, author, content);
|
|
927
|
+
closeCommentModal();
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// Load saved author name
|
|
931
|
+
function loadSavedAuthor() {
|
|
932
|
+
const saved = localStorage.getItem('claudeflow-author');
|
|
933
|
+
if (saved) {
|
|
934
|
+
document.getElementById('comment-author').value = saved;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
571
938
|
// Switch between tabs
|
|
572
939
|
async function switchTab(tabName) {
|
|
573
940
|
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
|
574
941
|
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
575
|
-
|
|
942
|
+
|
|
576
943
|
event.target.classList.add('active');
|
|
577
944
|
document.getElementById(`${tabName}-tab`).classList.add('active');
|
|
578
|
-
|
|
945
|
+
|
|
579
946
|
if (tabName === 'view') {
|
|
580
947
|
await loadTickets();
|
|
581
948
|
renderTickets();
|
|
@@ -590,11 +957,11 @@
|
|
|
590
957
|
document.getElementById('server-label').value = labels.server;
|
|
591
958
|
}
|
|
592
959
|
}
|
|
593
|
-
|
|
960
|
+
|
|
594
961
|
// Handle form submission
|
|
595
962
|
document.getElementById('ticket-form').addEventListener('submit', async function(e) {
|
|
596
963
|
e.preventDefault();
|
|
597
|
-
|
|
964
|
+
|
|
598
965
|
const ticket = {
|
|
599
966
|
route: document.getElementById('route').value,
|
|
600
967
|
f12Errors: document.getElementById('f12-errors').value,
|
|
@@ -602,15 +969,15 @@
|
|
|
602
969
|
description: document.getElementById('description').value,
|
|
603
970
|
status: document.getElementById('status').value
|
|
604
971
|
};
|
|
605
|
-
|
|
972
|
+
|
|
606
973
|
const savedTicket = await saveTicket(ticket);
|
|
607
|
-
|
|
974
|
+
|
|
608
975
|
// Clear form
|
|
609
976
|
this.reset();
|
|
610
|
-
|
|
977
|
+
|
|
611
978
|
alert('Ticket created: ' + savedTicket.id);
|
|
612
979
|
});
|
|
613
|
-
|
|
980
|
+
|
|
614
981
|
// Render statistics
|
|
615
982
|
function renderStats() {
|
|
616
983
|
const stats = {
|
|
@@ -619,7 +986,7 @@
|
|
|
619
986
|
inProgress: tickets.filter(t => t.status === 'in-progress').length,
|
|
620
987
|
fixed: tickets.filter(t => t.status === 'fixed').length
|
|
621
988
|
};
|
|
622
|
-
|
|
989
|
+
|
|
623
990
|
const statsHTML = `
|
|
624
991
|
<div class="stat-card">
|
|
625
992
|
<div class="stat-value">${stats.total}</div>
|
|
@@ -638,26 +1005,26 @@
|
|
|
638
1005
|
<div class="stat-label">Fixed</div>
|
|
639
1006
|
</div>
|
|
640
1007
|
`;
|
|
641
|
-
|
|
1008
|
+
|
|
642
1009
|
document.getElementById('stats').innerHTML = statsHTML;
|
|
643
1010
|
}
|
|
644
|
-
|
|
1011
|
+
|
|
645
1012
|
// Render tickets
|
|
646
1013
|
function renderTickets(filteredTickets = null) {
|
|
647
1014
|
const ticketsToRender = filteredTickets || tickets;
|
|
648
1015
|
const container = document.getElementById('ticket-list');
|
|
649
1016
|
const labels = getFieldLabels();
|
|
650
|
-
|
|
1017
|
+
|
|
651
1018
|
if (ticketsToRender.length === 0) {
|
|
652
1019
|
container.innerHTML = '<p style="text-align: center; color: #999;">No tickets found.</p>';
|
|
653
1020
|
return;
|
|
654
1021
|
}
|
|
655
|
-
|
|
1022
|
+
|
|
656
1023
|
// Sort by most recent first
|
|
657
|
-
const sorted = [...ticketsToRender].sort((a, b) =>
|
|
1024
|
+
const sorted = [...ticketsToRender].sort((a, b) =>
|
|
658
1025
|
new Date(b.createdAt) - new Date(a.createdAt)
|
|
659
1026
|
);
|
|
660
|
-
|
|
1027
|
+
|
|
661
1028
|
container.innerHTML = sorted.map(ticket => `
|
|
662
1029
|
<div class="ticket-card">
|
|
663
1030
|
<div class="ticket-header">
|
|
@@ -667,74 +1034,120 @@
|
|
|
667
1034
|
${ticket.priority ? `<span class="badge priority-${ticket.priority}">${ticket.priority.toUpperCase()}</span>` : ''}
|
|
668
1035
|
</div>
|
|
669
1036
|
</div>
|
|
670
|
-
|
|
1037
|
+
|
|
671
1038
|
<div class="ticket-route">📍 ${ticket.route}</div>
|
|
672
|
-
|
|
673
|
-
${ticket.description ? `<p style="margin-bottom: 10px;">${ticket.description}</p>` : ''}
|
|
674
|
-
|
|
1039
|
+
|
|
1040
|
+
${ticket.description ? `<p style="margin-bottom: 10px;">${escapeHtml(ticket.description)}</p>` : ''}
|
|
1041
|
+
|
|
675
1042
|
${ticket.f12Errors ? `
|
|
676
1043
|
<div class="error-section">
|
|
677
1044
|
<h4>🔴 ${labels.f12}</h4>
|
|
678
|
-
<div class="error-content">${ticket.f12Errors}</div>
|
|
1045
|
+
<div class="error-content">${escapeHtml(ticket.f12Errors)}</div>
|
|
679
1046
|
</div>
|
|
680
1047
|
` : ''}
|
|
681
|
-
|
|
1048
|
+
|
|
682
1049
|
${ticket.serverErrors ? `
|
|
683
1050
|
<div class="error-section">
|
|
684
1051
|
<h4>🖥️ ${labels.server}</h4>
|
|
685
|
-
<div class="error-content">${ticket.serverErrors}</div>
|
|
1052
|
+
<div class="error-content">${escapeHtml(ticket.serverErrors)}</div>
|
|
686
1053
|
</div>
|
|
687
1054
|
` : ''}
|
|
688
|
-
|
|
1055
|
+
|
|
689
1056
|
${ticket.swarmActions && ticket.swarmActions.length > 0 ? `
|
|
690
1057
|
<div class="error-section">
|
|
691
1058
|
<h4>🤖 Swarm Actions</h4>
|
|
692
|
-
<div class="error-content">${ticket.swarmActions.map(a =>
|
|
1059
|
+
<div class="error-content">${ticket.swarmActions.map(a =>
|
|
1060
|
+
typeof a === 'string' ? escapeHtml(a) :
|
|
1061
|
+
`[${a.timestamp}] ${escapeHtml(a.action)}${a.result ? ' → ' + escapeHtml(a.result) : ''}`
|
|
1062
|
+
).join('\n')}</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
` : ''}
|
|
1065
|
+
|
|
1066
|
+
${ticket.comments && ticket.comments.length > 0 ? `
|
|
1067
|
+
<div class="comments-section">
|
|
1068
|
+
<h4>💬 Comments (${ticket.comments.length})</h4>
|
|
1069
|
+
${ticket.comments.map(c => `
|
|
1070
|
+
<div class="comment ${c.type}">
|
|
1071
|
+
<div class="comment-header">
|
|
1072
|
+
<span>
|
|
1073
|
+
<span class="comment-author">${escapeHtml(c.author)}</span>
|
|
1074
|
+
<span class="comment-type ${c.type}">${c.type}</span>
|
|
1075
|
+
</span>
|
|
1076
|
+
<span>${new Date(c.timestamp).toLocaleString()}</span>
|
|
1077
|
+
</div>
|
|
1078
|
+
<div class="comment-content">${escapeHtml(c.content)}</div>
|
|
1079
|
+
</div>
|
|
1080
|
+
`).join('')}
|
|
693
1081
|
</div>
|
|
694
1082
|
` : ''}
|
|
695
|
-
|
|
1083
|
+
|
|
696
1084
|
${ticket.namespace ? `
|
|
697
1085
|
<div style="margin-top: 10px;">
|
|
698
|
-
<strong>Namespace:</strong> <code>${ticket.namespace}</code>
|
|
1086
|
+
<strong>Namespace:</strong> <code>${escapeHtml(ticket.namespace)}</code>
|
|
699
1087
|
</div>
|
|
700
1088
|
` : ''}
|
|
701
|
-
|
|
1089
|
+
|
|
702
1090
|
<div class="ticket-footer">
|
|
703
1091
|
<div>Created: ${new Date(ticket.createdAt).toLocaleString()}</div>
|
|
704
1092
|
<div>Updated: ${new Date(ticket.updatedAt).toLocaleString()}</div>
|
|
705
1093
|
${ticket.relatedTickets && ticket.relatedTickets.length > 0 ? `
|
|
706
1094
|
<div style="margin-top: 10px;">
|
|
707
|
-
<strong>Related:</strong>
|
|
1095
|
+
<strong>Related:</strong>
|
|
708
1096
|
${ticket.relatedTickets.map(id => `<span style="color: #00d4aa; margin-right: 10px;">${id}</span>`).join('')}
|
|
709
1097
|
</div>
|
|
710
1098
|
` : ''}
|
|
711
1099
|
</div>
|
|
712
|
-
|
|
713
|
-
<
|
|
714
|
-
|
|
715
|
-
|
|
1100
|
+
|
|
1101
|
+
<div class="ticket-actions">
|
|
1102
|
+
<button class="quick-prompt-btn btn-small" onclick="copyQuickPrompt('${ticket.id}')">
|
|
1103
|
+
📋 Quick Prompt
|
|
1104
|
+
</button>
|
|
1105
|
+
<button class="btn-small" onclick="openEditModal('${ticket.id}')" style="background: #3498db;">
|
|
1106
|
+
✏️ Edit
|
|
1107
|
+
</button>
|
|
1108
|
+
<button class="btn-secondary btn-small" onclick="openCommentModal('${ticket.id}')">
|
|
1109
|
+
💬 Comment
|
|
1110
|
+
</button>
|
|
1111
|
+
${ticket.status !== 'closed' ? `
|
|
1112
|
+
<button class="btn-danger btn-small" onclick="closeTicket('${ticket.id}')">
|
|
1113
|
+
✖️ Close
|
|
1114
|
+
</button>
|
|
1115
|
+
` : `
|
|
1116
|
+
<button class="btn-secondary btn-small" onclick="reopenTicket('${ticket.id}')">
|
|
1117
|
+
🔄 Reopen
|
|
1118
|
+
</button>
|
|
1119
|
+
`}
|
|
1120
|
+
</div>
|
|
716
1121
|
</div>
|
|
717
1122
|
`).join('');
|
|
718
1123
|
}
|
|
719
|
-
|
|
1124
|
+
|
|
1125
|
+
// Escape HTML to prevent XSS
|
|
1126
|
+
function escapeHtml(text) {
|
|
1127
|
+
if (!text) return '';
|
|
1128
|
+
const div = document.createElement('div');
|
|
1129
|
+
div.textContent = text;
|
|
1130
|
+
return div.innerHTML;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
720
1133
|
// Filter tickets
|
|
721
1134
|
function filterTickets() {
|
|
722
1135
|
const statusFilter = document.getElementById('filter-status').value;
|
|
723
1136
|
const priorityFilter = document.getElementById('filter-priority').value;
|
|
724
1137
|
const searchTerm = document.getElementById('search').value.toLowerCase();
|
|
725
|
-
|
|
1138
|
+
|
|
726
1139
|
let filtered = tickets;
|
|
727
|
-
|
|
1140
|
+
|
|
728
1141
|
if (statusFilter) {
|
|
729
1142
|
filtered = filtered.filter(t => t.status === statusFilter);
|
|
730
1143
|
}
|
|
731
|
-
|
|
1144
|
+
|
|
732
1145
|
if (priorityFilter) {
|
|
733
1146
|
filtered = filtered.filter(t => t.priority === priorityFilter);
|
|
734
1147
|
}
|
|
735
|
-
|
|
1148
|
+
|
|
736
1149
|
if (searchTerm) {
|
|
737
|
-
filtered = filtered.filter(t =>
|
|
1150
|
+
filtered = filtered.filter(t =>
|
|
738
1151
|
t.id.toLowerCase().includes(searchTerm) ||
|
|
739
1152
|
t.route.toLowerCase().includes(searchTerm) ||
|
|
740
1153
|
(t.description && t.description.toLowerCase().includes(searchTerm)) ||
|
|
@@ -742,13 +1155,35 @@
|
|
|
742
1155
|
(t.serverErrors && t.serverErrors.toLowerCase().includes(searchTerm))
|
|
743
1156
|
);
|
|
744
1157
|
}
|
|
745
|
-
|
|
1158
|
+
|
|
746
1159
|
renderTickets(filtered);
|
|
747
1160
|
}
|
|
748
|
-
|
|
1161
|
+
|
|
1162
|
+
// Close modals on escape
|
|
1163
|
+
document.addEventListener('keydown', (e) => {
|
|
1164
|
+
if (e.key === 'Escape') {
|
|
1165
|
+
closeCommentModal();
|
|
1166
|
+
closeEditModal();
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
// Close modals on overlay click
|
|
1171
|
+
document.getElementById('comment-modal').addEventListener('click', (e) => {
|
|
1172
|
+
if (e.target.classList.contains('modal')) {
|
|
1173
|
+
closeCommentModal();
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
document.getElementById('edit-modal').addEventListener('click', (e) => {
|
|
1178
|
+
if (e.target.classList.contains('modal')) {
|
|
1179
|
+
closeEditModal();
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
749
1183
|
// Initialize
|
|
750
1184
|
updateProjectName();
|
|
751
1185
|
updateFormLabels();
|
|
1186
|
+
loadSavedAuthor();
|
|
752
1187
|
loadTickets();
|
|
753
1188
|
</script>
|
|
754
1189
|
</body>
|