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.
@@ -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>Claude-flow Ticket Tracker</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>🎫 Claude-flow Ticket Tracker</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="background: #666; margin-left: 10px;">Reset to Default</button>
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="background: #666; margin-left: 10px;">Reset to Default</button>
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()">&times;</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
- const API_BASE = 'http://localhost:3456/api';
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}/tickets`);
498
- useServer = response.ok;
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.textContent = '✅ Server connected - using tickets.json';
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.textContent = '⚠️ Server offline - using localStorage';
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 => typeof a === 'string' ? a : JSON.stringify(a, null, 2)).join('\n\n')}</div>
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
- <button class="quick-prompt-btn" onclick="copyQuickPrompt('${ticket.id}')">
714
- 📋 Quick Prompt
715
- </button>
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>