@autocode-cli/autocode 0.0.22

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 (125) hide show
  1. package/README.md +172 -0
  2. package/bin/autocode +2 -0
  3. package/dist/cli/commands/comment.d.ts +9 -0
  4. package/dist/cli/commands/comment.d.ts.map +1 -0
  5. package/dist/cli/commands/comment.js +37 -0
  6. package/dist/cli/commands/comment.js.map +1 -0
  7. package/dist/cli/commands/init.d.ts +9 -0
  8. package/dist/cli/commands/init.d.ts.map +1 -0
  9. package/dist/cli/commands/init.js +41 -0
  10. package/dist/cli/commands/init.js.map +1 -0
  11. package/dist/cli/commands/list.d.ts +9 -0
  12. package/dist/cli/commands/list.d.ts.map +1 -0
  13. package/dist/cli/commands/list.js +78 -0
  14. package/dist/cli/commands/list.js.map +1 -0
  15. package/dist/cli/commands/move.d.ts +9 -0
  16. package/dist/cli/commands/move.d.ts.map +1 -0
  17. package/dist/cli/commands/move.js +59 -0
  18. package/dist/cli/commands/move.js.map +1 -0
  19. package/dist/cli/commands/new.d.ts +9 -0
  20. package/dist/cli/commands/new.d.ts.map +1 -0
  21. package/dist/cli/commands/new.js +74 -0
  22. package/dist/cli/commands/new.js.map +1 -0
  23. package/dist/cli/commands/next.d.ts +9 -0
  24. package/dist/cli/commands/next.d.ts.map +1 -0
  25. package/dist/cli/commands/next.js +46 -0
  26. package/dist/cli/commands/next.js.map +1 -0
  27. package/dist/cli/commands/serve.d.ts +9 -0
  28. package/dist/cli/commands/serve.d.ts.map +1 -0
  29. package/dist/cli/commands/serve.js +55 -0
  30. package/dist/cli/commands/serve.js.map +1 -0
  31. package/dist/cli/commands/show.d.ts +9 -0
  32. package/dist/cli/commands/show.d.ts.map +1 -0
  33. package/dist/cli/commands/show.js +91 -0
  34. package/dist/cli/commands/show.js.map +1 -0
  35. package/dist/cli/parser.d.ts +13 -0
  36. package/dist/cli/parser.d.ts.map +1 -0
  37. package/dist/cli/parser.js +54 -0
  38. package/dist/cli/parser.js.map +1 -0
  39. package/dist/core/column.d.ts +53 -0
  40. package/dist/core/column.d.ts.map +1 -0
  41. package/dist/core/column.js +128 -0
  42. package/dist/core/column.js.map +1 -0
  43. package/dist/core/ticket.d.ts +50 -0
  44. package/dist/core/ticket.d.ts.map +1 -0
  45. package/dist/core/ticket.js +228 -0
  46. package/dist/core/ticket.js.map +1 -0
  47. package/dist/core/workflow.d.ts +66 -0
  48. package/dist/core/workflow.d.ts.map +1 -0
  49. package/dist/core/workflow.js +176 -0
  50. package/dist/core/workflow.js.map +1 -0
  51. package/dist/index.d.ts +8 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +9 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/server/api.d.ts +9 -0
  56. package/dist/server/api.d.ts.map +1 -0
  57. package/dist/server/api.js +313 -0
  58. package/dist/server/api.js.map +1 -0
  59. package/dist/server/dashboard.d.ts +8 -0
  60. package/dist/server/dashboard.d.ts.map +1 -0
  61. package/dist/server/dashboard.js +1865 -0
  62. package/dist/server/dashboard.js.map +1 -0
  63. package/dist/server/index.d.ts +8 -0
  64. package/dist/server/index.d.ts.map +1 -0
  65. package/dist/server/index.js +94 -0
  66. package/dist/server/index.js.map +1 -0
  67. package/dist/server/watcher.d.ts +13 -0
  68. package/dist/server/watcher.d.ts.map +1 -0
  69. package/dist/server/watcher.js +62 -0
  70. package/dist/server/watcher.js.map +1 -0
  71. package/dist/server/websocket.d.ts +26 -0
  72. package/dist/server/websocket.d.ts.map +1 -0
  73. package/dist/server/websocket.js +165 -0
  74. package/dist/server/websocket.js.map +1 -0
  75. package/dist/services/claude.d.ts +52 -0
  76. package/dist/services/claude.d.ts.map +1 -0
  77. package/dist/services/claude.js +304 -0
  78. package/dist/services/claude.js.map +1 -0
  79. package/dist/services/ticket-io.d.ts +76 -0
  80. package/dist/services/ticket-io.d.ts.map +1 -0
  81. package/dist/services/ticket-io.js +248 -0
  82. package/dist/services/ticket-io.js.map +1 -0
  83. package/dist/types/index.d.ts +79 -0
  84. package/dist/types/index.d.ts.map +1 -0
  85. package/dist/types/index.js +5 -0
  86. package/dist/types/index.js.map +1 -0
  87. package/dist/utils/config.d.ts +93 -0
  88. package/dist/utils/config.d.ts.map +1 -0
  89. package/dist/utils/config.js +96 -0
  90. package/dist/utils/config.js.map +1 -0
  91. package/dist/utils/fs.d.ts +60 -0
  92. package/dist/utils/fs.d.ts.map +1 -0
  93. package/dist/utils/fs.js +129 -0
  94. package/dist/utils/fs.js.map +1 -0
  95. package/dist/utils/logger.d.ts +23 -0
  96. package/dist/utils/logger.d.ts.map +1 -0
  97. package/dist/utils/logger.js +56 -0
  98. package/dist/utils/logger.js.map +1 -0
  99. package/package.json +57 -0
  100. package/templates/columns/00_backlog.en.md +31 -0
  101. package/templates/columns/00_backlog.fr.md +31 -0
  102. package/templates/columns/01_ready.en.md +30 -0
  103. package/templates/columns/01_ready.fr.md +30 -0
  104. package/templates/columns/02_in-progress.en.md +30 -0
  105. package/templates/columns/02_in-progress.fr.md +30 -0
  106. package/templates/columns/03_review-best-practices.en.md +31 -0
  107. package/templates/columns/03_review-best-practices.fr.md +31 -0
  108. package/templates/columns/04_review-no-duplication.en.md +30 -0
  109. package/templates/columns/04_review-no-duplication.fr.md +30 -0
  110. package/templates/columns/05_review-consistency.en.md +31 -0
  111. package/templates/columns/05_review-consistency.fr.md +31 -0
  112. package/templates/columns/06_review-security.en.md +32 -0
  113. package/templates/columns/06_review-security.fr.md +32 -0
  114. package/templates/columns/07_testing-playwright.en.md +30 -0
  115. package/templates/columns/07_testing-playwright.fr.md +30 -0
  116. package/templates/columns/08_testing-cypress.en.md +31 -0
  117. package/templates/columns/08_testing-cypress.fr.md +31 -0
  118. package/templates/columns/09_update-docs.en.md +29 -0
  119. package/templates/columns/09_update-docs.fr.md +29 -0
  120. package/templates/columns/10_deploy-staging.en.md +29 -0
  121. package/templates/columns/10_deploy-staging.fr.md +29 -0
  122. package/templates/columns/11_validate-staging.en.md +32 -0
  123. package/templates/columns/11_validate-staging.fr.md +32 -0
  124. package/templates/columns/12_done.en.md +23 -0
  125. package/templates/columns/12_done.fr.md +23 -0
@@ -0,0 +1,1865 @@
1
+ /**
2
+ * Dashboard HTML generation - Full feature parity with legacy autocode.sh
3
+ */
4
+ import { getColumns } from '../core/column.js';
5
+ import { listTickets } from '../core/ticket.js';
6
+ import { getWorkflowSummary } from '../core/workflow.js';
7
+ import { getConfig } from '../utils/config.js';
8
+ /**
9
+ * Generate the full dashboard HTML
10
+ */
11
+ export function generateDashboard() {
12
+ const config = getConfig();
13
+ const tickets = listTickets(config.root);
14
+ const columns = getColumns();
15
+ const summary = getWorkflowSummary(tickets);
16
+ // Prepare data for client-side rendering
17
+ const columnsJson = JSON.stringify(columns);
18
+ const ticketsJson = JSON.stringify(tickets);
19
+ const timestamp = new Date().toLocaleString('en-US');
20
+ return `<!DOCTYPE html>
21
+ <html lang="en">
22
+ <head>
23
+ <meta charset="UTF-8">
24
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
25
+ <title>AutoCode Dashboard</title>
26
+ <style>
27
+ ${getStyles()}
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <div id="notifications" class="notifications"></div>
32
+
33
+ <header>
34
+ <div class="header-logo">
35
+ <a href="#" class="logo-link"><span>⚡</span> AutoCode</a>
36
+ <span class="header-title">Dashboard</span>
37
+ </div>
38
+ <div class="stats" id="stats"></div>
39
+ <div class="filters">
40
+ <select id="filter-priority">
41
+ <option value="" data-i18n="filter.allPriorities">All priorities</option>
42
+ <option value="P0">P0</option>
43
+ <option value="P1">P1</option>
44
+ <option value="P2">P2</option>
45
+ <option value="P3">P3</option>
46
+ </select>
47
+ <input type="text" id="filter-search" placeholder="Search..." data-i18n-placeholder="filter.search">
48
+ <select id="ui-lang" onchange="switchLanguage(this.value)" title="Interface language">
49
+ <option value="en">EN</option>
50
+ <option value="fr">FR</option>
51
+ </select>
52
+ <button class="btn btn-primary" onclick="openModal()" data-i18n="btn.newTicket">+ New ticket</button>
53
+ </div>
54
+ </header>
55
+
56
+ <main class="board" id="board"></main>
57
+
58
+ <!-- Modal Ticket (create/edit) -->
59
+ <div class="modal-overlay" id="modal-overlay">
60
+ <div class="modal">
61
+ <div class="modal-header">
62
+ <h2 id="modal-title" data-i18n="modal.newTicket">New ticket</h2>
63
+ <button class="modal-close" onclick="closeModal()">&times;</button>
64
+ </div>
65
+ <div class="modal-body">
66
+ <div class="form-group">
67
+ <label for="ticket-title" data-i18n="modal.title">Title *</label>
68
+ <input type="text" id="ticket-title" placeholder="E.g.: Fix the login bug" data-i18n-placeholder="modal.titlePlaceholder">
69
+ </div>
70
+ <div class="form-group">
71
+ <label for="ticket-description" data-i18n="modal.description">Description</label>
72
+ <textarea id="ticket-description" placeholder="Describe the context and details..." data-i18n-placeholder="modal.descriptionPlaceholder"></textarea>
73
+ </div>
74
+ <div class="form-row">
75
+ <div class="form-group">
76
+ <label for="ticket-priority" data-i18n="modal.priority">Priority</label>
77
+ <select id="ticket-priority">
78
+ <option value="P2" data-i18n="priority.p2">P2 - Normal</option>
79
+ <option value="P0" data-i18n="priority.p0">P0 - Critical</option>
80
+ <option value="P1" data-i18n="priority.p1">P1 - High</option>
81
+ <option value="P3" data-i18n="priority.p3">P3 - Low</option>
82
+ </select>
83
+ </div>
84
+ <div class="form-group">
85
+ <label for="ticket-semver" data-i18n="modal.releaseType">Release type</label>
86
+ <select id="ticket-semver">
87
+ <option value="patch" data-i18n="semver.patch">Patch (x.x.X) - Bug fix</option>
88
+ <option value="minor" data-i18n="semver.minor">Minor (x.X.0) - Feature</option>
89
+ <option value="major" data-i18n="semver.major">Major (X.0.0) - Breaking</option>
90
+ <option value="none" data-i18n="semver.none">No deployment</option>
91
+ </select>
92
+ </div>
93
+ </div>
94
+ <div class="form-group labels-select">
95
+ <label for="ticket-labels" data-i18n="modal.labels">Labels</label>
96
+ <select id="ticket-labels" onchange="addLabel(this)">
97
+ <option value="" data-i18n="modal.selectLabel">Select a label...</option>
98
+ <option value="bug">bug</option>
99
+ <option value="feature">feature</option>
100
+ <option value="enhancement">enhancement</option>
101
+ <option value="documentation">documentation</option>
102
+ <option value="urgent">urgent</option>
103
+ <option value="refactoring">refactoring</option>
104
+ <option value="security">security</option>
105
+ <option value="performance">performance</option>
106
+ <option value="ui">ui</option>
107
+ <option value="api">api</option>
108
+ </select>
109
+ <div class="selected-labels" id="selected-labels"></div>
110
+ </div>
111
+ <div class="form-group">
112
+ <label data-i18n="modal.acceptanceCriteria">Acceptance criteria</label>
113
+ <div class="criteria-list" id="criteria-list"></div>
114
+ <button type="button" class="btn-add" onclick="addCriteria()">
115
+ <span>+</span> <span data-i18n="modal.addCriteria">Add criteria</span>
116
+ </button>
117
+ </div>
118
+ <div class="comments-section" id="comments-section" style="display:none">
119
+ <div class="comments-header">
120
+ <h3 data-i18n="modal.comments">Comments</h3>
121
+ <span class="comments-count" id="comments-count">0</span>
122
+ </div>
123
+ <div class="comments-list" id="comments-list">
124
+ <div class="no-comments" data-i18n="modal.noComments">No comments</div>
125
+ </div>
126
+ <div class="add-comment">
127
+ <textarea id="new-comment" placeholder="Add a comment..." data-i18n-placeholder="modal.addCommentPlaceholder"></textarea>
128
+ <button class="btn-comment" onclick="addComment()" data-i18n="btn.add">Add</button>
129
+ </div>
130
+ </div>
131
+ <div class="claude-log-section" id="claude-log-section" style="display:none">
132
+ <div class="claude-log-header">
133
+ <h3 data-i18n="modal.claudeTerminal">Claude Terminal</h3>
134
+ <span class="claude-log-status" id="claude-log-status" data-i18n="status.waiting">Waiting</span>
135
+ </div>
136
+ <pre class="claude-log" id="claude-log"></pre>
137
+ </div>
138
+ <div class="modal-actions">
139
+ <button class="btn btn-secondary" onclick="closeModal()" data-i18n="btn.cancel">Cancel</button>
140
+ <button class="btn btn-danger" id="btn-archive" style="display:none" onclick="archiveTicket()" data-i18n="btn.archive">Archive</button>
141
+ <button class="btn btn-next" id="btn-next" style="display:none" onclick="advanceTicket()" data-i18n="btn.next">Next</button>
142
+ <button class="btn btn-primary" id="btn-save" onclick="saveTicket()" data-i18n="btn.createTicket">Create ticket</button>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ <!-- Modal ACTION.md -->
149
+ <div class="modal-overlay" id="action-modal">
150
+ <div class="modal action-modal">
151
+ <div class="modal-header">
152
+ <div>
153
+ <span class="pill">ACTION.md</span>
154
+ <h2 id="action-modal-title" data-i18n="action.instructions">Instructions</h2>
155
+ </div>
156
+ <button class="modal-close" onclick="closeActionModal()">&times;</button>
157
+ </div>
158
+ <div class="modal-body">
159
+ <div class="action-toolbar">
160
+ <button class="btn btn-secondary" onclick="reloadActionContent()" data-i18n="btn.reload">Reload</button>
161
+ <button class="btn btn-secondary" id="action-edit-btn" onclick="enterActionEdit()" data-i18n="btn.edit">Edit</button>
162
+ <button class="btn btn-primary" id="action-save-btn" onclick="saveActionContent()" disabled data-i18n="btn.save">Save</button>
163
+ </div>
164
+ <div class="action-meta" id="action-meta"></div>
165
+ <div class="action-empty" id="action-empty" style="display:none"></div>
166
+ <textarea id="action-content" readonly placeholder="No instructions available" data-i18n-placeholder="action.noInstructions"></textarea>
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <!-- Context menu -->
172
+ <div class="context-menu" id="context-menu">
173
+ <div class="context-menu-item" onclick="editFromContext()" data-i18n="btn.edit">Edit</div>
174
+ <div class="context-menu-item danger" onclick="archiveFromContext()" data-i18n="btn.archive">Archive</div>
175
+ </div>
176
+
177
+ <footer>
178
+ <span>AutoCode v2.0 | <span id="time">${timestamp}</span></span>
179
+ </footer>
180
+
181
+ <script>
182
+ const COLUMNS = ${columnsJson};
183
+ const TICKETS = ${ticketsJson};
184
+ ${getScript()}
185
+ </script>
186
+ </body>
187
+ </html>`;
188
+ }
189
+ /**
190
+ * Escape HTML entities
191
+ */
192
+ function escapeHtml(text) {
193
+ return text
194
+ .replace(/&/g, '&amp;')
195
+ .replace(/</g, '&lt;')
196
+ .replace(/>/g, '&gt;')
197
+ .replace(/"/g, '&quot;');
198
+ }
199
+ /**
200
+ * Get CSS styles
201
+ */
202
+ function getStyles() {
203
+ return `
204
+ :root {
205
+ --bg-primary: #0a0a0f;
206
+ --bg-secondary: #12121a;
207
+ --bg-tertiary: #1a1a25;
208
+ --bg: #0a0a0f;
209
+ --card: #12121a;
210
+ --border: #2a2a3a;
211
+ --text: #f1f5f9;
212
+ --text-primary: #f1f5f9;
213
+ --text-secondary: #94a3b8;
214
+ --muted: #64748b;
215
+ --accent: #6366f1;
216
+ --accent-light: #818cf8;
217
+ --accent-hover: #818cf8;
218
+ --accent-glow: rgba(99, 102, 241, 0.3);
219
+ --blue: #3b82f6;
220
+ --green: #22c55e;
221
+ --yellow: #f59e0b;
222
+ --red: #ef4444;
223
+ --gradient-1: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);
224
+ }
225
+
226
+ * { box-sizing: border-box; margin: 0; padding: 0; }
227
+
228
+ body {
229
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
230
+ background: var(--bg);
231
+ color: var(--text);
232
+ min-height: 100vh;
233
+ }
234
+
235
+ /* Header */
236
+ header {
237
+ background: var(--bg-secondary);
238
+ padding: 12px 24px;
239
+ border-bottom: 1px solid var(--border);
240
+ display: flex;
241
+ justify-content: space-between;
242
+ align-items: center;
243
+ flex-wrap: wrap;
244
+ gap: 12px;
245
+ height: 60px;
246
+ }
247
+
248
+ .header-logo {
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 12px;
252
+ }
253
+
254
+ .logo-link {
255
+ font-size: 1.25rem;
256
+ font-weight: 700;
257
+ background: var(--gradient-1);
258
+ -webkit-background-clip: text;
259
+ -webkit-text-fill-color: transparent;
260
+ background-clip: text;
261
+ text-decoration: none;
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 8px;
265
+ }
266
+
267
+ .header-title {
268
+ color: var(--text-secondary);
269
+ font-size: 0.9rem;
270
+ padding-left: 12px;
271
+ border-left: 1px solid var(--border);
272
+ }
273
+
274
+ h1 { font-size: 20px; font-weight: 600; color: var(--accent); }
275
+
276
+ .stats { display: flex; gap: 8px; flex-wrap: wrap; }
277
+ .stat {
278
+ background: var(--bg-tertiary);
279
+ padding: 6px 10px;
280
+ border-radius: 6px;
281
+ font-size: 13px;
282
+ }
283
+ .stat.connected { color: var(--green); }
284
+ .stat.P0 { color: var(--red); }
285
+ .stat.P1 { color: var(--yellow); }
286
+ .stat.P2 { color: var(--blue); }
287
+
288
+ .filters { display: flex; gap: 8px; align-items: center; }
289
+
290
+ select, input, textarea {
291
+ background: var(--bg-primary);
292
+ border: 1px solid var(--border);
293
+ color: var(--text);
294
+ padding: 6px 10px;
295
+ border-radius: 8px;
296
+ font-size: 13px;
297
+ transition: border-color 0.3s;
298
+ }
299
+ select:focus, input:focus, textarea:focus {
300
+ outline: none;
301
+ border-color: var(--accent);
302
+ }
303
+
304
+ .btn {
305
+ padding: 10px 24px;
306
+ border: none;
307
+ border-radius: 8px;
308
+ font-size: 0.9rem;
309
+ font-weight: 600;
310
+ cursor: pointer;
311
+ transition: all 0.3s;
312
+ }
313
+ .btn-primary { background: var(--accent); color: #fff; }
314
+ .btn-primary:hover { background: var(--accent-light); transform: translateY(-2px); }
315
+ .btn-secondary { background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border); }
316
+ .btn-secondary:hover { border-color: var(--accent); }
317
+ .btn-danger { background: var(--red); color: #fff; }
318
+ .btn-next { background: var(--blue); color: #fff; }
319
+
320
+ /* Board */
321
+ .board {
322
+ display: flex;
323
+ gap: 16px;
324
+ padding: 20px;
325
+ overflow-x: auto;
326
+ height: calc(100vh - 60px - 50px);
327
+ padding-bottom: 10px;
328
+ }
329
+
330
+ .column {
331
+ min-width: 300px;
332
+ max-width: 350px;
333
+ flex: 1;
334
+ background: var(--bg-secondary);
335
+ border: 1px solid var(--border);
336
+ border-radius: 12px;
337
+ display: flex;
338
+ flex-direction: column;
339
+ transition: border-color 0.2s;
340
+ }
341
+ .column.drag-over {
342
+ border-color: var(--accent);
343
+ background: rgba(99, 102, 241, 0.1);
344
+ }
345
+
346
+ .column-header {
347
+ padding: 16px;
348
+ display: flex;
349
+ justify-content: space-between;
350
+ align-items: center;
351
+ border-bottom: 1px solid var(--border);
352
+ }
353
+ .column-title { font-size: 14px; font-weight: 600; }
354
+ .column-header-actions { display: flex; gap: 10px; align-items: center; }
355
+ .column-count {
356
+ background: var(--bg-tertiary);
357
+ color: var(--text-secondary);
358
+ padding: 2px 10px;
359
+ border-radius: 10px;
360
+ font-size: 0.85rem;
361
+ font-weight: 600;
362
+ }
363
+ .btn-action {
364
+ background: none;
365
+ border: none;
366
+ cursor: pointer;
367
+ font-size: 16px;
368
+ padding: 4px;
369
+ opacity: 0.6;
370
+ transition: opacity 0.3s;
371
+ }
372
+ .btn-action:hover { opacity: 1; }
373
+
374
+ .column-body {
375
+ padding: 12px;
376
+ flex: 1;
377
+ overflow-y: auto;
378
+ display: flex;
379
+ flex-direction: column;
380
+ gap: 10px;
381
+ }
382
+
383
+ .empty {
384
+ text-align: center;
385
+ color: var(--muted);
386
+ padding: 24px;
387
+ font-size: 13px;
388
+ }
389
+
390
+ /* Tickets */
391
+ .ticket {
392
+ background: var(--bg-primary);
393
+ border: 1px solid var(--border);
394
+ border-radius: 8px;
395
+ padding: 14px;
396
+ cursor: pointer;
397
+ transition: all 0.3s ease;
398
+ }
399
+ .ticket:hover {
400
+ border-color: var(--accent);
401
+ transform: translateY(-2px);
402
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
403
+ }
404
+ .ticket.dragging {
405
+ opacity: 0.5;
406
+ transform: rotate(3deg) scale(1.05);
407
+ box-shadow: 0 15px 40px var(--accent-glow);
408
+ z-index: 100;
409
+ }
410
+
411
+ .ticket-key {
412
+ font-family: 'JetBrains Mono', monospace;
413
+ font-size: 0.8rem;
414
+ color: var(--accent-light);
415
+ font-weight: 600;
416
+ margin-bottom: 4px;
417
+ }
418
+ .ticket-title {
419
+ font-size: 0.95rem;
420
+ font-weight: 500;
421
+ line-height: 1.4;
422
+ margin-bottom: 10px;
423
+ }
424
+ .ticket-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
425
+
426
+ .priority {
427
+ font-size: 0.7rem;
428
+ font-weight: 700;
429
+ padding: 3px 8px;
430
+ border-radius: 4px;
431
+ }
432
+ .priority.P0 { background: #ef444420; color: #ef4444; }
433
+ .priority.P1 { background: #f59e0b20; color: #f59e0b; }
434
+ .priority.P2 { background: #3b82f620; color: #3b82f6; }
435
+ .priority.P3 { background: #64748b20; color: #64748b; }
436
+
437
+ /* Claude Processing Indicator */
438
+ .ticket.claude-processing {
439
+ border-color: #a855f7;
440
+ animation: claudePulse 2s ease-in-out infinite;
441
+ box-shadow: 0 0 20px rgba(168, 85, 247, 0.3);
442
+ }
443
+
444
+ @keyframes claudePulse {
445
+ 0%, 100% {
446
+ box-shadow: 0 0 10px rgba(168, 85, 247, 0.2);
447
+ border-color: #a855f7;
448
+ }
449
+ 50% {
450
+ box-shadow: 0 0 25px rgba(168, 85, 247, 0.5);
451
+ border-color: #c084fc;
452
+ }
453
+ }
454
+
455
+ .ticket-claude-indicator {
456
+ margin-top: 10px;
457
+ padding-top: 10px;
458
+ border-top: 1px solid var(--border);
459
+ display: flex;
460
+ align-items: center;
461
+ gap: 8px;
462
+ font-size: 0.8rem;
463
+ color: #a855f7;
464
+ }
465
+
466
+ .claude-dot {
467
+ width: 8px;
468
+ height: 8px;
469
+ background: #a855f7;
470
+ border-radius: 50%;
471
+ animation: claudeDotPulse 1.5s ease infinite;
472
+ }
473
+
474
+ @keyframes claudeDotPulse {
475
+ 0%, 100% { opacity: 1; transform: scale(1); }
476
+ 50% { opacity: 0.5; transform: scale(1.3); }
477
+ }
478
+
479
+ /* Modal */
480
+ .modal-overlay {
481
+ position: fixed;
482
+ inset: 0;
483
+ background: rgba(0,0,0,0.8);
484
+ backdrop-filter: blur(5px);
485
+ z-index: 1000;
486
+ display: none;
487
+ overflow-y: auto;
488
+ }
489
+ .modal-overlay.active { display: block; }
490
+
491
+ .modal {
492
+ width: 100%;
493
+ min-height: 100vh;
494
+ padding: 24px 48px;
495
+ max-width: 100%;
496
+ margin: 0;
497
+ background: var(--bg-secondary);
498
+ }
499
+
500
+ .modal-header {
501
+ display: flex;
502
+ justify-content: space-between;
503
+ align-items: center;
504
+ margin-bottom: 24px;
505
+ padding-bottom: 16px;
506
+ border-bottom: 1px solid var(--border);
507
+ }
508
+ .modal-header h2 { font-size: 1.1rem; font-weight: 600; }
509
+ .modal-close {
510
+ background: none;
511
+ border: none;
512
+ color: var(--text-secondary);
513
+ font-size: 1.5rem;
514
+ cursor: pointer;
515
+ line-height: 1;
516
+ padding: 0;
517
+ }
518
+ .modal-close:hover { color: var(--text-primary); }
519
+
520
+ .modal-body { display: flex; flex-direction: column; gap: 16px; }
521
+
522
+ .form-group { display: flex; flex-direction: column; gap: 6px; }
523
+ .form-group label { font-size: 13px; color: var(--muted); font-weight: 500; }
524
+ .form-group input, .form-group textarea, .form-group select {
525
+ padding: 10px 12px;
526
+ font-size: 14px;
527
+ }
528
+ .form-group textarea { min-height: 100px; resize: vertical; }
529
+
530
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
531
+
532
+ .labels-select .selected-labels {
533
+ display: flex;
534
+ flex-wrap: wrap;
535
+ gap: 6px;
536
+ margin-top: 8px;
537
+ }
538
+ .label-tag {
539
+ background: #21262d;
540
+ padding: 4px 8px;
541
+ border-radius: 4px;
542
+ font-size: 12px;
543
+ display: flex;
544
+ align-items: center;
545
+ gap: 6px;
546
+ }
547
+ .remove-label {
548
+ cursor: pointer;
549
+ opacity: 0.6;
550
+ }
551
+ .remove-label:hover { opacity: 1; color: var(--red); }
552
+
553
+ .criteria-list { display: flex; flex-direction: column; gap: 8px; }
554
+ .criteria-item {
555
+ display: flex;
556
+ gap: 8px;
557
+ align-items: center;
558
+ }
559
+ .criteria-item input { flex: 1; }
560
+ .btn-remove {
561
+ background: none;
562
+ border: none;
563
+ color: var(--red);
564
+ font-size: 18px;
565
+ cursor: pointer;
566
+ }
567
+ .btn-add {
568
+ background: none;
569
+ border: 1px dashed var(--border);
570
+ color: var(--muted);
571
+ padding: 8px 12px;
572
+ border-radius: 6px;
573
+ cursor: pointer;
574
+ font-size: 13px;
575
+ margin-top: 8px;
576
+ }
577
+ .btn-add:hover { border-color: var(--accent); color: var(--accent); }
578
+
579
+ .comments-section {
580
+ border-top: 1px solid var(--border);
581
+ padding-top: 16px;
582
+ margin-top: 8px;
583
+ }
584
+ .comments-header {
585
+ display: flex;
586
+ align-items: center;
587
+ gap: 8px;
588
+ margin-bottom: 12px;
589
+ }
590
+ .comments-header h3 { font-size: 14px; font-weight: 600; }
591
+ .comments-count {
592
+ background: #21262d;
593
+ padding: 2px 8px;
594
+ border-radius: 10px;
595
+ font-size: 12px;
596
+ }
597
+ .comments-list {
598
+ background: var(--bg);
599
+ border-radius: 6px;
600
+ padding: 12px;
601
+ max-height: 200px;
602
+ overflow-y: auto;
603
+ }
604
+ .no-comments { color: var(--muted); font-size: 13px; text-align: center; }
605
+ .comment {
606
+ padding: 10px 0;
607
+ border-bottom: 1px solid var(--border);
608
+ }
609
+ .comment:last-child { border-bottom: none; }
610
+ .comment-meta {
611
+ display: flex;
612
+ justify-content: space-between;
613
+ margin-bottom: 6px;
614
+ }
615
+ .comment-column {
616
+ font-size: 11px;
617
+ color: var(--blue);
618
+ padding: 2px 6px;
619
+ background: rgba(77,171,247,0.1);
620
+ border-radius: 4px;
621
+ }
622
+ .comment-date { font-size: 11px; color: var(--muted); }
623
+ .comment-text { font-size: 13px; line-height: 1.5; }
624
+ .comment-text h2, .comment-text h3, .comment-text h4 { margin: 8px 0 4px; }
625
+ .comment-text code {
626
+ background: #21262d;
627
+ padding: 2px 6px;
628
+ border-radius: 4px;
629
+ font-size: 12px;
630
+ }
631
+ .comment-text ul { margin-left: 20px; }
632
+
633
+ .add-comment { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
634
+ .add-comment textarea { min-height: 60px; }
635
+ .btn-comment {
636
+ align-self: flex-end;
637
+ background: var(--accent);
638
+ color: #fff;
639
+ border: none;
640
+ padding: 8px 16px;
641
+ border-radius: 6px;
642
+ cursor: pointer;
643
+ font-size: 13px;
644
+ }
645
+
646
+ .modal-actions {
647
+ display: flex;
648
+ gap: 12px;
649
+ justify-content: flex-end;
650
+ margin-top: 24px;
651
+ padding-top: 24px;
652
+ border-top: 1px solid var(--border);
653
+ }
654
+
655
+ /* Action Modal */
656
+ .action-modal textarea {
657
+ width: 100%;
658
+ min-height: 300px;
659
+ font-family: monospace;
660
+ font-size: 13px;
661
+ background: var(--bg);
662
+ resize: vertical;
663
+ }
664
+ .action-toolbar {
665
+ display: flex;
666
+ gap: 8px;
667
+ margin-bottom: 12px;
668
+ }
669
+ .action-meta {
670
+ font-size: 12px;
671
+ color: var(--muted);
672
+ margin-bottom: 8px;
673
+ }
674
+ .action-empty {
675
+ background: var(--bg);
676
+ padding: 24px;
677
+ text-align: center;
678
+ border-radius: 6px;
679
+ color: var(--muted);
680
+ }
681
+ .pill {
682
+ background: var(--accent);
683
+ color: #fff;
684
+ padding: 4px 8px;
685
+ border-radius: 4px;
686
+ font-size: 11px;
687
+ font-weight: 600;
688
+ margin-right: 8px;
689
+ }
690
+
691
+ /* Context Menu */
692
+ .context-menu {
693
+ position: fixed;
694
+ background: var(--bg-tertiary);
695
+ border: 1px solid var(--border);
696
+ border-radius: 8px;
697
+ padding: 4px 0;
698
+ min-width: 140px;
699
+ z-index: 1001;
700
+ display: none;
701
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
702
+ }
703
+ .context-menu.active { display: block; }
704
+ .context-menu-item {
705
+ padding: 8px 16px;
706
+ font-size: 0.85rem;
707
+ cursor: pointer;
708
+ transition: background 0.2s;
709
+ }
710
+ .context-menu-item:hover { background: var(--bg-secondary); }
711
+ .context-menu-item.danger { color: var(--red); }
712
+
713
+ /* Notifications */
714
+ .notifications {
715
+ position: fixed;
716
+ top: 80px;
717
+ right: 20px;
718
+ z-index: 1001;
719
+ display: flex;
720
+ flex-direction: column;
721
+ gap: 10px;
722
+ max-width: 350px;
723
+ }
724
+ .notification {
725
+ background: var(--bg-tertiary);
726
+ border: 1px solid var(--border);
727
+ border-left: 4px solid var(--accent);
728
+ border-radius: 8px;
729
+ padding: 12px 16px;
730
+ display: flex;
731
+ align-items: flex-start;
732
+ gap: 12px;
733
+ animation: slideInRight 0.4s ease;
734
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
735
+ }
736
+ .notification.success { border-left-color: var(--green); }
737
+ .notification.error { border-left-color: var(--red); }
738
+ .notification.warning { border-left-color: var(--yellow); }
739
+ .notification.claude { border-left-color: #a855f7; }
740
+ .notification-icon { font-size: 1.2rem; flex-shrink: 0; }
741
+ .notification-content { flex: 1; }
742
+ .notification-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 2px; }
743
+ .notification-message { font-size: 0.8rem; color: var(--text-secondary); }
744
+ .notification-close {
745
+ background: none;
746
+ border: none;
747
+ color: var(--text-secondary);
748
+ cursor: pointer;
749
+ font-size: 16px;
750
+ }
751
+
752
+ @keyframes slideInRight {
753
+ from { transform: translateX(100%); opacity: 0; }
754
+ to { transform: translateX(0); opacity: 1; }
755
+ }
756
+
757
+ /* Claude Log Section (in modal) */
758
+ .claude-log-section {
759
+ border-top: 1px solid var(--border);
760
+ padding-top: 16px;
761
+ margin-top: 8px;
762
+ }
763
+ .claude-log-header {
764
+ display: flex;
765
+ align-items: center;
766
+ gap: 8px;
767
+ margin-bottom: 12px;
768
+ }
769
+ .claude-log-header h3 { font-size: 14px; font-weight: 600; }
770
+ .claude-log-status {
771
+ font-size: 12px;
772
+ padding: 2px 8px;
773
+ border-radius: 10px;
774
+ background: #21262d;
775
+ }
776
+ .claude-log-status.processing { color: var(--yellow); animation: pulse 1s infinite; }
777
+ .claude-log-status.success { color: var(--green); }
778
+ .claude-log-status.error { color: var(--red); }
779
+ .claude-log {
780
+ background: #0d1117;
781
+ border: 1px solid var(--border);
782
+ border-radius: 6px;
783
+ padding: 16px;
784
+ max-height: 500px;
785
+ overflow-y: auto;
786
+ font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
787
+ font-size: 13px;
788
+ line-height: 1.6;
789
+ white-space: pre-wrap;
790
+ word-break: break-word;
791
+ color: var(--text);
792
+ margin: 0;
793
+ }
794
+ @keyframes pulse {
795
+ 0%, 100% { opacity: 1; }
796
+ 50% { opacity: 0.5; }
797
+ }
798
+
799
+ /* Footer */
800
+ footer {
801
+ position: fixed;
802
+ bottom: 0;
803
+ left: 0;
804
+ right: 0;
805
+ padding: 16px 24px;
806
+ text-align: center;
807
+ font-size: 0.9rem;
808
+ color: var(--text-secondary);
809
+ background: var(--bg-secondary);
810
+ border-top: 1px solid var(--border);
811
+ display: flex;
812
+ align-items: center;
813
+ justify-content: center;
814
+ gap: 16px;
815
+ height: 50px;
816
+ }
817
+
818
+ @media (max-width: 768px) {
819
+ header { flex-direction: column; align-items: stretch; }
820
+ .filters { flex-wrap: wrap; }
821
+ .form-row { grid-template-columns: 1fr; }
822
+ }
823
+ `;
824
+ }
825
+ /**
826
+ * Get JavaScript for interactivity
827
+ */
828
+ function getScript() {
829
+ return `
830
+ // i18n Translations
831
+ const translations = {
832
+ en: {
833
+ // Filter
834
+ 'filter.allPriorities': 'All priorities',
835
+ 'filter.search': 'Search...',
836
+ // Buttons
837
+ 'btn.newTicket': '+ New ticket',
838
+ 'btn.createTicket': 'Create ticket',
839
+ 'btn.update': 'Update',
840
+ 'btn.cancel': 'Cancel',
841
+ 'btn.archive': 'Archive',
842
+ 'btn.next': 'Next',
843
+ 'btn.add': 'Add',
844
+ 'btn.edit': 'Edit',
845
+ 'btn.save': 'Save',
846
+ 'btn.reload': 'Reload',
847
+ // Modal
848
+ 'modal.newTicket': 'New ticket',
849
+ 'modal.editTicket': 'Edit',
850
+ 'modal.title': 'Title *',
851
+ 'modal.titlePlaceholder': 'E.g.: Fix the login bug',
852
+ 'modal.description': 'Description',
853
+ 'modal.descriptionPlaceholder': 'Describe the context and details...',
854
+ 'modal.priority': 'Priority',
855
+ 'modal.releaseType': 'Release type',
856
+ 'modal.labels': 'Labels',
857
+ 'modal.selectLabel': 'Select a label...',
858
+ 'modal.acceptanceCriteria': 'Acceptance criteria',
859
+ 'modal.addCriteria': 'Add criteria',
860
+ 'modal.comments': 'Comments',
861
+ 'modal.noComments': 'No comments',
862
+ 'modal.addCommentPlaceholder': 'Add a comment...',
863
+ 'modal.claudeTerminal': 'Claude Terminal',
864
+ // Priority
865
+ 'priority.p0': 'P0 - Critical',
866
+ 'priority.p1': 'P1 - High',
867
+ 'priority.p2': 'P2 - Normal',
868
+ 'priority.p3': 'P3 - Low',
869
+ // Semver
870
+ 'semver.patch': 'Patch (x.x.X) - Bug fix',
871
+ 'semver.minor': 'Minor (x.X.0) - Feature',
872
+ 'semver.major': 'Major (X.0.0) - Breaking',
873
+ 'semver.none': 'No deployment',
874
+ // Status
875
+ 'status.waiting': 'Waiting',
876
+ 'status.processing': 'Running...',
877
+ 'status.completed': 'Completed',
878
+ 'status.failed': 'Failed',
879
+ 'status.connected': 'Connected',
880
+ // Stats
881
+ 'stats.total': 'Total',
882
+ // Board
883
+ 'board.empty': 'Empty',
884
+ // Action modal
885
+ 'action.instructions': 'Instructions',
886
+ 'action.noInstructions': 'No instructions available',
887
+ 'action.noFile': 'No ACTION file. Click "Edit" to create it.',
888
+ 'action.modifiedOn': 'Modified on',
889
+ // Notifications
890
+ 'notify.titleRequired': 'Title required',
891
+ 'notify.titleMandatory': 'Title is mandatory',
892
+ 'notify.ticketUpdated': 'Ticket updated',
893
+ 'notify.ticketCreated': 'Ticket created',
894
+ 'notify.ticketAdvanced': 'Ticket advanced',
895
+ 'notify.ticketArchived': 'Ticket archived',
896
+ 'notify.ticketMoved': 'moved',
897
+ 'notify.moveTo': 'To',
898
+ 'notify.commentAdded': 'Comment added',
899
+ 'notify.actionUpdated': 'updated',
900
+ 'notify.error': 'Error',
901
+ 'notify.unableToSave': 'Unable to save',
902
+ 'notify.loadingError': 'Loading error',
903
+ 'notify.claudeStarted': 'Claude started',
904
+ 'notify.claudeFinished': 'Claude finished',
905
+ 'notify.claudeFailed': 'Claude failed',
906
+ 'notify.processingSuccess': 'Processing successful',
907
+ 'notify.checkLogs': 'Check logs',
908
+ // Confirm
909
+ 'confirm.archive': 'Archive',
910
+ // Button states
911
+ 'btn.updating': 'Updating...',
912
+ 'btn.creating': 'Creating...',
913
+ 'btn.moving': 'Moving...',
914
+ 'btn.archiving': 'Archiving...',
915
+ 'btn.sending': 'Sending...',
916
+ 'btn.saving': 'Saving...',
917
+ },
918
+ fr: {
919
+ // Filter
920
+ 'filter.allPriorities': 'Toutes priorités',
921
+ 'filter.search': 'Rechercher...',
922
+ // Buttons
923
+ 'btn.newTicket': '+ Nouveau ticket',
924
+ 'btn.createTicket': 'Créer le ticket',
925
+ 'btn.update': 'Mettre à jour',
926
+ 'btn.cancel': 'Annuler',
927
+ 'btn.archive': 'Archiver',
928
+ 'btn.next': 'Suivant',
929
+ 'btn.add': 'Ajouter',
930
+ 'btn.edit': 'Modifier',
931
+ 'btn.save': 'Enregistrer',
932
+ 'btn.reload': 'Recharger',
933
+ // Modal
934
+ 'modal.newTicket': 'Nouveau ticket',
935
+ 'modal.editTicket': 'Modifier',
936
+ 'modal.title': 'Titre *',
937
+ 'modal.titlePlaceholder': 'Ex: Corriger le bug de connexion',
938
+ 'modal.description': 'Description',
939
+ 'modal.descriptionPlaceholder': 'Décrivez le contexte et les détails...',
940
+ 'modal.priority': 'Priorité',
941
+ 'modal.releaseType': 'Type de release',
942
+ 'modal.labels': 'Labels',
943
+ 'modal.selectLabel': 'Sélectionner un label...',
944
+ 'modal.acceptanceCriteria': 'Critères d\\'acceptation',
945
+ 'modal.addCriteria': 'Ajouter un critère',
946
+ 'modal.comments': 'Commentaires',
947
+ 'modal.noComments': 'Aucun commentaire',
948
+ 'modal.addCommentPlaceholder': 'Ajouter un commentaire...',
949
+ 'modal.claudeTerminal': 'Terminal Claude',
950
+ // Priority
951
+ 'priority.p0': 'P0 - Critique',
952
+ 'priority.p1': 'P1 - Haute',
953
+ 'priority.p2': 'P2 - Normale',
954
+ 'priority.p3': 'P3 - Basse',
955
+ // Semver
956
+ 'semver.patch': 'Patch (x.x.X) - Bug fix',
957
+ 'semver.minor': 'Minor (x.X.0) - Fonctionnalité',
958
+ 'semver.major': 'Major (X.0.0) - Breaking',
959
+ 'semver.none': 'Aucun déploiement',
960
+ // Status
961
+ 'status.waiting': 'En attente',
962
+ 'status.processing': 'En cours...',
963
+ 'status.completed': 'Terminé',
964
+ 'status.failed': 'Échoué',
965
+ 'status.connected': 'Connecté',
966
+ // Stats
967
+ 'stats.total': 'Total',
968
+ // Board
969
+ 'board.empty': 'Vide',
970
+ // Action modal
971
+ 'action.instructions': 'Instructions',
972
+ 'action.noInstructions': 'Aucune instruction disponible',
973
+ 'action.noFile': 'Aucun fichier ACTION. Cliquez sur "Modifier" pour le créer.',
974
+ 'action.modifiedOn': 'Modifié le',
975
+ // Notifications
976
+ 'notify.titleRequired': 'Titre requis',
977
+ 'notify.titleMandatory': 'Le titre est obligatoire',
978
+ 'notify.ticketUpdated': 'Ticket mis à jour',
979
+ 'notify.ticketCreated': 'Ticket créé',
980
+ 'notify.ticketAdvanced': 'Ticket avancé',
981
+ 'notify.ticketArchived': 'Ticket archivé',
982
+ 'notify.ticketMoved': 'déplacé',
983
+ 'notify.moveTo': 'Vers',
984
+ 'notify.commentAdded': 'Commentaire ajouté',
985
+ 'notify.actionUpdated': 'mis à jour',
986
+ 'notify.error': 'Erreur',
987
+ 'notify.unableToSave': 'Impossible de sauvegarder',
988
+ 'notify.loadingError': 'Erreur de chargement',
989
+ 'notify.claudeStarted': 'Claude démarré',
990
+ 'notify.claudeFinished': 'Claude terminé',
991
+ 'notify.claudeFailed': 'Claude échoué',
992
+ 'notify.processingSuccess': 'Traitement réussi',
993
+ 'notify.checkLogs': 'Voir les logs',
994
+ // Confirm
995
+ 'confirm.archive': 'Archiver',
996
+ // Button states
997
+ 'btn.updating': 'Mise à jour...',
998
+ 'btn.creating': 'Création...',
999
+ 'btn.moving': 'Déplacement...',
1000
+ 'btn.archiving': 'Archivage...',
1001
+ 'btn.sending': 'Envoi...',
1002
+ 'btn.saving': 'Sauvegarde...',
1003
+ }
1004
+ };
1005
+
1006
+ let currentLang = localStorage.getItem('autocode-ui-lang') || 'en';
1007
+
1008
+ function t(key) {
1009
+ return translations[currentLang][key] || translations['en'][key] || key;
1010
+ }
1011
+
1012
+ function switchLanguage(lang) {
1013
+ currentLang = lang;
1014
+ currentActionLang = lang; // Sync ACTION file language
1015
+ localStorage.setItem('autocode-ui-lang', lang);
1016
+ localStorage.setItem('autocode-lang', lang);
1017
+ document.documentElement.lang = lang;
1018
+ const selector = document.getElementById('ui-lang');
1019
+ if (selector) selector.value = lang;
1020
+
1021
+ // Update all elements with data-i18n
1022
+ document.querySelectorAll('[data-i18n]').forEach(el => {
1023
+ const key = el.getAttribute('data-i18n');
1024
+ if (translations[lang] && translations[lang][key]) {
1025
+ el.textContent = translations[lang][key];
1026
+ }
1027
+ });
1028
+
1029
+ // Update placeholders
1030
+ document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
1031
+ const key = el.getAttribute('data-i18n-placeholder');
1032
+ if (translations[lang] && translations[lang][key]) {
1033
+ el.placeholder = translations[lang][key];
1034
+ }
1035
+ });
1036
+
1037
+ // Update select options
1038
+ document.querySelectorAll('select option[data-i18n]').forEach(el => {
1039
+ const key = el.getAttribute('data-i18n');
1040
+ if (translations[lang] && translations[lang][key]) {
1041
+ el.textContent = translations[lang][key];
1042
+ }
1043
+ });
1044
+
1045
+ // Re-render board to update dynamic content
1046
+ render();
1047
+ }
1048
+
1049
+ // State
1050
+ let filterPriority = '';
1051
+ let filterSearch = '';
1052
+ let selectedLabels = [];
1053
+ let criteriaCount = 0;
1054
+ let editingKey = null;
1055
+ let currentComments = [];
1056
+ let contextMenuTicket = null;
1057
+ let draggedTicket = null;
1058
+ let draggedFromColumn = null;
1059
+ let currentActionSlug = null;
1060
+ let currentActionLang = localStorage.getItem('autocode-lang') || 'en';
1061
+ let originalActionContent = '';
1062
+ let claudeProcessingTickets = new Set(); // Tickets currently being processed by Claude
1063
+
1064
+ // Initialize language selector on page load
1065
+ (function initLangSelector() {
1066
+ const langSelect = document.getElementById('ui-lang');
1067
+ if (langSelect) langSelect.value = currentLang;
1068
+ switchLanguage(currentLang);
1069
+ })();
1070
+
1071
+ // ========================================
1072
+ // NOTIFICATIONS
1073
+ // ========================================
1074
+ function showNotification(type, title, message, duration = 5000) {
1075
+ const container = document.getElementById('notifications');
1076
+ const id = 'notif-' + Date.now();
1077
+ const icons = { info: '\\u{1F535}', success: '\\u2705', warning: '\\u26A0\\uFE0F', error: '\\u274C', claude: '\\u{1F916}' };
1078
+
1079
+ const notif = document.createElement('div');
1080
+ notif.className = 'notification ' + (type === 'claude' ? 'info' : type);
1081
+ notif.id = id;
1082
+ notif.innerHTML = '<span class="notification-icon">' + (icons[type] || icons.info) + '</span>' +
1083
+ '<div class="notification-content"><div class="notification-title">' + title + '</div>' +
1084
+ (message ? '<div class="notification-message">' + message + '</div>' : '') + '</div>' +
1085
+ '<button class="notification-close" onclick="closeNotification(\\'' + id + '\\')">&times;</button>';
1086
+
1087
+ container.appendChild(notif);
1088
+ setTimeout(() => closeNotification(id), duration);
1089
+ }
1090
+
1091
+ function closeNotification(id) {
1092
+ const notif = document.getElementById(id);
1093
+ if (notif) notif.remove();
1094
+ }
1095
+
1096
+ // ========================================
1097
+ // RENDER
1098
+ // ========================================
1099
+ function render() {
1100
+ const board = document.getElementById('board');
1101
+ board.innerHTML = '';
1102
+ let stats = { total: 0, byPriority: { P0: 0, P1: 0, P2: 0, P3: 0 } };
1103
+
1104
+ COLUMNS.forEach(col => {
1105
+ let tickets = TICKETS.filter(t => t.column_slug === col.slug);
1106
+ if (filterPriority) tickets = tickets.filter(t => t.priority === filterPriority);
1107
+ if (filterSearch) {
1108
+ const s = filterSearch.toLowerCase();
1109
+ tickets = tickets.filter(t => t.title.toLowerCase().includes(s) || t.key.toLowerCase().includes(s));
1110
+ }
1111
+ tickets.sort((a, b) => {
1112
+ const p = { P0: 0, P1: 1, P2: 2, P3: 3 };
1113
+ return p[a.priority] - p[b.priority];
1114
+ });
1115
+
1116
+ stats.total += tickets.length;
1117
+ tickets.forEach(t => stats.byPriority[t.priority] = (stats.byPriority[t.priority] || 0) + 1);
1118
+
1119
+ const div = document.createElement('div');
1120
+ div.className = 'column';
1121
+ div.ondragover = onDragOver;
1122
+ div.ondragenter = onDragEnter;
1123
+ div.ondragleave = onDragLeave;
1124
+ div.ondrop = (e) => onDrop(e, col.slug, col.name);
1125
+
1126
+ div.innerHTML = '<div class="column-header">' +
1127
+ '<span class="column-title">' + col.name + '</span>' +
1128
+ '<div class="column-header-actions">' +
1129
+ '<button class="btn-action" title="ACTION.md" onclick="openActionModal(\\'' + col.slug + '\\')">\\u{1F4D8}</button>' +
1130
+ '<span class="column-count">' + tickets.length + '</span>' +
1131
+ '</div></div>' +
1132
+ '<div class="column-body">' +
1133
+ (tickets.length ? tickets.map(tk => {
1134
+ const isProcessing = claudeProcessingTickets.has(tk.key);
1135
+ return '<div class="ticket' + (isProcessing ? ' claude-processing' : '') + '" draggable="true" ' +
1136
+ 'onclick="onTicketClick(\\'' + tk.key + '\\')" ' +
1137
+ 'oncontextmenu="showContextMenu(event,\\'' + tk.key + '\\')" ' +
1138
+ 'ondragstart="onDragStart(event,\\'' + tk.key + '\\',\\'' + col.slug + '\\')" ' +
1139
+ 'ondragend="onDragEnd(event)">' +
1140
+ '<div class="ticket-key">' + tk.key + '</div>' +
1141
+ '<div class="ticket-title">' + escapeHtml(tk.title) + '</div>' +
1142
+ '<div class="ticket-meta"><span class="priority ' + tk.priority + '">' + tk.priority + '</span></div>' +
1143
+ (isProcessing ? '<div class="ticket-claude-indicator"><span class="claude-dot"></span>\\u{1F916} ' + t('status.processing') + '</div>' : '') +
1144
+ '</div>';
1145
+ }).join('') : '<div class="empty">' + t('board.empty') + '</div>') +
1146
+ '</div>';
1147
+
1148
+ board.appendChild(div);
1149
+ });
1150
+
1151
+ document.getElementById('stats').innerHTML =
1152
+ '<span class="stat connected">\\u{1F7E2} ' + t('status.connected') + '</span>' +
1153
+ '<span class="stat">' + t('stats.total') + ': ' + stats.total + '</span>' +
1154
+ '<span class="stat P0">P0: ' + (stats.byPriority.P0 || 0) + '</span>' +
1155
+ '<span class="stat P1">P1: ' + (stats.byPriority.P1 || 0) + '</span>' +
1156
+ '<span class="stat P2">P2: ' + (stats.byPriority.P2 || 0) + '</span>';
1157
+ }
1158
+
1159
+ function escapeHtml(text) {
1160
+ const div = document.createElement('div');
1161
+ div.textContent = text;
1162
+ return div.innerHTML;
1163
+ }
1164
+
1165
+ // ========================================
1166
+ // FILTERS
1167
+ // ========================================
1168
+ document.getElementById('filter-priority').onchange = e => { filterPriority = e.target.value; render(); };
1169
+ document.getElementById('filter-search').oninput = e => { filterSearch = e.target.value.toLowerCase(); render(); };
1170
+
1171
+ // ========================================
1172
+ // MODAL TICKET
1173
+ // ========================================
1174
+ function openModal(key = null) {
1175
+ editingKey = key;
1176
+ document.getElementById('modal-overlay').classList.add('active');
1177
+ document.body.style.overflow = 'hidden';
1178
+
1179
+ const modalTitle = document.getElementById('modal-title');
1180
+ const saveBtn = document.getElementById('btn-save');
1181
+ const nextBtn = document.getElementById('btn-next');
1182
+ const archiveBtn = document.getElementById('btn-archive');
1183
+ const commentsSection = document.getElementById('comments-section');
1184
+
1185
+ if (key) {
1186
+ modalTitle.textContent = t('modal.editTicket') + ' ' + key;
1187
+ saveBtn.textContent = t('btn.update');
1188
+ nextBtn.style.display = 'inline-block';
1189
+ archiveBtn.style.display = 'inline-block';
1190
+ commentsSection.style.display = 'block';
1191
+ loadTicketForEdit(key);
1192
+ } else {
1193
+ modalTitle.textContent = t('modal.newTicket');
1194
+ saveBtn.textContent = t('btn.createTicket');
1195
+ nextBtn.style.display = 'none';
1196
+ archiveBtn.style.display = 'none';
1197
+ commentsSection.style.display = 'none';
1198
+ resetForm();
1199
+ resetComments();
1200
+ }
1201
+ }
1202
+
1203
+ function closeModal() {
1204
+ document.getElementById('modal-overlay').classList.remove('active');
1205
+ document.body.style.overflow = '';
1206
+ editingKey = null;
1207
+ resetForm();
1208
+ resetClaudeLog();
1209
+ }
1210
+
1211
+ function resetForm() {
1212
+ document.getElementById('ticket-title').value = '';
1213
+ document.getElementById('ticket-description').value = '';
1214
+ document.getElementById('ticket-priority').value = 'P2';
1215
+ document.getElementById('ticket-semver').value = 'patch';
1216
+ document.getElementById('ticket-labels').value = '';
1217
+ selectedLabels = [];
1218
+ criteriaCount = 0;
1219
+ document.getElementById('selected-labels').innerHTML = '';
1220
+ document.getElementById('criteria-list').innerHTML = '';
1221
+ }
1222
+
1223
+ async function loadTicketForEdit(key) {
1224
+ try {
1225
+ const res = await fetch('/api/tickets/' + key);
1226
+ if (!res.ok) throw new Error('Ticket not found');
1227
+ const json = await res.json();
1228
+ if (!json.success || !json.data) throw new Error('Ticket not found');
1229
+ const ticket = json.data;
1230
+
1231
+ document.getElementById('ticket-title').value = ticket.title || '';
1232
+ document.getElementById('ticket-description').value = ticket.description || '';
1233
+ document.getElementById('ticket-priority').value = ticket.priority || 'P2';
1234
+ document.getElementById('ticket-semver').value = ticket.semver || 'patch';
1235
+
1236
+ selectedLabels = Array.isArray(ticket.labels) ? [...ticket.labels] : [];
1237
+ renderLabels();
1238
+
1239
+ criteriaCount = 0;
1240
+ document.getElementById('criteria-list').innerHTML = '';
1241
+ if (Array.isArray(ticket.acceptance_criteria)) {
1242
+ ticket.acceptance_criteria.forEach(c => addCriteria(c));
1243
+ }
1244
+
1245
+ renderComments(ticket.comments || []);
1246
+
1247
+ // Fetch Claude log if exists
1248
+ fetchLog(key);
1249
+ } catch (e) {
1250
+ console.error('Error:', e);
1251
+ showNotification('error', 'Error', e.message);
1252
+ closeModal();
1253
+ }
1254
+ }
1255
+
1256
+ async function saveTicket() {
1257
+ const title = document.getElementById('ticket-title').value.trim();
1258
+ const description = document.getElementById('ticket-description').value.trim();
1259
+ const priority = document.getElementById('ticket-priority').value;
1260
+ const semver = document.getElementById('ticket-semver').value;
1261
+ const criteria = getCriteria();
1262
+
1263
+ if (!title) {
1264
+ showNotification('warning', t('notify.titleRequired'), t('notify.titleMandatory'));
1265
+ return;
1266
+ }
1267
+
1268
+ const btn = document.getElementById('btn-save');
1269
+ btn.disabled = true;
1270
+
1271
+ try {
1272
+ if (editingKey) {
1273
+ btn.textContent = t('btn.updating');
1274
+ await fetch('/api/tickets/' + editingKey, {
1275
+ method: 'POST',
1276
+ headers: { 'Content-Type': 'application/json' },
1277
+ body: JSON.stringify({ title, description, priority, semver, labels: selectedLabels, acceptance_criteria: criteria })
1278
+ });
1279
+ showNotification('success', t('notify.ticketUpdated'), editingKey);
1280
+ } else {
1281
+ btn.textContent = t('btn.creating');
1282
+ const res = await fetch('/api/tickets', {
1283
+ method: 'POST',
1284
+ headers: { 'Content-Type': 'application/json' },
1285
+ body: JSON.stringify({ title, priority, labels: selectedLabels, description, semver, acceptance_criteria: criteria })
1286
+ });
1287
+ const data = await res.json();
1288
+ showNotification('success', t('notify.ticketCreated'), data.key || '');
1289
+ }
1290
+ closeModal();
1291
+ loadTicketsFromAPI();
1292
+ } catch (e) {
1293
+ showNotification('error', t('notify.error'), e.message);
1294
+ btn.disabled = false;
1295
+ btn.textContent = editingKey ? t('btn.update') : t('btn.createTicket');
1296
+ }
1297
+ }
1298
+
1299
+ async function advanceTicket() {
1300
+ if (!editingKey) return;
1301
+ const btn = document.getElementById('btn-next');
1302
+ btn.disabled = true;
1303
+ btn.textContent = t('btn.moving');
1304
+ try {
1305
+ await fetch('/api/tickets/' + editingKey + '/next', { method: 'POST' });
1306
+ showNotification('info', t('notify.ticketAdvanced'), editingKey);
1307
+ closeModal();
1308
+ loadTicketsFromAPI();
1309
+ } catch (e) {
1310
+ showNotification('error', t('notify.error'), e.message);
1311
+ btn.disabled = false;
1312
+ btn.textContent = t('btn.next');
1313
+ }
1314
+ }
1315
+
1316
+ async function archiveTicket() {
1317
+ if (!editingKey) return;
1318
+ if (!confirm(t('confirm.archive') + ' ' + editingKey + '?')) return;
1319
+
1320
+ const btn = document.getElementById('btn-archive');
1321
+ btn.disabled = true;
1322
+ btn.textContent = t('btn.archiving');
1323
+ try {
1324
+ const lastColumn = COLUMNS[COLUMNS.length - 1];
1325
+ await fetch('/api/tickets/' + editingKey + '/move', {
1326
+ method: 'POST',
1327
+ headers: { 'Content-Type': 'application/json' },
1328
+ body: JSON.stringify({ column: lastColumn.name, force: true })
1329
+ });
1330
+ showNotification('info', t('notify.ticketArchived'), editingKey);
1331
+ closeModal();
1332
+ loadTicketsFromAPI();
1333
+ } catch (e) {
1334
+ showNotification('error', t('notify.error'), e.message);
1335
+ btn.disabled = false;
1336
+ btn.textContent = t('btn.archive');
1337
+ }
1338
+ }
1339
+
1340
+ // Labels
1341
+ function addLabel(select) {
1342
+ const value = select.value;
1343
+ if (value && !selectedLabels.includes(value)) {
1344
+ selectedLabels.push(value);
1345
+ renderLabels();
1346
+ }
1347
+ select.value = '';
1348
+ }
1349
+
1350
+ function removeLabel(label) {
1351
+ selectedLabels = selectedLabels.filter(l => l !== label);
1352
+ renderLabels();
1353
+ }
1354
+
1355
+ function renderLabels() {
1356
+ const container = document.getElementById('selected-labels');
1357
+ container.innerHTML = selectedLabels.map(label =>
1358
+ '<span class="label-tag">' + label + '<span class="remove-label" onclick="removeLabel(\\'' + label + '\\')">&times;</span></span>'
1359
+ ).join('');
1360
+ }
1361
+
1362
+ // Criteria
1363
+ function addCriteria(value = '') {
1364
+ criteriaCount++;
1365
+ const id = criteriaCount;
1366
+ const container = document.getElementById('criteria-list');
1367
+ const div = document.createElement('div');
1368
+ div.className = 'criteria-item';
1369
+ div.id = 'criteria-' + id;
1370
+ div.innerHTML = '<input type="text" placeholder="Ex: L\\'utilisateur peut..." value="' + escapeHtml(value) + '">' +
1371
+ '<button type="button" class="btn-remove" onclick="removeCriteria(' + id + ')">&times;</button>';
1372
+ container.appendChild(div);
1373
+ }
1374
+
1375
+ function removeCriteria(id) {
1376
+ const el = document.getElementById('criteria-' + id);
1377
+ if (el) el.remove();
1378
+ }
1379
+
1380
+ function getCriteria() {
1381
+ const items = document.querySelectorAll('.criteria-item input');
1382
+ return Array.from(items).map(i => i.value.trim()).filter(Boolean);
1383
+ }
1384
+
1385
+ // ========================================
1386
+ // COMMENTS
1387
+ // ========================================
1388
+ function resetComments() {
1389
+ currentComments = [];
1390
+ document.getElementById('comments-list').innerHTML = '<div class="no-comments">' + t('modal.noComments') + '</div>';
1391
+ document.getElementById('comments-count').textContent = '0';
1392
+ document.getElementById('new-comment').value = '';
1393
+ }
1394
+
1395
+ function renderComments(comments) {
1396
+ currentComments = comments || [];
1397
+ const list = document.getElementById('comments-list');
1398
+ const count = document.getElementById('comments-count');
1399
+ count.textContent = currentComments.length;
1400
+
1401
+ if (currentComments.length === 0) {
1402
+ list.innerHTML = '<div class="no-comments">' + t('modal.noComments') + '</div>';
1403
+ return;
1404
+ }
1405
+
1406
+ const sorted = [...currentComments].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
1407
+ list.innerHTML = sorted.map(comment => {
1408
+ const date = new Date(comment.created_at);
1409
+ const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
1410
+ return '<div class="comment">' +
1411
+ '<div class="comment-meta"><span class="comment-column">' + (comment.column || 'N/A') + '</span>' +
1412
+ '<span class="comment-date">' + dateStr + '</span></div>' +
1413
+ '<div class="comment-text">' + renderMarkdown(comment.text) + '</div></div>';
1414
+ }).join('');
1415
+ }
1416
+
1417
+ function renderMarkdown(text) {
1418
+ if (!text) return '';
1419
+ let html = escapeHtml(text);
1420
+ html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
1421
+ html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
1422
+ html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>');
1423
+ html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
1424
+ html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
1425
+ html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
1426
+ html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
1427
+ html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
1428
+ html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
1429
+ html = html.replace(/\\n/g, '<br>');
1430
+ return html;
1431
+ }
1432
+
1433
+ async function addComment() {
1434
+ if (!editingKey) return;
1435
+ const textarea = document.getElementById('new-comment');
1436
+ const text = textarea.value.trim();
1437
+ if (!text) return;
1438
+
1439
+ const btn = document.querySelector('.btn-comment');
1440
+ btn.disabled = true;
1441
+ btn.textContent = t('btn.sending');
1442
+
1443
+ try {
1444
+ const res = await fetch('/api/tickets/' + editingKey + '/comments', {
1445
+ method: 'POST',
1446
+ headers: { 'Content-Type': 'application/json' },
1447
+ body: JSON.stringify({ text })
1448
+ });
1449
+ const result = await res.json();
1450
+ textarea.value = '';
1451
+ if (result.comments) renderComments(result.comments);
1452
+ showNotification('success', t('notify.commentAdded'), '');
1453
+ } catch (e) {
1454
+ showNotification('error', t('notify.error'), e.message);
1455
+ } finally {
1456
+ btn.disabled = false;
1457
+ btn.textContent = t('btn.add');
1458
+ }
1459
+ }
1460
+
1461
+ // ========================================
1462
+ // DRAG & DROP
1463
+ // ========================================
1464
+ function onDragStart(e, key, columnSlug) {
1465
+ draggedTicket = key;
1466
+ draggedFromColumn = columnSlug;
1467
+ e.target.classList.add('dragging');
1468
+ e.dataTransfer.effectAllowed = 'move';
1469
+ e.dataTransfer.setData('text/plain', key);
1470
+ }
1471
+
1472
+ function onDragEnd(e) {
1473
+ e.target.classList.remove('dragging');
1474
+ document.querySelectorAll('.column').forEach(c => c.classList.remove('drag-over'));
1475
+ }
1476
+
1477
+ function onDragOver(e) {
1478
+ e.preventDefault();
1479
+ e.dataTransfer.dropEffect = 'move';
1480
+ }
1481
+
1482
+ function onDragEnter(e) {
1483
+ e.preventDefault();
1484
+ const column = e.target.closest('.column');
1485
+ if (column) column.classList.add('drag-over');
1486
+ }
1487
+
1488
+ function onDragLeave(e) {
1489
+ const column = e.target.closest('.column');
1490
+ if (column && !column.contains(e.relatedTarget)) {
1491
+ column.classList.remove('drag-over');
1492
+ }
1493
+ }
1494
+
1495
+ async function onDrop(e, targetColumnSlug, targetColumnName) {
1496
+ e.preventDefault();
1497
+ document.querySelectorAll('.column').forEach(c => c.classList.remove('drag-over'));
1498
+
1499
+ if (!draggedTicket) return;
1500
+ if (draggedFromColumn === targetColumnSlug) {
1501
+ draggedTicket = null;
1502
+ draggedFromColumn = null;
1503
+ return;
1504
+ }
1505
+
1506
+ const key = draggedTicket;
1507
+ draggedTicket = null;
1508
+ draggedFromColumn = null;
1509
+
1510
+ try {
1511
+ await fetch('/api/tickets/' + key + '/move', {
1512
+ method: 'POST',
1513
+ headers: { 'Content-Type': 'application/json' },
1514
+ body: JSON.stringify({ column: targetColumnName, force: true })
1515
+ });
1516
+ showNotification('info', key + ' ' + t('notify.ticketMoved'), t('notify.moveTo') + ' "' + targetColumnName + '"');
1517
+ loadTicketsFromAPI();
1518
+ } catch (err) {
1519
+ showNotification('error', t('notify.error'), err.message);
1520
+ }
1521
+ }
1522
+
1523
+ // ========================================
1524
+ // CONTEXT MENU
1525
+ // ========================================
1526
+ function showContextMenu(e, key) {
1527
+ e.preventDefault();
1528
+ contextMenuTicket = key;
1529
+ const menu = document.getElementById('context-menu');
1530
+ menu.style.left = e.clientX + 'px';
1531
+ menu.style.top = e.clientY + 'px';
1532
+ menu.classList.add('active');
1533
+ }
1534
+
1535
+ function hideContextMenu() {
1536
+ document.getElementById('context-menu').classList.remove('active');
1537
+ contextMenuTicket = null;
1538
+ }
1539
+
1540
+ function editFromContext() {
1541
+ if (contextMenuTicket) openModal(contextMenuTicket);
1542
+ hideContextMenu();
1543
+ }
1544
+
1545
+ async function archiveFromContext() {
1546
+ if (!contextMenuTicket) return;
1547
+ const key = contextMenuTicket;
1548
+ hideContextMenu();
1549
+
1550
+ if (confirm(t('confirm.archive') + ' ' + key + '?')) {
1551
+ try {
1552
+ const lastColumn = COLUMNS[COLUMNS.length - 1];
1553
+ await fetch('/api/tickets/' + key + '/move', {
1554
+ method: 'POST',
1555
+ headers: { 'Content-Type': 'application/json' },
1556
+ body: JSON.stringify({ column: lastColumn.name, force: true })
1557
+ });
1558
+ showNotification('info', t('notify.ticketArchived'), key);
1559
+ loadTicketsFromAPI();
1560
+ } catch (err) {
1561
+ showNotification('error', t('notify.error'), err.message);
1562
+ }
1563
+ }
1564
+ }
1565
+
1566
+ document.addEventListener('click', hideContextMenu);
1567
+
1568
+ // ========================================
1569
+ // ACTION.md MODAL
1570
+ // ========================================
1571
+ function openActionModal(slug) {
1572
+ currentActionSlug = slug;
1573
+ const col = COLUMNS.find(c => c.slug === slug);
1574
+ document.getElementById('action-modal-title').textContent = col?.name || slug;
1575
+ document.getElementById('action-modal').classList.add('active');
1576
+ document.body.style.overflow = 'hidden';
1577
+ reloadActionContent();
1578
+ }
1579
+
1580
+ function closeActionModal() {
1581
+ document.getElementById('action-modal').classList.remove('active');
1582
+ document.body.style.overflow = '';
1583
+ currentActionSlug = null;
1584
+ originalActionContent = '';
1585
+ setActionEditMode(false);
1586
+ document.getElementById('action-content').value = '';
1587
+ document.getElementById('action-empty').style.display = 'none';
1588
+ document.getElementById('action-meta').textContent = '';
1589
+ }
1590
+
1591
+ function setActionEditMode(isEdit) {
1592
+ const textarea = document.getElementById('action-content');
1593
+ const saveBtn = document.getElementById('action-save-btn');
1594
+ const editBtn = document.getElementById('action-edit-btn');
1595
+ textarea.readOnly = !isEdit;
1596
+ saveBtn.disabled = !isEdit;
1597
+ editBtn.style.display = isEdit ? 'none' : 'inline-block';
1598
+ }
1599
+
1600
+ async function reloadActionContent() {
1601
+ if (!currentActionSlug) return;
1602
+ setActionEditMode(false);
1603
+ const textarea = document.getElementById('action-content');
1604
+ const empty = document.getElementById('action-empty');
1605
+ const meta = document.getElementById('action-meta');
1606
+
1607
+ empty.style.display = 'none';
1608
+ textarea.style.display = 'block';
1609
+ textarea.value = '...';
1610
+
1611
+ try {
1612
+ const res = await fetch('/api/columns/' + currentActionSlug + '/actions?lang=' + currentActionLang);
1613
+ const data = res.ok ? await res.json() : {};
1614
+
1615
+ if (!res.ok || !data.success) {
1616
+ if (res.status === 404) {
1617
+ textarea.value = '';
1618
+ textarea.style.display = 'none';
1619
+ empty.textContent = t('action.noFile');
1620
+ empty.style.display = 'block';
1621
+ meta.textContent = '';
1622
+ return;
1623
+ }
1624
+ throw new Error(data.error || t('notify.loadingError'));
1625
+ }
1626
+
1627
+ const actionData = data.data || {};
1628
+ originalActionContent = actionData.content || '';
1629
+ textarea.value = originalActionContent;
1630
+ const updated = actionData.updated_at ? new Date(actionData.updated_at).toLocaleString(currentLang === 'fr' ? 'fr-FR' : 'en-US') : '';
1631
+ meta.textContent = (actionData.path || '') + (updated ? ' - ' + t('action.modifiedOn') + ' ' + updated : '');
1632
+ } catch (e) {
1633
+ empty.textContent = t('notify.error') + ': ' + e.message;
1634
+ empty.style.display = 'block';
1635
+ textarea.style.display = 'none';
1636
+ meta.textContent = '';
1637
+ }
1638
+ }
1639
+
1640
+ function enterActionEdit() {
1641
+ if (!currentActionSlug) return;
1642
+ const textarea = document.getElementById('action-content');
1643
+ const empty = document.getElementById('action-empty');
1644
+ if (empty.style.display === 'block' && !textarea.value) {
1645
+ textarea.value = '# ' + document.getElementById('action-modal-title').textContent + '\\n\\n';
1646
+ empty.style.display = 'none';
1647
+ textarea.style.display = 'block';
1648
+ }
1649
+ setActionEditMode(true);
1650
+ textarea.focus();
1651
+ }
1652
+
1653
+ async function saveActionContent() {
1654
+ if (!currentActionSlug) return;
1655
+ const btn = document.getElementById('action-save-btn');
1656
+ const textarea = document.getElementById('action-content');
1657
+ btn.disabled = true;
1658
+ btn.textContent = t('btn.saving');
1659
+
1660
+ try {
1661
+ const res = await fetch('/api/columns/' + currentActionSlug + '/actions?lang=' + currentActionLang, {
1662
+ method: 'POST',
1663
+ headers: { 'Content-Type': 'application/json' },
1664
+ body: JSON.stringify({ content: textarea.value })
1665
+ });
1666
+ const data = await res.json();
1667
+ if (!res.ok || !data.success) throw new Error(data.error || t('notify.error'));
1668
+
1669
+ originalActionContent = textarea.value;
1670
+ setActionEditMode(false);
1671
+ showNotification('success', 'ACTION.' + currentActionLang + '.md ' + t('notify.actionUpdated'), currentActionSlug);
1672
+ } catch (e) {
1673
+ showNotification('error', t('notify.unableToSave'), e.message);
1674
+ } finally {
1675
+ btn.disabled = false;
1676
+ btn.textContent = t('btn.save');
1677
+ }
1678
+ }
1679
+
1680
+ // ========================================
1681
+ // WEBSOCKET
1682
+ // ========================================
1683
+ let ws;
1684
+
1685
+ function connectWebSocket() {
1686
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
1687
+ ws = new WebSocket(protocol + '//' + location.host + '/ws');
1688
+
1689
+ ws.onmessage = (event) => {
1690
+ try {
1691
+ const data = JSON.parse(event.data);
1692
+ switch (data.type) {
1693
+ case 'refresh':
1694
+ case 'ticket_updated':
1695
+ case 'ticket_created':
1696
+ case 'ticket_moved':
1697
+ loadTicketsFromAPI();
1698
+ break;
1699
+ case 'claude_start':
1700
+ onClaudeStart(data.ticket);
1701
+ break;
1702
+ case 'claude_stream':
1703
+ onClaudeStream(data.ticket);
1704
+ break;
1705
+ case 'claude_end':
1706
+ onClaudeEnd(data.ticket, data.success, data.duration);
1707
+ loadTicketsFromAPI();
1708
+ break;
1709
+ case 'claude_complete':
1710
+ if (data.success) {
1711
+ showNotification('claude', t('notify.claudeFinished') + ' ' + data.ticket, t('notify.processingSuccess'));
1712
+ } else {
1713
+ showNotification('error', t('notify.claudeFailed') + ' ' + data.ticket, t('notify.checkLogs'));
1714
+ }
1715
+ break;
1716
+ }
1717
+ } catch {}
1718
+ };
1719
+
1720
+ ws.onclose = () => setTimeout(connectWebSocket, 2000);
1721
+ ws.onerror = () => ws.close();
1722
+ }
1723
+
1724
+ // ========================================
1725
+ // CLAUDE LOG (in modal)
1726
+ // ========================================
1727
+ let logPollingInterval = null;
1728
+ let claudeProcessingTicket = null;
1729
+
1730
+ function startLogPolling(key) {
1731
+ stopLogPolling();
1732
+ logPollingInterval = setInterval(() => fetchLog(key), 1000);
1733
+ fetchLog(key); // Immediate first fetch
1734
+ }
1735
+
1736
+ function stopLogPolling() {
1737
+ if (logPollingInterval) {
1738
+ clearInterval(logPollingInterval);
1739
+ logPollingInterval = null;
1740
+ }
1741
+ }
1742
+
1743
+ function resetClaudeLog() {
1744
+ stopLogPolling();
1745
+ document.getElementById('claude-log-section').style.display = 'none';
1746
+ document.getElementById('claude-log').textContent = '';
1747
+ document.getElementById('claude-log-status').className = 'claude-log-status';
1748
+ document.getElementById('claude-log-status').textContent = t('status.waiting');
1749
+ }
1750
+
1751
+ async function fetchLog(key) {
1752
+ try {
1753
+ const res = await fetch('/api/tickets/' + key + '/log');
1754
+ const json = await res.json();
1755
+ if (json.success && json.data) {
1756
+ const section = document.getElementById('claude-log-section');
1757
+ const log = document.getElementById('claude-log');
1758
+ const status = document.getElementById('claude-log-status');
1759
+
1760
+ if (json.data.exists || json.data.content) {
1761
+ section.style.display = 'block';
1762
+ log.textContent = json.data.content || '';
1763
+ // Auto-scroll
1764
+ log.scrollTop = log.scrollHeight;
1765
+ }
1766
+ }
1767
+ } catch (e) {
1768
+ console.error('Log fetch error:', e);
1769
+ }
1770
+ }
1771
+
1772
+ function onClaudeStart(ticket) {
1773
+ claudeProcessingTicket = ticket;
1774
+ claudeProcessingTickets.add(ticket); // Track this ticket as being processed
1775
+ render(); // Refresh to show processing indicator
1776
+
1777
+ const status = document.getElementById('claude-log-status');
1778
+ if (status) {
1779
+ status.className = 'claude-log-status processing';
1780
+ status.textContent = '🤖 ' + t('status.processing');
1781
+ }
1782
+ // Start polling if modal is open for this ticket
1783
+ if (editingKey === ticket) {
1784
+ document.getElementById('claude-log-section').style.display = 'block';
1785
+ startLogPolling(ticket);
1786
+ }
1787
+ showNotification('claude', t('notify.claudeStarted'), ticket);
1788
+ }
1789
+
1790
+ function onClaudeStream(ticket) {
1791
+ // Refresh log if modal is open for this ticket
1792
+ if (editingKey === ticket) {
1793
+ fetchLog(ticket);
1794
+ }
1795
+ }
1796
+
1797
+ function onClaudeEnd(ticket, success, duration) {
1798
+ claudeProcessingTicket = null;
1799
+ claudeProcessingTickets.delete(ticket); // Remove from processing set
1800
+ render(); // Refresh to hide processing indicator
1801
+
1802
+ const status = document.getElementById('claude-log-status');
1803
+ if (status && editingKey === ticket) {
1804
+ if (success) {
1805
+ status.className = 'claude-log-status success';
1806
+ status.textContent = '✅ ' + t('status.completed') + ' (' + (duration / 1000).toFixed(1) + 's)';
1807
+ } else {
1808
+ status.className = 'claude-log-status error';
1809
+ status.textContent = '❌ ' + t('status.failed');
1810
+ }
1811
+ fetchLog(ticket); // Final fetch
1812
+ stopLogPolling();
1813
+ }
1814
+ if (success) {
1815
+ showNotification('success', t('notify.claudeFinished'), ticket);
1816
+ } else {
1817
+ showNotification('error', t('notify.claudeFailed'), ticket);
1818
+ }
1819
+ }
1820
+
1821
+ // ========================================
1822
+ // API
1823
+ // ========================================
1824
+ async function loadTicketsFromAPI() {
1825
+ try {
1826
+ const res = await fetch('/api/tickets');
1827
+ const json = await res.json();
1828
+ if (json.success && json.data) {
1829
+ TICKETS.length = 0;
1830
+ (json.data.tickets || []).forEach(tk => TICKETS.push(tk));
1831
+ COLUMNS.length = 0;
1832
+ (json.data.columns || []).forEach(c => COLUMNS.push(c));
1833
+ render();
1834
+ }
1835
+ } catch (e) {
1836
+ console.error(t('notify.loadingError') + ':', e);
1837
+ }
1838
+ }
1839
+
1840
+ function onTicketClick(key) {
1841
+ openModal(key);
1842
+ }
1843
+
1844
+ // ========================================
1845
+ // KEYBOARD
1846
+ // ========================================
1847
+ document.addEventListener('keydown', e => {
1848
+ if (e.key === 'Escape') {
1849
+ const actionModal = document.getElementById('action-modal');
1850
+ if (actionModal?.classList.contains('active')) {
1851
+ closeActionModal();
1852
+ return;
1853
+ }
1854
+ closeModal();
1855
+ }
1856
+ });
1857
+
1858
+ // ========================================
1859
+ // INIT
1860
+ // ========================================
1861
+ render();
1862
+ connectWebSocket();
1863
+ `;
1864
+ }
1865
+ //# sourceMappingURL=dashboard.js.map