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