swarm-tickets 1.0.0 → 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 +9 -11
- package/ticket-cli.js +0 -0
- package/ticket-server.js +425 -269
- package/ticket-tracker.html +459 -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,70 @@
|
|
|
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>
|
|
392
537
|
</div>
|
|
393
538
|
</div>
|
|
394
539
|
</div>
|
|
395
|
-
|
|
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>
|
|
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>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
|
|
396
563
|
<script>
|
|
397
564
|
let tickets = [];
|
|
398
565
|
let useServer = false;
|
|
399
|
-
|
|
566
|
+
let storageType = 'json';
|
|
567
|
+
const API_BASE = window.location.origin + '/api';
|
|
400
568
|
const DEFAULT_PROMPT_TEMPLATE = 'Please investigate and fix ticket {TICKET_ID}.';
|
|
401
569
|
const DEFAULT_FIELD_LABELS = {
|
|
402
570
|
projectName: 'Ticket Tracker',
|
|
@@ -404,19 +572,19 @@
|
|
|
404
572
|
f12: 'F12 Console Errors',
|
|
405
573
|
server: 'Server Console Errors'
|
|
406
574
|
};
|
|
407
|
-
|
|
575
|
+
|
|
408
576
|
// Load field labels from localStorage or use defaults
|
|
409
577
|
function getFieldLabels() {
|
|
410
578
|
const stored = localStorage.getItem('claudeflow-field-labels');
|
|
411
579
|
return stored ? JSON.parse(stored) : DEFAULT_FIELD_LABELS;
|
|
412
580
|
}
|
|
413
|
-
|
|
581
|
+
|
|
414
582
|
// Update project name display
|
|
415
583
|
function updateProjectName() {
|
|
416
584
|
const labels = getFieldLabels();
|
|
417
585
|
document.getElementById('project-name').textContent = '📁 ' + labels.projectName;
|
|
418
586
|
}
|
|
419
|
-
|
|
587
|
+
|
|
420
588
|
// Update all form labels
|
|
421
589
|
function updateFormLabels() {
|
|
422
590
|
const labels = getFieldLabels();
|
|
@@ -424,12 +592,12 @@
|
|
|
424
592
|
document.getElementById('f12-form-label').textContent = labels.f12;
|
|
425
593
|
document.getElementById('server-form-label').textContent = labels.server;
|
|
426
594
|
}
|
|
427
|
-
|
|
595
|
+
|
|
428
596
|
// Load prompt template from localStorage or use default
|
|
429
597
|
function getPromptTemplate() {
|
|
430
598
|
return localStorage.getItem('claudeflow-prompt-template') || DEFAULT_PROMPT_TEMPLATE;
|
|
431
599
|
}
|
|
432
|
-
|
|
600
|
+
|
|
433
601
|
// Save prompt template to localStorage
|
|
434
602
|
function savePromptTemplate() {
|
|
435
603
|
const template = document.getElementById('prompt-template').value.trim();
|
|
@@ -440,7 +608,7 @@
|
|
|
440
608
|
localStorage.setItem('claudeflow-prompt-template', template);
|
|
441
609
|
alert('✅ Prompt template saved!');
|
|
442
610
|
}
|
|
443
|
-
|
|
611
|
+
|
|
444
612
|
// Reset prompt template to default
|
|
445
613
|
function resetPromptTemplate() {
|
|
446
614
|
if (confirm('Reset to default template?')) {
|
|
@@ -449,7 +617,7 @@
|
|
|
449
617
|
alert('✅ Template reset to default!');
|
|
450
618
|
}
|
|
451
619
|
}
|
|
452
|
-
|
|
620
|
+
|
|
453
621
|
// Save field labels to localStorage
|
|
454
622
|
function saveFieldLabels() {
|
|
455
623
|
const labels = {
|
|
@@ -463,7 +631,7 @@
|
|
|
463
631
|
updateFormLabels();
|
|
464
632
|
alert('✅ Field labels saved!');
|
|
465
633
|
}
|
|
466
|
-
|
|
634
|
+
|
|
467
635
|
// Reset field labels to defaults
|
|
468
636
|
function resetFieldLabels() {
|
|
469
637
|
if (confirm('Reset all field labels to defaults?')) {
|
|
@@ -477,12 +645,12 @@
|
|
|
477
645
|
alert('✅ Labels reset to defaults!');
|
|
478
646
|
}
|
|
479
647
|
}
|
|
480
|
-
|
|
648
|
+
|
|
481
649
|
// Copy quick prompt for a ticket
|
|
482
650
|
function copyQuickPrompt(ticketId) {
|
|
483
651
|
const template = getPromptTemplate();
|
|
484
652
|
const prompt = template.replace(/{TICKET_ID}/g, ticketId);
|
|
485
|
-
|
|
653
|
+
|
|
486
654
|
navigator.clipboard.writeText(prompt).then(() => {
|
|
487
655
|
alert('✅ Prompt copied to clipboard!\n\n' + prompt);
|
|
488
656
|
}).catch(err => {
|
|
@@ -490,12 +658,18 @@
|
|
|
490
658
|
alert('❌ Failed to copy to clipboard');
|
|
491
659
|
});
|
|
492
660
|
}
|
|
493
|
-
|
|
661
|
+
|
|
494
662
|
// Check if server is available
|
|
495
663
|
async function checkServer() {
|
|
496
664
|
try {
|
|
497
|
-
const response = await fetch(`${API_BASE}/
|
|
498
|
-
|
|
665
|
+
const response = await fetch(`${API_BASE}/health`);
|
|
666
|
+
if (response.ok) {
|
|
667
|
+
const data = await response.json();
|
|
668
|
+
useServer = true;
|
|
669
|
+
storageType = data.storage || 'json';
|
|
670
|
+
} else {
|
|
671
|
+
useServer = false;
|
|
672
|
+
}
|
|
499
673
|
updateServerStatus();
|
|
500
674
|
return useServer;
|
|
501
675
|
} catch {
|
|
@@ -504,23 +678,29 @@
|
|
|
504
678
|
return false;
|
|
505
679
|
}
|
|
506
680
|
}
|
|
507
|
-
|
|
681
|
+
|
|
508
682
|
// Update server status indicator
|
|
509
683
|
function updateServerStatus() {
|
|
510
684
|
const statusEl = document.getElementById('server-status');
|
|
511
685
|
if (useServer) {
|
|
512
686
|
statusEl.className = 'server-status server-online';
|
|
513
|
-
statusEl.
|
|
687
|
+
statusEl.innerHTML = `
|
|
688
|
+
<span>✅ Server connected</span>
|
|
689
|
+
<span class="storage-badge">${storageType}</span>
|
|
690
|
+
`;
|
|
514
691
|
} else {
|
|
515
692
|
statusEl.className = 'server-status server-offline';
|
|
516
|
-
statusEl.
|
|
693
|
+
statusEl.innerHTML = `
|
|
694
|
+
<span>⚠️ Server offline - using localStorage</span>
|
|
695
|
+
<span class="storage-badge">local</span>
|
|
696
|
+
`;
|
|
517
697
|
}
|
|
518
698
|
}
|
|
519
|
-
|
|
699
|
+
|
|
520
700
|
// Load tickets from server or localStorage
|
|
521
701
|
async function loadTickets() {
|
|
522
702
|
await checkServer();
|
|
523
|
-
|
|
703
|
+
|
|
524
704
|
if (useServer) {
|
|
525
705
|
try {
|
|
526
706
|
const response = await fetch(`${API_BASE}/tickets`);
|
|
@@ -534,7 +714,7 @@
|
|
|
534
714
|
loadFromLocalStorage();
|
|
535
715
|
}
|
|
536
716
|
}
|
|
537
|
-
|
|
717
|
+
|
|
538
718
|
// Load from localStorage
|
|
539
719
|
function loadFromLocalStorage() {
|
|
540
720
|
const stored = localStorage.getItem('claudeflow-tickets');
|
|
@@ -543,7 +723,7 @@
|
|
|
543
723
|
console.log('✅ Loaded', tickets.length, 'tickets from localStorage');
|
|
544
724
|
}
|
|
545
725
|
}
|
|
546
|
-
|
|
726
|
+
|
|
547
727
|
// Save ticket to server or localStorage
|
|
548
728
|
async function saveTicket(ticket) {
|
|
549
729
|
if (useServer) {
|
|
@@ -561,21 +741,110 @@
|
|
|
561
741
|
return ticket;
|
|
562
742
|
}
|
|
563
743
|
} else {
|
|
744
|
+
ticket.id = 'TKT-' + Date.now();
|
|
745
|
+
ticket.createdAt = new Date().toISOString();
|
|
746
|
+
ticket.updatedAt = new Date().toISOString();
|
|
747
|
+
ticket.swarmActions = [];
|
|
748
|
+
ticket.comments = [];
|
|
749
|
+
ticket.relatedTickets = [];
|
|
564
750
|
tickets.push(ticket);
|
|
565
751
|
localStorage.setItem('claudeflow-tickets', JSON.stringify(tickets));
|
|
566
752
|
console.log('✅ Ticket saved to localStorage');
|
|
567
753
|
return ticket;
|
|
568
754
|
}
|
|
569
755
|
}
|
|
570
|
-
|
|
756
|
+
|
|
757
|
+
// Close ticket
|
|
758
|
+
async function closeTicket(ticketId) {
|
|
759
|
+
if (!confirm('Close this ticket?')) return;
|
|
760
|
+
|
|
761
|
+
if (useServer) {
|
|
762
|
+
try {
|
|
763
|
+
await fetch(`${API_BASE}/tickets/${ticketId}/close`, { method: 'POST' });
|
|
764
|
+
await loadTickets();
|
|
765
|
+
renderTickets();
|
|
766
|
+
renderStats();
|
|
767
|
+
} catch (error) {
|
|
768
|
+
alert('Failed to close ticket');
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Reopen ticket
|
|
774
|
+
async function reopenTicket(ticketId) {
|
|
775
|
+
if (!confirm('Reopen this ticket?')) return;
|
|
776
|
+
|
|
777
|
+
if (useServer) {
|
|
778
|
+
try {
|
|
779
|
+
await fetch(`${API_BASE}/tickets/${ticketId}/reopen`, { method: 'POST' });
|
|
780
|
+
await loadTickets();
|
|
781
|
+
renderTickets();
|
|
782
|
+
renderStats();
|
|
783
|
+
} catch (error) {
|
|
784
|
+
alert('Failed to reopen ticket');
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Open comment modal
|
|
790
|
+
function openCommentModal(ticketId) {
|
|
791
|
+
document.getElementById('comment-ticket-id').value = ticketId;
|
|
792
|
+
document.getElementById('comment-content').value = '';
|
|
793
|
+
document.getElementById('comment-modal').classList.add('open');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Close comment modal
|
|
797
|
+
function closeCommentModal() {
|
|
798
|
+
document.getElementById('comment-modal').classList.remove('open');
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Add comment
|
|
802
|
+
async function addComment(ticketId, author, content) {
|
|
803
|
+
if (useServer) {
|
|
804
|
+
try {
|
|
805
|
+
await fetch(`${API_BASE}/tickets/${ticketId}/comments`, {
|
|
806
|
+
method: 'POST',
|
|
807
|
+
headers: { 'Content-Type': 'application/json' },
|
|
808
|
+
body: JSON.stringify({ type: 'human', author, content })
|
|
809
|
+
});
|
|
810
|
+
await loadTickets();
|
|
811
|
+
renderTickets();
|
|
812
|
+
} catch (error) {
|
|
813
|
+
alert('Failed to add comment');
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Handle comment form
|
|
819
|
+
document.getElementById('comment-form').addEventListener('submit', async function(e) {
|
|
820
|
+
e.preventDefault();
|
|
821
|
+
const ticketId = document.getElementById('comment-ticket-id').value;
|
|
822
|
+
const author = document.getElementById('comment-author').value;
|
|
823
|
+
const content = document.getElementById('comment-content').value;
|
|
824
|
+
|
|
825
|
+
// Save author name for next time
|
|
826
|
+
localStorage.setItem('claudeflow-author', author);
|
|
827
|
+
|
|
828
|
+
await addComment(ticketId, author, content);
|
|
829
|
+
closeCommentModal();
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// Load saved author name
|
|
833
|
+
function loadSavedAuthor() {
|
|
834
|
+
const saved = localStorage.getItem('claudeflow-author');
|
|
835
|
+
if (saved) {
|
|
836
|
+
document.getElementById('comment-author').value = saved;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
571
840
|
// Switch between tabs
|
|
572
841
|
async function switchTab(tabName) {
|
|
573
842
|
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
|
574
843
|
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
575
|
-
|
|
844
|
+
|
|
576
845
|
event.target.classList.add('active');
|
|
577
846
|
document.getElementById(`${tabName}-tab`).classList.add('active');
|
|
578
|
-
|
|
847
|
+
|
|
579
848
|
if (tabName === 'view') {
|
|
580
849
|
await loadTickets();
|
|
581
850
|
renderTickets();
|
|
@@ -590,11 +859,11 @@
|
|
|
590
859
|
document.getElementById('server-label').value = labels.server;
|
|
591
860
|
}
|
|
592
861
|
}
|
|
593
|
-
|
|
862
|
+
|
|
594
863
|
// Handle form submission
|
|
595
864
|
document.getElementById('ticket-form').addEventListener('submit', async function(e) {
|
|
596
865
|
e.preventDefault();
|
|
597
|
-
|
|
866
|
+
|
|
598
867
|
const ticket = {
|
|
599
868
|
route: document.getElementById('route').value,
|
|
600
869
|
f12Errors: document.getElementById('f12-errors').value,
|
|
@@ -602,15 +871,15 @@
|
|
|
602
871
|
description: document.getElementById('description').value,
|
|
603
872
|
status: document.getElementById('status').value
|
|
604
873
|
};
|
|
605
|
-
|
|
874
|
+
|
|
606
875
|
const savedTicket = await saveTicket(ticket);
|
|
607
|
-
|
|
876
|
+
|
|
608
877
|
// Clear form
|
|
609
878
|
this.reset();
|
|
610
|
-
|
|
879
|
+
|
|
611
880
|
alert('Ticket created: ' + savedTicket.id);
|
|
612
881
|
});
|
|
613
|
-
|
|
882
|
+
|
|
614
883
|
// Render statistics
|
|
615
884
|
function renderStats() {
|
|
616
885
|
const stats = {
|
|
@@ -619,7 +888,7 @@
|
|
|
619
888
|
inProgress: tickets.filter(t => t.status === 'in-progress').length,
|
|
620
889
|
fixed: tickets.filter(t => t.status === 'fixed').length
|
|
621
890
|
};
|
|
622
|
-
|
|
891
|
+
|
|
623
892
|
const statsHTML = `
|
|
624
893
|
<div class="stat-card">
|
|
625
894
|
<div class="stat-value">${stats.total}</div>
|
|
@@ -638,26 +907,26 @@
|
|
|
638
907
|
<div class="stat-label">Fixed</div>
|
|
639
908
|
</div>
|
|
640
909
|
`;
|
|
641
|
-
|
|
910
|
+
|
|
642
911
|
document.getElementById('stats').innerHTML = statsHTML;
|
|
643
912
|
}
|
|
644
|
-
|
|
913
|
+
|
|
645
914
|
// Render tickets
|
|
646
915
|
function renderTickets(filteredTickets = null) {
|
|
647
916
|
const ticketsToRender = filteredTickets || tickets;
|
|
648
917
|
const container = document.getElementById('ticket-list');
|
|
649
918
|
const labels = getFieldLabels();
|
|
650
|
-
|
|
919
|
+
|
|
651
920
|
if (ticketsToRender.length === 0) {
|
|
652
921
|
container.innerHTML = '<p style="text-align: center; color: #999;">No tickets found.</p>';
|
|
653
922
|
return;
|
|
654
923
|
}
|
|
655
|
-
|
|
924
|
+
|
|
656
925
|
// Sort by most recent first
|
|
657
|
-
const sorted = [...ticketsToRender].sort((a, b) =>
|
|
926
|
+
const sorted = [...ticketsToRender].sort((a, b) =>
|
|
658
927
|
new Date(b.createdAt) - new Date(a.createdAt)
|
|
659
928
|
);
|
|
660
|
-
|
|
929
|
+
|
|
661
930
|
container.innerHTML = sorted.map(ticket => `
|
|
662
931
|
<div class="ticket-card">
|
|
663
932
|
<div class="ticket-header">
|
|
@@ -667,74 +936,117 @@
|
|
|
667
936
|
${ticket.priority ? `<span class="badge priority-${ticket.priority}">${ticket.priority.toUpperCase()}</span>` : ''}
|
|
668
937
|
</div>
|
|
669
938
|
</div>
|
|
670
|
-
|
|
939
|
+
|
|
671
940
|
<div class="ticket-route">📍 ${ticket.route}</div>
|
|
672
|
-
|
|
673
|
-
${ticket.description ? `<p style="margin-bottom: 10px;">${ticket.description}</p>` : ''}
|
|
674
|
-
|
|
941
|
+
|
|
942
|
+
${ticket.description ? `<p style="margin-bottom: 10px;">${escapeHtml(ticket.description)}</p>` : ''}
|
|
943
|
+
|
|
675
944
|
${ticket.f12Errors ? `
|
|
676
945
|
<div class="error-section">
|
|
677
946
|
<h4>🔴 ${labels.f12}</h4>
|
|
678
|
-
<div class="error-content">${ticket.f12Errors}</div>
|
|
947
|
+
<div class="error-content">${escapeHtml(ticket.f12Errors)}</div>
|
|
679
948
|
</div>
|
|
680
949
|
` : ''}
|
|
681
|
-
|
|
950
|
+
|
|
682
951
|
${ticket.serverErrors ? `
|
|
683
952
|
<div class="error-section">
|
|
684
953
|
<h4>🖥️ ${labels.server}</h4>
|
|
685
|
-
<div class="error-content">${ticket.serverErrors}</div>
|
|
954
|
+
<div class="error-content">${escapeHtml(ticket.serverErrors)}</div>
|
|
686
955
|
</div>
|
|
687
956
|
` : ''}
|
|
688
|
-
|
|
957
|
+
|
|
689
958
|
${ticket.swarmActions && ticket.swarmActions.length > 0 ? `
|
|
690
959
|
<div class="error-section">
|
|
691
960
|
<h4>🤖 Swarm Actions</h4>
|
|
692
|
-
<div class="error-content">${ticket.swarmActions.map(a =>
|
|
961
|
+
<div class="error-content">${ticket.swarmActions.map(a =>
|
|
962
|
+
typeof a === 'string' ? escapeHtml(a) :
|
|
963
|
+
`[${a.timestamp}] ${escapeHtml(a.action)}${a.result ? ' → ' + escapeHtml(a.result) : ''}`
|
|
964
|
+
).join('\n')}</div>
|
|
965
|
+
</div>
|
|
966
|
+
` : ''}
|
|
967
|
+
|
|
968
|
+
${ticket.comments && ticket.comments.length > 0 ? `
|
|
969
|
+
<div class="comments-section">
|
|
970
|
+
<h4>💬 Comments (${ticket.comments.length})</h4>
|
|
971
|
+
${ticket.comments.map(c => `
|
|
972
|
+
<div class="comment ${c.type}">
|
|
973
|
+
<div class="comment-header">
|
|
974
|
+
<span>
|
|
975
|
+
<span class="comment-author">${escapeHtml(c.author)}</span>
|
|
976
|
+
<span class="comment-type ${c.type}">${c.type}</span>
|
|
977
|
+
</span>
|
|
978
|
+
<span>${new Date(c.timestamp).toLocaleString()}</span>
|
|
979
|
+
</div>
|
|
980
|
+
<div class="comment-content">${escapeHtml(c.content)}</div>
|
|
981
|
+
</div>
|
|
982
|
+
`).join('')}
|
|
693
983
|
</div>
|
|
694
984
|
` : ''}
|
|
695
|
-
|
|
985
|
+
|
|
696
986
|
${ticket.namespace ? `
|
|
697
987
|
<div style="margin-top: 10px;">
|
|
698
|
-
<strong>Namespace:</strong> <code>${ticket.namespace}</code>
|
|
988
|
+
<strong>Namespace:</strong> <code>${escapeHtml(ticket.namespace)}</code>
|
|
699
989
|
</div>
|
|
700
990
|
` : ''}
|
|
701
|
-
|
|
991
|
+
|
|
702
992
|
<div class="ticket-footer">
|
|
703
993
|
<div>Created: ${new Date(ticket.createdAt).toLocaleString()}</div>
|
|
704
994
|
<div>Updated: ${new Date(ticket.updatedAt).toLocaleString()}</div>
|
|
705
995
|
${ticket.relatedTickets && ticket.relatedTickets.length > 0 ? `
|
|
706
996
|
<div style="margin-top: 10px;">
|
|
707
|
-
<strong>Related:</strong>
|
|
997
|
+
<strong>Related:</strong>
|
|
708
998
|
${ticket.relatedTickets.map(id => `<span style="color: #00d4aa; margin-right: 10px;">${id}</span>`).join('')}
|
|
709
999
|
</div>
|
|
710
1000
|
` : ''}
|
|
711
1001
|
</div>
|
|
712
|
-
|
|
713
|
-
<
|
|
714
|
-
|
|
715
|
-
|
|
1002
|
+
|
|
1003
|
+
<div class="ticket-actions">
|
|
1004
|
+
<button class="quick-prompt-btn btn-small" onclick="copyQuickPrompt('${ticket.id}')">
|
|
1005
|
+
📋 Quick Prompt
|
|
1006
|
+
</button>
|
|
1007
|
+
<button class="btn-secondary btn-small" onclick="openCommentModal('${ticket.id}')">
|
|
1008
|
+
💬 Add Comment
|
|
1009
|
+
</button>
|
|
1010
|
+
${ticket.status !== 'closed' ? `
|
|
1011
|
+
<button class="btn-danger btn-small" onclick="closeTicket('${ticket.id}')">
|
|
1012
|
+
✖️ Close
|
|
1013
|
+
</button>
|
|
1014
|
+
` : `
|
|
1015
|
+
<button class="btn-secondary btn-small" onclick="reopenTicket('${ticket.id}')">
|
|
1016
|
+
🔄 Reopen
|
|
1017
|
+
</button>
|
|
1018
|
+
`}
|
|
1019
|
+
</div>
|
|
716
1020
|
</div>
|
|
717
1021
|
`).join('');
|
|
718
1022
|
}
|
|
719
|
-
|
|
1023
|
+
|
|
1024
|
+
// Escape HTML to prevent XSS
|
|
1025
|
+
function escapeHtml(text) {
|
|
1026
|
+
if (!text) return '';
|
|
1027
|
+
const div = document.createElement('div');
|
|
1028
|
+
div.textContent = text;
|
|
1029
|
+
return div.innerHTML;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
720
1032
|
// Filter tickets
|
|
721
1033
|
function filterTickets() {
|
|
722
1034
|
const statusFilter = document.getElementById('filter-status').value;
|
|
723
1035
|
const priorityFilter = document.getElementById('filter-priority').value;
|
|
724
1036
|
const searchTerm = document.getElementById('search').value.toLowerCase();
|
|
725
|
-
|
|
1037
|
+
|
|
726
1038
|
let filtered = tickets;
|
|
727
|
-
|
|
1039
|
+
|
|
728
1040
|
if (statusFilter) {
|
|
729
1041
|
filtered = filtered.filter(t => t.status === statusFilter);
|
|
730
1042
|
}
|
|
731
|
-
|
|
1043
|
+
|
|
732
1044
|
if (priorityFilter) {
|
|
733
1045
|
filtered = filtered.filter(t => t.priority === priorityFilter);
|
|
734
1046
|
}
|
|
735
|
-
|
|
1047
|
+
|
|
736
1048
|
if (searchTerm) {
|
|
737
|
-
filtered = filtered.filter(t =>
|
|
1049
|
+
filtered = filtered.filter(t =>
|
|
738
1050
|
t.id.toLowerCase().includes(searchTerm) ||
|
|
739
1051
|
t.route.toLowerCase().includes(searchTerm) ||
|
|
740
1052
|
(t.description && t.description.toLowerCase().includes(searchTerm)) ||
|
|
@@ -742,13 +1054,28 @@
|
|
|
742
1054
|
(t.serverErrors && t.serverErrors.toLowerCase().includes(searchTerm))
|
|
743
1055
|
);
|
|
744
1056
|
}
|
|
745
|
-
|
|
1057
|
+
|
|
746
1058
|
renderTickets(filtered);
|
|
747
1059
|
}
|
|
748
|
-
|
|
1060
|
+
|
|
1061
|
+
// Close modal on escape
|
|
1062
|
+
document.addEventListener('keydown', (e) => {
|
|
1063
|
+
if (e.key === 'Escape') {
|
|
1064
|
+
closeCommentModal();
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// Close modal on overlay click
|
|
1069
|
+
document.getElementById('comment-modal').addEventListener('click', (e) => {
|
|
1070
|
+
if (e.target.classList.contains('modal')) {
|
|
1071
|
+
closeCommentModal();
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
|
|
749
1075
|
// Initialize
|
|
750
1076
|
updateProjectName();
|
|
751
1077
|
updateFormLabels();
|
|
1078
|
+
loadSavedAuthor();
|
|
752
1079
|
loadTickets();
|
|
753
1080
|
</script>
|
|
754
1081
|
</body>
|