@openqa/cli 1.3.4 → 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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/brain/diff-analyzer.js +140 -0
  3. package/dist/agent/brain/diff-analyzer.js.map +1 -0
  4. package/dist/agent/brain/llm-cache.js +47 -0
  5. package/dist/agent/brain/llm-cache.js.map +1 -0
  6. package/dist/agent/brain/llm-resilience.js +252 -0
  7. package/dist/agent/brain/llm-resilience.js.map +1 -0
  8. package/dist/agent/config/index.js +588 -0
  9. package/dist/agent/config/index.js.map +1 -0
  10. package/dist/agent/coverage/index.js +74 -0
  11. package/dist/agent/coverage/index.js.map +1 -0
  12. package/dist/agent/export/index.js +158 -0
  13. package/dist/agent/export/index.js.map +1 -0
  14. package/dist/agent/index-v2.js +2795 -0
  15. package/dist/agent/index-v2.js.map +1 -0
  16. package/dist/agent/index.js +369 -105
  17. package/dist/agent/index.js.map +1 -1
  18. package/dist/agent/logger.js +41 -0
  19. package/dist/agent/logger.js.map +1 -0
  20. package/dist/agent/metrics.js +39 -0
  21. package/dist/agent/metrics.js.map +1 -0
  22. package/dist/agent/notifications/index.js +106 -0
  23. package/dist/agent/notifications/index.js.map +1 -0
  24. package/dist/agent/openapi/spec.js +338 -0
  25. package/dist/agent/openapi/spec.js.map +1 -0
  26. package/dist/agent/tools/project-runner.js +481 -0
  27. package/dist/agent/tools/project-runner.js.map +1 -0
  28. package/dist/cli/config.html.js +454 -0
  29. package/dist/cli/daemon.js +7572 -0
  30. package/dist/cli/dashboard.html.js +1619 -0
  31. package/dist/cli/index.js +3492 -1622
  32. package/dist/cli/kanban.html.js +577 -0
  33. package/dist/cli/routes.js +895 -0
  34. package/dist/cli/routes.js.map +1 -0
  35. package/dist/cli/server.js +3469 -1630
  36. package/dist/database/index.js +485 -60
  37. package/dist/database/index.js.map +1 -1
  38. package/dist/database/sqlite.js +281 -0
  39. package/dist/database/sqlite.js.map +1 -0
  40. package/package.json +18 -5
@@ -0,0 +1,577 @@
1
+ // cli/kanban.html.ts
2
+ function getKanbanHTML() {
3
+ return `<!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>OpenQA \u2014 Task Board</title>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <style>
11
+ * { box-sizing: border-box; margin: 0; padding: 0; }
12
+
13
+ /* Light Theme (default) */
14
+ :root, [data-theme="light"] {
15
+ --bg: #f5f6f8; --bg-2: #eef0f2; --white: #fff; --border: #e1e4e8;
16
+ --text: #1a1a2e; --text-2: #6b7280; --text-3: #9ca3af;
17
+ --red: #ef4444; --red-bg: #fef2f2;
18
+ --orange: #f97316; --orange-bg: #fff7ed;
19
+ --blue: #3b82f6; --blue-bg: #eff6ff;
20
+ --green: #22c55e; --green-bg: #f0fdf4;
21
+ --purple: #8b5cf6; --purple-bg: #f5f3ff;
22
+ --yellow: #eab308;
23
+ --shadow: 0 1px 3px rgba(0,0,0,0.1);
24
+ --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
25
+ --modal-bg: rgba(0,0,0,0.5);
26
+ }
27
+
28
+ /* Dark Theme */
29
+ [data-theme="dark"] {
30
+ --bg: #0f172a; --bg-2: #1e293b; --white: #1e293b; --border: #334155;
31
+ --text: #f1f5f9; --text-2: #94a3b8; --text-3: #64748b;
32
+ --red-bg: rgba(239,68,68,0.15);
33
+ --orange-bg: rgba(249,115,22,0.15);
34
+ --blue-bg: rgba(59,130,246,0.15);
35
+ --green-bg: rgba(34,197,94,0.15);
36
+ --purple-bg: rgba(139,92,246,0.15);
37
+ --shadow: 0 1px 3px rgba(0,0,0,0.3);
38
+ --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.4);
39
+ --modal-bg: rgba(0,0,0,0.7);
40
+ }
41
+
42
+ body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; font-size: 14px; transition: background 0.3s, color 0.3s; }
43
+
44
+ .header { background: var(--white); border-bottom: 1px solid var(--border); padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 100; }
45
+ .header-left { display: flex; align-items: center; gap: 24px; }
46
+ .logo { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--text); }
47
+ .logo-icon { width: 36px; height: 36px; background: linear-gradient(135deg, var(--orange), #fb923c); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
48
+ .logo-text { font-weight: 700; font-size: 20px; }
49
+ .nav-tabs { display: flex; gap: 4px; }
50
+ .nav-tab { padding: 8px 16px; border-radius: 8px; color: var(--text-2); text-decoration: none; font-weight: 500; transition: all 0.15s; }
51
+ .nav-tab:hover { background: var(--bg); color: var(--text); }
52
+ .nav-tab.active { background: var(--orange-bg); color: var(--orange); }
53
+ .header-right { display: flex; align-items: center; gap: 12px; }
54
+
55
+ .btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; border: none; transition: all 0.15s; }
56
+ .btn-ghost { background: transparent; color: var(--text-2); }
57
+ .btn-ghost:hover { background: var(--bg); color: var(--text); }
58
+ .btn-primary { background: var(--orange); color: white; }
59
+ .btn-primary:hover { background: #ea580c; }
60
+ .btn-secondary { background: var(--white); color: var(--text); border: 1px solid var(--border); }
61
+ .btn-secondary:hover { background: var(--bg); }
62
+ .btn-danger { background: var(--red); color: white; }
63
+ .btn-danger:hover { background: #dc2626; }
64
+ .btn-icon { width: 36px; height: 36px; padding: 0; display: flex; align-items: center; justify-content: center; border-radius: 8px; background: var(--bg-2); border: 1px solid var(--border); color: var(--text-2); cursor: pointer; font-size: 18px; }
65
+ .btn-icon:hover { background: var(--border); color: var(--text); }
66
+
67
+ .toolbar { background: var(--white); border-bottom: 1px solid var(--border); padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; }
68
+ .toolbar-left { display: flex; align-items: center; gap: 16px; }
69
+ .board-title { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 600; }
70
+ .filter-group { display: flex; align-items: center; gap: 8px; }
71
+ .filter-label { font-size: 13px; color: var(--text-3); }
72
+ .filter-select { padding: 6px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; background: var(--white); color: var(--text); cursor: pointer; }
73
+ .toolbar-right { display: flex; align-items: center; gap: 12px; }
74
+ .view-toggle { display: flex; background: var(--bg); border-radius: 8px; padding: 3px; }
75
+ .view-btn { padding: 6px 14px; border: none; background: transparent; border-radius: 6px; font-size: 12px; font-weight: 500; color: var(--text-2); cursor: pointer; transition: all 0.15s; }
76
+ .view-btn.active { background: var(--white); color: var(--text); box-shadow: var(--shadow); }
77
+
78
+ /* Board View */
79
+ .board { display: flex; gap: 16px; padding: 24px; overflow-x: auto; min-height: calc(100vh - 130px); }
80
+ .board.hidden { display: none; }
81
+
82
+ .column { flex: 0 0 300px; display: flex; flex-direction: column; max-height: calc(100vh - 170px); }
83
+ .column-header { padding: 12px 16px; border-radius: 12px 12px 0 0; display: flex; align-items: center; justify-content: space-between; color: white; font-weight: 600; font-size: 13px; }
84
+ .column-header.not-started { background: var(--red); }
85
+ .column-header.in-progress { background: var(--orange); }
86
+ .column-header.in-review { background: var(--blue); }
87
+ .column-header.completed { background: var(--green); }
88
+ .column-title { display: flex; align-items: center; gap: 8px; }
89
+ .column-count { background: rgba(255,255,255,0.25); padding: 2px 8px; border-radius: 10px; font-size: 12px; }
90
+ .column-menu { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; opacity: 0.8; }
91
+ .column-menu:hover { background: rgba(255,255,255,0.2); opacity: 1; }
92
+
93
+ .column-body { flex: 1; background: var(--white); border-left: 1px solid var(--border); border-right: 1px solid var(--border); padding: 8px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; min-height: 200px; }
94
+ .column-body.drag-over { background: var(--orange-bg); }
95
+ .column-footer { background: var(--white); border: 1px solid var(--border); border-top: none; border-radius: 0 0 12px 12px; padding: 8px; }
96
+
97
+ .add-task-btn { width: 100%; padding: 10px; background: transparent; border: 1px dashed var(--border); border-radius: 8px; color: var(--text-3); font-size: 13px; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; transition: all 0.15s; }
98
+ .add-task-btn:hover { border-color: var(--orange); color: var(--orange); background: var(--orange-bg); }
99
+
100
+ /* Task Card */
101
+ .task-card { background: var(--white); border: 1px solid var(--border); border-radius: 8px; padding: 12px; cursor: grab; transition: all 0.15s; position: relative; }
102
+ .task-card:hover { border-color: var(--orange); box-shadow: var(--shadow-md); }
103
+ .task-card.dragging { opacity: 0.5; transform: rotate(2deg); }
104
+
105
+ .task-actions { position: absolute; top: 8px; right: 8px; display: none; gap: 4px; }
106
+ .task-card:hover .task-actions { display: flex; }
107
+ .task-action-btn { width: 28px; height: 28px; border: none; background: var(--bg); border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; color: var(--text-2); transition: all 0.15s; }
108
+ .task-action-btn:hover { background: var(--border); color: var(--text); }
109
+ .task-action-btn.delete:hover { background: var(--red-bg); color: var(--red); }
110
+
111
+ .task-type-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin-bottom: 8px; }
112
+ .task-type-badge.bug { background: var(--red-bg); color: var(--red); }
113
+ .task-type-badge.task { background: var(--blue-bg); color: var(--blue); }
114
+ .task-type-badge.feature { background: var(--green-bg); color: var(--green); }
115
+ .task-type-badge.improvement { background: var(--purple-bg); color: var(--purple); }
116
+
117
+ .task-title { font-size: 14px; font-weight: 500; margin-bottom: 8px; line-height: 1.4; padding-right: 60px; }
118
+ .task-dates { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-3); margin-bottom: 12px; }
119
+ .task-footer { display: flex; align-items: center; justify-content: space-between; }
120
+
121
+ .priority-indicator { display: flex; align-items: center; gap: 4px; font-size: 11px; color: var(--text-3); }
122
+ .priority-dot { width: 8px; height: 8px; border-radius: 50%; }
123
+ .priority-dot.critical { background: var(--red); }
124
+ .priority-dot.high { background: var(--orange); }
125
+ .priority-dot.medium { background: var(--yellow); }
126
+ .priority-dot.low { background: var(--green); }
127
+
128
+ .task-avatar { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; color: white; background: var(--orange); }
129
+
130
+ .empty-state { padding: 32px 16px; text-align: center; color: var(--text-3); }
131
+ .empty-state-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.5; }
132
+
133
+ /* List View */
134
+ .list-view { padding: 24px; display: none; }
135
+ .list-view.active { display: block; }
136
+
137
+ .list-table { width: 100%; background: var(--white); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
138
+ .list-header { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 100px; gap: 16px; padding: 12px 16px; background: var(--bg); border-bottom: 1px solid var(--border); font-weight: 600; font-size: 12px; color: var(--text-2); text-transform: uppercase; }
139
+ .list-row { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 100px; gap: 16px; padding: 12px 16px; border-bottom: 1px solid var(--border); align-items: center; transition: background 0.15s; }
140
+ .list-row:last-child { border-bottom: none; }
141
+ .list-row:hover { background: var(--bg); }
142
+ .list-title { font-weight: 500; }
143
+ .list-actions { display: flex; gap: 8px; justify-content: flex-end; }
144
+
145
+ /* Modal */
146
+ .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--modal-bg); display: none; align-items: center; justify-content: center; z-index: 1000; }
147
+ .modal-overlay.active { display: flex; }
148
+ .modal { background: var(--white); border-radius: 12px; width: 520px; max-width: 90vw; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); }
149
+ .modal-header { padding: 20px 24px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
150
+ .modal-title { font-size: 18px; font-weight: 700; }
151
+ .modal-close { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 8px; color: var(--text-3); cursor: pointer; background: none; border: none; font-size: 20px; }
152
+ .modal-close:hover { background: var(--bg); color: var(--text); }
153
+ .modal-body { padding: 24px; }
154
+ .form-group { margin-bottom: 20px; }
155
+ .form-label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 8px; color: var(--text); }
156
+ .form-input, .form-select, .form-textarea { width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; font-family: inherit; background: var(--white); color: var(--text); }
157
+ .form-input:focus, .form-select:focus, .form-textarea:focus { outline: none; border-color: var(--orange); box-shadow: 0 0 0 3px rgba(249,115,22,0.1); }
158
+ .form-textarea { resize: vertical; min-height: 100px; }
159
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
160
+ .modal-footer { padding: 16px 24px; border-top: 1px solid var(--border); display: flex; justify-content: space-between; gap: 12px; background: var(--bg); border-radius: 0 0 12px 12px; }
161
+ .modal-footer-left { display: flex; gap: 12px; }
162
+ .modal-footer-right { display: flex; gap: 12px; }
163
+
164
+ ::-webkit-scrollbar { width: 6px; }
165
+ ::-webkit-scrollbar-track { background: transparent; }
166
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
167
+ </style>
168
+ </head>
169
+ <body data-theme="light">
170
+
171
+ <header class="header">
172
+ <div class="header-left">
173
+ <a href="/" class="logo">
174
+ <div class="logo-icon">\u{1F52C}</div>
175
+ <span class="logo-text">OpenQA</span>
176
+ </a>
177
+ <nav class="nav-tabs">
178
+ <a href="/" class="nav-tab">Dashboard</a>
179
+ <a href="/kanban" class="nav-tab active">Board</a>
180
+ <a href="/config" class="nav-tab">Config</a>
181
+ </nav>
182
+ </div>
183
+ <div class="header-right">
184
+ <button class="btn-icon" onclick="toggleTheme()" title="Toggle theme" id="theme-btn">\u{1F319}</button>
185
+ <button class="btn btn-ghost" onclick="location.reload()">\u21BB Refresh</button>
186
+ <button class="btn btn-primary" onclick="openCreateModal()">+ Add New</button>
187
+ </div>
188
+ </header>
189
+
190
+ <div class="toolbar">
191
+ <div class="toolbar-left">
192
+ <div class="board-title">\u{1F4CB} Task Board</div>
193
+ <div class="filter-group">
194
+ <span class="filter-label">Show:</span>
195
+ <select class="filter-select" id="filter-type" onchange="applyFilters()">
196
+ <option value="all">All</option>
197
+ <option value="bug">Bugs</option>
198
+ <option value="task">Tasks</option>
199
+ <option value="feature">Features</option>
200
+ </select>
201
+ </div>
202
+ </div>
203
+ <div class="toolbar-right">
204
+ <div class="view-toggle">
205
+ <button class="view-btn active" id="view-board-btn" onclick="setView('board')">Board</button>
206
+ <button class="view-btn" id="view-list-btn" onclick="setView('list')">List</button>
207
+ </div>
208
+ </div>
209
+ </div>
210
+
211
+ <div class="board" id="board-view">
212
+ <div class="column">
213
+ <div class="column-header not-started">
214
+ <div class="column-title">Not Started <span class="column-count" id="count-backlog">0</span></div>
215
+ <div class="column-menu">\u22EE</div>
216
+ </div>
217
+ <div class="column-body" id="col-backlog"></div>
218
+ <div class="column-footer">
219
+ <button class="add-task-btn" onclick="openCreateModal('backlog')">+ Add New Task</button>
220
+ </div>
221
+ </div>
222
+
223
+ <div class="column">
224
+ <div class="column-header in-progress">
225
+ <div class="column-title">In Progress <span class="column-count" id="count-in-progress">0</span></div>
226
+ <div class="column-menu">\u22EE</div>
227
+ </div>
228
+ <div class="column-body" id="col-in-progress"></div>
229
+ <div class="column-footer">
230
+ <button class="add-task-btn" onclick="openCreateModal('in-progress')">+ Add New Task</button>
231
+ </div>
232
+ </div>
233
+
234
+ <div class="column">
235
+ <div class="column-header in-review">
236
+ <div class="column-title">In Review <span class="column-count" id="count-to-do">0</span></div>
237
+ <div class="column-menu">\u22EE</div>
238
+ </div>
239
+ <div class="column-body" id="col-to-do"></div>
240
+ <div class="column-footer">
241
+ <button class="add-task-btn" onclick="openCreateModal('to-do')">+ Add New Task</button>
242
+ </div>
243
+ </div>
244
+
245
+ <div class="column">
246
+ <div class="column-header completed">
247
+ <div class="column-title">Completed <span class="column-count" id="count-done">0</span></div>
248
+ <div class="column-menu">\u22EE</div>
249
+ </div>
250
+ <div class="column-body" id="col-done"></div>
251
+ </div>
252
+ </div>
253
+
254
+ <div class="list-view" id="list-view">
255
+ <div class="list-table">
256
+ <div class="list-header">
257
+ <div>Title</div>
258
+ <div>Type</div>
259
+ <div>Status</div>
260
+ <div>Priority</div>
261
+ <div>Actions</div>
262
+ </div>
263
+ <div id="list-body"></div>
264
+ </div>
265
+ </div>
266
+
267
+ <!-- Create/Edit Modal -->
268
+ <div class="modal-overlay" id="task-modal">
269
+ <div class="modal">
270
+ <div class="modal-header">
271
+ <h2 class="modal-title" id="modal-title">Create New Task</h2>
272
+ <button class="modal-close" onclick="closeModal()">\xD7</button>
273
+ </div>
274
+ <div class="modal-body">
275
+ <input type="hidden" id="ticket-id" value="">
276
+ <div class="form-group">
277
+ <label class="form-label">Task Title *</label>
278
+ <input type="text" class="form-input" id="ticket-title" placeholder="Enter task title">
279
+ </div>
280
+ <div class="form-group">
281
+ <label class="form-label">Description</label>
282
+ <textarea class="form-textarea" id="ticket-description" placeholder="Describe the task..."></textarea>
283
+ </div>
284
+ <div class="form-row">
285
+ <div class="form-group">
286
+ <label class="form-label">Type</label>
287
+ <select class="form-select" id="ticket-type">
288
+ <option value="bug">\u{1F41B} Bug</option>
289
+ <option value="task">\u{1F4CC} Task</option>
290
+ <option value="feature">\u2728 Feature</option>
291
+ <option value="improvement">\u{1F527} Improvement</option>
292
+ </select>
293
+ </div>
294
+ <div class="form-group">
295
+ <label class="form-label">Priority</label>
296
+ <select class="form-select" id="ticket-priority">
297
+ <option value="low">Low</option>
298
+ <option value="medium" selected>Medium</option>
299
+ <option value="high">High</option>
300
+ <option value="critical">Critical</option>
301
+ </select>
302
+ </div>
303
+ </div>
304
+ <div class="form-group">
305
+ <label class="form-label">Status</label>
306
+ <select class="form-select" id="ticket-column">
307
+ <option value="backlog">Not Started</option>
308
+ <option value="in-progress">In Progress</option>
309
+ <option value="to-do">In Review</option>
310
+ <option value="done">Completed</option>
311
+ </select>
312
+ </div>
313
+ </div>
314
+ <div class="modal-footer">
315
+ <div class="modal-footer-left">
316
+ <button class="btn btn-danger" id="delete-btn" onclick="deleteTicket()" style="display:none">Delete</button>
317
+ </div>
318
+ <div class="modal-footer-right">
319
+ <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
320
+ <button class="btn btn-primary" id="save-btn" onclick="saveTicket()">Create Task</button>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ </div>
325
+
326
+ <script>
327
+ let tickets = [];
328
+ let currentFilter = 'all';
329
+ let currentView = 'board';
330
+ let editingId = null;
331
+
332
+ // Theme
333
+ function toggleTheme() {
334
+ const body = document.body;
335
+ const btn = document.getElementById('theme-btn');
336
+ if (body.dataset.theme === 'dark') {
337
+ body.dataset.theme = 'light';
338
+ btn.textContent = '\u{1F319}';
339
+ localStorage.setItem('theme', 'light');
340
+ } else {
341
+ body.dataset.theme = 'dark';
342
+ btn.textContent = '\u2600\uFE0F';
343
+ localStorage.setItem('theme', 'dark');
344
+ }
345
+ }
346
+
347
+ // Load saved theme
348
+ const savedTheme = localStorage.getItem('theme') || 'light';
349
+ document.body.dataset.theme = savedTheme;
350
+ document.getElementById('theme-btn').textContent = savedTheme === 'dark' ? '\u2600\uFE0F' : '\u{1F319}';
351
+
352
+ // View toggle
353
+ function setView(view) {
354
+ currentView = view;
355
+ document.getElementById('view-board-btn').classList.toggle('active', view === 'board');
356
+ document.getElementById('view-list-btn').classList.toggle('active', view === 'list');
357
+ document.getElementById('board-view').classList.toggle('hidden', view !== 'board');
358
+ document.getElementById('list-view').classList.toggle('active', view === 'list');
359
+ renderBoard();
360
+ }
361
+
362
+ async function loadTickets() {
363
+ try {
364
+ const res = await fetch('/api/kanban', { credentials: 'include' });
365
+ tickets = await res.json();
366
+ renderBoard();
367
+ } catch (e) { console.error(e); }
368
+ }
369
+
370
+ function getTypeIcon(type) {
371
+ return { bug: '\u{1F41B}', task: '\u{1F4CC}', feature: '\u2728', improvement: '\u{1F527}' }[type] || '\u{1F41B}';
372
+ }
373
+
374
+ function getStatusLabel(col) {
375
+ return { backlog: 'Not Started', 'in-progress': 'In Progress', 'to-do': 'In Review', done: 'Completed' }[col] || col;
376
+ }
377
+
378
+ function renderBoard() {
379
+ if (currentView === 'board') {
380
+ renderBoardView();
381
+ } else {
382
+ renderListView();
383
+ }
384
+ }
385
+
386
+ function renderBoardView() {
387
+ const cols = ['backlog', 'in-progress', 'to-do', 'done'];
388
+ cols.forEach(col => {
389
+ const container = document.getElementById('col-' + col);
390
+ let items = tickets.filter(t => t.column === col);
391
+ if (currentFilter !== 'all') items = items.filter(t => (t.type || 'bug') === currentFilter);
392
+
393
+ document.getElementById('count-' + col).textContent = items.length;
394
+
395
+ if (!items.length) {
396
+ container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u{1F4CB}</div><div>No tasks</div></div>';
397
+ } else {
398
+ container.innerHTML = items.map(t => \`
399
+ <div class="task-card" draggable="true" data-id="\${t.id}" ondragstart="dragStart(event)" ondragend="dragEnd(event)">
400
+ <div class="task-actions">
401
+ <button class="task-action-btn" onclick="event.stopPropagation(); openEditModal('\${t.id}')" title="Edit">\u270F\uFE0F</button>
402
+ <button class="task-action-btn delete" onclick="event.stopPropagation(); confirmDelete('\${t.id}')" title="Delete">\u{1F5D1}\uFE0F</button>
403
+ </div>
404
+ <div class="task-type-badge \${t.type || 'bug'}">\${getTypeIcon(t.type)} \${(t.type || 'bug').charAt(0).toUpperCase() + (t.type || 'bug').slice(1)}</div>
405
+ <div class="task-title">\${t.title}</div>
406
+ <div class="task-dates">\u{1F4C5} \${new Date(t.created_at).toLocaleDateString()}</div>
407
+ <div class="task-footer">
408
+ <div class="priority-indicator">
409
+ <span class="priority-dot \${t.priority}"></span>
410
+ \${t.priority}
411
+ </div>
412
+ <div class="task-avatar">QA</div>
413
+ </div>
414
+ </div>
415
+ \`).join('');
416
+ }
417
+ });
418
+ }
419
+
420
+ function renderListView() {
421
+ let items = [...tickets];
422
+ if (currentFilter !== 'all') items = items.filter(t => (t.type || 'bug') === currentFilter);
423
+
424
+ const listBody = document.getElementById('list-body');
425
+ if (!items.length) {
426
+ listBody.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u{1F4CB}</div><div>No tasks</div></div>';
427
+ } else {
428
+ listBody.innerHTML = items.map(t => \`
429
+ <div class="list-row">
430
+ <div class="list-title">\${t.title}</div>
431
+ <div><span class="task-type-badge \${t.type || 'bug'}">\${getTypeIcon(t.type)} \${(t.type || 'bug').charAt(0).toUpperCase() + (t.type || 'bug').slice(1)}</span></div>
432
+ <div>\${getStatusLabel(t.column)}</div>
433
+ <div class="priority-indicator"><span class="priority-dot \${t.priority}"></span> \${t.priority}</div>
434
+ <div class="list-actions">
435
+ <button class="task-action-btn" onclick="openEditModal('\${t.id}')" title="Edit">\u270F\uFE0F</button>
436
+ <button class="task-action-btn delete" onclick="confirmDelete('\${t.id}')" title="Delete">\u{1F5D1}\uFE0F</button>
437
+ </div>
438
+ </div>
439
+ \`).join('');
440
+ }
441
+ }
442
+
443
+ function applyFilters() {
444
+ currentFilter = document.getElementById('filter-type').value;
445
+ renderBoard();
446
+ }
447
+
448
+ // Drag & Drop
449
+ function dragStart(e) {
450
+ e.target.classList.add('dragging');
451
+ e.dataTransfer.setData('text/plain', e.target.dataset.id);
452
+ }
453
+
454
+ function dragEnd(e) {
455
+ e.target.classList.remove('dragging');
456
+ document.querySelectorAll('.column-body').forEach(c => c.classList.remove('drag-over'));
457
+ }
458
+
459
+ document.querySelectorAll('.column-body').forEach(col => {
460
+ col.addEventListener('dragover', e => { e.preventDefault(); col.classList.add('drag-over'); });
461
+ col.addEventListener('dragleave', () => col.classList.remove('drag-over'));
462
+ col.addEventListener('drop', async e => {
463
+ e.preventDefault();
464
+ col.classList.remove('drag-over');
465
+ const id = e.dataTransfer.getData('text/plain');
466
+ const newCol = col.id.replace('col-', '');
467
+ await fetch('/api/kanban/' + id, {
468
+ method: 'PUT',
469
+ headers: { 'Content-Type': 'application/json' },
470
+ credentials: 'include',
471
+ body: JSON.stringify({ column: newCol })
472
+ });
473
+ const t = tickets.find(x => x.id === id);
474
+ if (t) t.column = newCol;
475
+ renderBoard();
476
+ });
477
+ });
478
+
479
+ // Modal
480
+ function openCreateModal(col = 'backlog') {
481
+ editingId = null;
482
+ document.getElementById('modal-title').textContent = 'Create New Task';
483
+ document.getElementById('save-btn').textContent = 'Create Task';
484
+ document.getElementById('delete-btn').style.display = 'none';
485
+ document.getElementById('ticket-id').value = '';
486
+ document.getElementById('ticket-title').value = '';
487
+ document.getElementById('ticket-description').value = '';
488
+ document.getElementById('ticket-type').value = 'bug';
489
+ document.getElementById('ticket-priority').value = 'medium';
490
+ document.getElementById('ticket-column').value = col;
491
+ document.getElementById('task-modal').classList.add('active');
492
+ document.getElementById('ticket-title').focus();
493
+ }
494
+
495
+ function openEditModal(id) {
496
+ const ticket = tickets.find(t => t.id === id);
497
+ if (!ticket) return;
498
+
499
+ editingId = id;
500
+ document.getElementById('modal-title').textContent = 'Edit Task';
501
+ document.getElementById('save-btn').textContent = 'Save Changes';
502
+ document.getElementById('delete-btn').style.display = 'block';
503
+ document.getElementById('ticket-id').value = id;
504
+ document.getElementById('ticket-title').value = ticket.title;
505
+ document.getElementById('ticket-description').value = ticket.description || '';
506
+ document.getElementById('ticket-type').value = ticket.type || 'bug';
507
+ document.getElementById('ticket-priority').value = ticket.priority || 'medium';
508
+ document.getElementById('ticket-column').value = ticket.column;
509
+ document.getElementById('task-modal').classList.add('active');
510
+ }
511
+
512
+ function closeModal() {
513
+ document.getElementById('task-modal').classList.remove('active');
514
+ editingId = null;
515
+ }
516
+
517
+ async function saveTicket() {
518
+ const title = document.getElementById('ticket-title').value.trim();
519
+ if (!title) return alert('Please enter a title');
520
+
521
+ const data = {
522
+ title,
523
+ description: document.getElementById('ticket-description').value,
524
+ type: document.getElementById('ticket-type').value,
525
+ priority: document.getElementById('ticket-priority').value,
526
+ column: document.getElementById('ticket-column').value
527
+ };
528
+
529
+ if (editingId) {
530
+ await fetch('/api/kanban/' + editingId, {
531
+ method: 'PUT',
532
+ headers: { 'Content-Type': 'application/json' },
533
+ credentials: 'include',
534
+ body: JSON.stringify(data)
535
+ });
536
+ } else {
537
+ await fetch('/api/kanban', {
538
+ method: 'POST',
539
+ headers: { 'Content-Type': 'application/json' },
540
+ credentials: 'include',
541
+ body: JSON.stringify(data)
542
+ });
543
+ }
544
+
545
+ closeModal();
546
+ loadTickets();
547
+ }
548
+
549
+ function confirmDelete(id) {
550
+ if (confirm('Are you sure you want to delete this task?')) {
551
+ deleteTicketById(id);
552
+ }
553
+ }
554
+
555
+ async function deleteTicket() {
556
+ if (!editingId) return;
557
+ if (confirm('Are you sure you want to delete this task?')) {
558
+ await deleteTicketById(editingId);
559
+ closeModal();
560
+ }
561
+ }
562
+
563
+ async function deleteTicketById(id) {
564
+ await fetch('/api/kanban/' + id, { method: 'DELETE', credentials: 'include' });
565
+ loadTickets();
566
+ }
567
+
568
+ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
569
+ loadTickets();
570
+ </script>
571
+
572
+ </body>
573
+ </html>`;
574
+ }
575
+ export {
576
+ getKanbanHTML
577
+ };