@autocode-cli/autocode 0.0.44 → 0.1.5

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 (143) hide show
  1. package/LICENSE +5 -5
  2. package/LICENSE.fr.md +203 -0
  3. package/README.md +57 -20
  4. package/dist/cli/commands/init.js +1 -1
  5. package/dist/cli/commands/init.js.map +1 -1
  6. package/dist/cli/parser.d.ts +1 -1
  7. package/dist/cli/parser.d.ts.map +1 -1
  8. package/dist/cli/parser.js +5 -4
  9. package/dist/cli/parser.js.map +1 -1
  10. package/dist/index.js +4 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/server/api.d.ts.map +1 -1
  13. package/dist/server/api.js +47 -7
  14. package/dist/server/api.js.map +1 -1
  15. package/dist/server/dashboard/index.d.ts +10 -0
  16. package/dist/server/dashboard/index.d.ts.map +1 -0
  17. package/dist/server/dashboard/index.js +16 -0
  18. package/dist/server/dashboard/index.js.map +1 -0
  19. package/dist/server/dashboard/pages/column-edit.d.ts +8 -0
  20. package/dist/server/dashboard/pages/column-edit.d.ts.map +1 -0
  21. package/dist/server/dashboard/pages/column-edit.js +303 -0
  22. package/dist/server/dashboard/pages/column-edit.js.map +1 -0
  23. package/dist/server/dashboard/pages/column-prompt.d.ts +8 -0
  24. package/dist/server/dashboard/pages/column-prompt.d.ts.map +1 -0
  25. package/dist/server/dashboard/pages/column-prompt.js +228 -0
  26. package/dist/server/dashboard/pages/column-prompt.js.map +1 -0
  27. package/dist/server/dashboard/pages/column-terminal.d.ts +8 -0
  28. package/dist/server/dashboard/pages/column-terminal.d.ts.map +1 -0
  29. package/dist/server/dashboard/pages/column-terminal.js +529 -0
  30. package/dist/server/dashboard/pages/column-terminal.js.map +1 -0
  31. package/dist/server/dashboard/pages/index.d.ts +12 -0
  32. package/dist/server/dashboard/pages/index.d.ts.map +1 -0
  33. package/dist/server/dashboard/pages/index.js +12 -0
  34. package/dist/server/dashboard/pages/index.js.map +1 -0
  35. package/dist/server/dashboard/pages/main-dashboard.d.ts +11 -0
  36. package/dist/server/dashboard/pages/main-dashboard.d.ts.map +1 -0
  37. package/dist/server/dashboard/pages/main-dashboard.js +209 -0
  38. package/dist/server/dashboard/pages/main-dashboard.js.map +1 -0
  39. package/dist/server/dashboard/pages/shared.d.ts +8 -0
  40. package/dist/server/dashboard/pages/shared.d.ts.map +1 -0
  41. package/dist/server/dashboard/pages/shared.js +58 -0
  42. package/dist/server/dashboard/pages/shared.js.map +1 -0
  43. package/dist/server/dashboard/pages/ticket-view.d.ts +8 -0
  44. package/dist/server/dashboard/pages/ticket-view.d.ts.map +1 -0
  45. package/dist/server/dashboard/pages/ticket-view.js +1364 -0
  46. package/dist/server/dashboard/pages/ticket-view.js.map +1 -0
  47. package/dist/server/dashboard/scripts/index.d.ts +11 -0
  48. package/dist/server/dashboard/scripts/index.d.ts.map +1 -0
  49. package/dist/server/dashboard/scripts/index.js +1325 -0
  50. package/dist/server/dashboard/scripts/index.js.map +1 -0
  51. package/dist/server/dashboard/styles/base.d.ts +5 -0
  52. package/dist/server/dashboard/styles/base.d.ts.map +1 -0
  53. package/dist/server/dashboard/styles/base.js +110 -0
  54. package/dist/server/dashboard/styles/base.js.map +1 -0
  55. package/dist/server/dashboard/styles/board.d.ts +5 -0
  56. package/dist/server/dashboard/styles/board.d.ts.map +1 -0
  57. package/dist/server/dashboard/styles/board.js +168 -0
  58. package/dist/server/dashboard/styles/board.js.map +1 -0
  59. package/dist/server/dashboard/styles/comments.d.ts +5 -0
  60. package/dist/server/dashboard/styles/comments.d.ts.map +1 -0
  61. package/dist/server/dashboard/styles/comments.js +249 -0
  62. package/dist/server/dashboard/styles/comments.js.map +1 -0
  63. package/dist/server/dashboard/styles/components.d.ts +5 -0
  64. package/dist/server/dashboard/styles/components.d.ts.map +1 -0
  65. package/dist/server/dashboard/styles/components.js +190 -0
  66. package/dist/server/dashboard/styles/components.js.map +1 -0
  67. package/dist/server/dashboard/styles/footer.d.ts +5 -0
  68. package/dist/server/dashboard/styles/footer.d.ts.map +1 -0
  69. package/dist/server/dashboard/styles/footer.js +32 -0
  70. package/dist/server/dashboard/styles/footer.js.map +1 -0
  71. package/dist/server/dashboard/styles/index.d.ts +8 -0
  72. package/dist/server/dashboard/styles/index.d.ts.map +1 -0
  73. package/dist/server/dashboard/styles/index.js +27 -0
  74. package/dist/server/dashboard/styles/index.js.map +1 -0
  75. package/dist/server/dashboard/styles/logs.d.ts +5 -0
  76. package/dist/server/dashboard/styles/logs.d.ts.map +1 -0
  77. package/dist/server/dashboard/styles/logs.js +89 -0
  78. package/dist/server/dashboard/styles/logs.js.map +1 -0
  79. package/dist/server/dashboard/styles/notifications.d.ts +5 -0
  80. package/dist/server/dashboard/styles/notifications.d.ts.map +1 -0
  81. package/dist/server/dashboard/styles/notifications.js +51 -0
  82. package/dist/server/dashboard/styles/notifications.js.map +1 -0
  83. package/dist/server/dashboard/styles/variables.d.ts +5 -0
  84. package/dist/server/dashboard/styles/variables.d.ts.map +1 -0
  85. package/dist/server/dashboard/styles/variables.js +29 -0
  86. package/dist/server/dashboard/styles/variables.js.map +1 -0
  87. package/dist/server/dashboard/utils.d.ts +8 -0
  88. package/dist/server/dashboard/utils.d.ts.map +1 -0
  89. package/dist/server/dashboard/utils.js +14 -0
  90. package/dist/server/dashboard/utils.js.map +1 -0
  91. package/dist/server/dashboard.d.ts +5 -21
  92. package/dist/server/dashboard.d.ts.map +1 -1
  93. package/dist/server/dashboard.js +5 -4945
  94. package/dist/server/dashboard.js.map +1 -1
  95. package/dist/server/index.d.ts.map +1 -1
  96. package/dist/server/index.js +51 -0
  97. package/dist/server/index.js.map +1 -1
  98. package/dist/server/websocket.d.ts +12 -0
  99. package/dist/server/websocket.d.ts.map +1 -1
  100. package/dist/server/websocket.js +19 -0
  101. package/dist/server/websocket.js.map +1 -1
  102. package/dist/services/claude.d.ts.map +1 -1
  103. package/dist/services/claude.js +4 -1
  104. package/dist/services/claude.js.map +1 -1
  105. package/dist/utils/config.d.ts +29 -21
  106. package/dist/utils/config.d.ts.map +1 -1
  107. package/dist/utils/config.js +13 -11
  108. package/dist/utils/config.js.map +1 -1
  109. package/dist/utils/version-check.d.ts +26 -0
  110. package/dist/utils/version-check.d.ts.map +1 -0
  111. package/dist/utils/version-check.js +234 -0
  112. package/dist/utils/version-check.js.map +1 -0
  113. package/package.json +2 -2
  114. package/templates/columns/00_backlog.en.md +1 -1
  115. package/templates/columns/00_backlog.fr.md +1 -1
  116. package/templates/columns/01_ready.en.md +1 -1
  117. package/templates/columns/01_ready.fr.md +1 -1
  118. package/templates/columns/02_in-progress.en.md +1 -1
  119. package/templates/columns/02_in-progress.fr.md +1 -1
  120. package/templates/columns/{07_testing-playwright.en.md → 03_testing-playwright.en.md} +1 -1
  121. package/templates/columns/{07_testing-playwright.fr.md → 03_testing-playwright.fr.md} +1 -1
  122. package/templates/columns/{08_testing-cypress.en.md → 04_testing-cypress.en.md} +1 -1
  123. package/templates/columns/{08_testing-cypress.fr.md → 04_testing-cypress.fr.md} +1 -1
  124. package/templates/columns/{03_review-best-practices.en.md → 05_review-best-practices.en.md} +1 -1
  125. package/templates/columns/{03_review-best-practices.fr.md → 05_review-best-practices.fr.md} +1 -1
  126. package/templates/columns/{04_review-no-duplication.en.md → 06_review-no-duplication.en.md} +1 -1
  127. package/templates/columns/{04_review-no-duplication.fr.md → 06_review-no-duplication.fr.md} +1 -1
  128. package/templates/columns/{05_review-consistency.en.md → 07_review-consistency.en.md} +1 -1
  129. package/templates/columns/{05_review-consistency.fr.md → 07_review-consistency.fr.md} +1 -1
  130. package/templates/columns/{06_review-security.en.md → 08_review-security.en.md} +1 -1
  131. package/templates/columns/{06_review-security.fr.md → 08_review-security.fr.md} +1 -1
  132. package/templates/columns/09_retest-playwright.en.md +30 -0
  133. package/templates/columns/09_retest-playwright.fr.md +30 -0
  134. package/templates/columns/10_retest-cypress.en.md +29 -0
  135. package/templates/columns/10_retest-cypress.fr.md +29 -0
  136. package/templates/columns/{09_update-docs.en.md → 11_update-docs.en.md} +1 -1
  137. package/templates/columns/{09_update-docs.fr.md → 11_update-docs.fr.md} +1 -1
  138. package/templates/columns/{10_deploy-staging.en.md → 12_deploy-staging.en.md} +1 -1
  139. package/templates/columns/{10_deploy-staging.fr.md → 12_deploy-staging.fr.md} +1 -1
  140. package/templates/columns/{11_validate-staging.en.md → 13_validate-staging.en.md} +1 -1
  141. package/templates/columns/{11_validate-staging.fr.md → 13_validate-staging.fr.md} +1 -1
  142. package/templates/columns/{12_done.en.md → 14_done.en.md} +1 -1
  143. package/templates/columns/{12_done.fr.md → 14_done.fr.md} +1 -1
@@ -0,0 +1,1325 @@
1
+ /**
2
+ * Dashboard client-side JavaScript
3
+ *
4
+ * This file contains all interactive functionality for the dashboard.
5
+ * The code is kept in a single function to maintain shared state and scope.
6
+ */
7
+ /**
8
+ * Get all JavaScript for dashboard interactivity
9
+ */
10
+ export function getScript() {
11
+ return `
12
+ // ========================================
13
+ // i18n TRANSLATIONS
14
+ // ========================================
15
+ const translations = {
16
+ en: {
17
+ // Filter
18
+ 'filter.allPriorities': 'All priorities',
19
+ 'filter.search': 'Search...',
20
+ // Buttons
21
+ 'btn.newTicket': '+ New ticket',
22
+ 'btn.createTicket': 'Create ticket',
23
+ 'btn.update': 'Update',
24
+ 'btn.cancel': 'Cancel',
25
+ 'btn.archive': 'Archive',
26
+ 'btn.next': 'Next',
27
+ 'btn.add': 'Add',
28
+ 'btn.edit': 'Edit',
29
+ 'btn.save': 'Save',
30
+ 'btn.reload': 'Reload',
31
+ // Modal
32
+ 'modal.newTicket': 'New ticket',
33
+ 'modal.editTicket': 'Edit',
34
+ 'modal.title': 'Title *',
35
+ 'modal.titlePlaceholder': 'E.g.: Fix the login bug',
36
+ 'modal.description': 'Description',
37
+ 'modal.descriptionPlaceholder': 'Describe the context and details...',
38
+ 'modal.priority': 'Priority',
39
+ 'modal.releaseType': 'Release type',
40
+ 'modal.labels': 'Labels',
41
+ 'modal.selectLabel': 'Select a label...',
42
+ 'modal.acceptanceCriteria': 'Acceptance criteria',
43
+ 'modal.addCriteria': 'Add criteria',
44
+ 'modal.attachments': 'Attachments',
45
+ 'modal.addAttachment': 'Add file',
46
+ 'modal.noAttachments': 'No attachments',
47
+ 'modal.comments': 'Comments',
48
+ 'modal.noComments': 'No comments',
49
+ 'modal.addCommentPlaceholder': 'Add a comment...',
50
+ 'modal.claudeTerminal': 'Claude Terminal',
51
+ // Priority
52
+ 'priority.p0': 'P0 - Critical',
53
+ 'priority.p1': 'P1 - High',
54
+ 'priority.p2': 'P2 - Normal',
55
+ 'priority.p3': 'P3 - Low',
56
+ // Semver
57
+ 'semver.patch': 'Patch (x.x.X) - Bug fix',
58
+ 'semver.minor': 'Minor (x.X.0) - Feature',
59
+ 'semver.major': 'Major (X.0.0) - Breaking',
60
+ 'semver.none': 'No deployment',
61
+ // Status
62
+ 'status.waiting': 'Waiting',
63
+ 'status.processing': 'Running...',
64
+ 'status.completed': 'Completed',
65
+ 'status.failed': 'Failed',
66
+ 'status.connected': 'Connected',
67
+ // Stats
68
+ 'stats.total': 'Total',
69
+ // Board
70
+ 'board.empty': 'Empty',
71
+ // Action modal
72
+ 'action.instructions': 'Instructions',
73
+ 'action.noInstructions': 'No instructions available',
74
+ 'action.noFile': 'No ACTION file. Click "Edit" to create it.',
75
+ 'action.modifiedOn': 'Modified on',
76
+ // Notifications
77
+ 'notify.titleRequired': 'Title required',
78
+ 'notify.titleMandatory': 'Title is mandatory',
79
+ 'notify.ticketUpdated': 'Ticket updated',
80
+ 'notify.ticketCreated': 'Ticket created',
81
+ 'notify.ticketAdvanced': 'Ticket advanced',
82
+ 'notify.ticketArchived': 'Ticket archived',
83
+ 'notify.ticketMoved': 'moved',
84
+ 'notify.moveTo': 'To',
85
+ 'notify.commentAdded': 'Comment added',
86
+ 'notify.actionUpdated': 'updated',
87
+ 'notify.error': 'Error',
88
+ 'notify.unableToSave': 'Unable to save',
89
+ 'notify.loadingError': 'Loading error',
90
+ 'notify.claudeStarted': 'Claude started',
91
+ 'notify.claudeFinished': 'Claude finished',
92
+ 'notify.claudeFailed': 'Claude failed',
93
+ 'notify.processingSuccess': 'Processing successful',
94
+ 'notify.checkLogs': 'Check logs',
95
+ // Confirm
96
+ 'confirm.archive': 'Archive',
97
+ // Button states
98
+ 'btn.updating': 'Updating...',
99
+ 'btn.creating': 'Creating...',
100
+ 'btn.moving': 'Moving...',
101
+ 'btn.archiving': 'Archiving...',
102
+ 'btn.sending': 'Sending...',
103
+ 'btn.saving': 'Saving...',
104
+ },
105
+ fr: {
106
+ // Filter
107
+ 'filter.allPriorities': 'Toutes priorités',
108
+ 'filter.search': 'Rechercher...',
109
+ // Buttons
110
+ 'btn.newTicket': '+ Nouveau ticket',
111
+ 'btn.createTicket': 'Créer le ticket',
112
+ 'btn.update': 'Mettre à jour',
113
+ 'btn.cancel': 'Annuler',
114
+ 'btn.archive': 'Archiver',
115
+ 'btn.next': 'Suivant',
116
+ 'btn.add': 'Ajouter',
117
+ 'btn.edit': 'Modifier',
118
+ 'btn.save': 'Enregistrer',
119
+ 'btn.reload': 'Recharger',
120
+ // Modal
121
+ 'modal.newTicket': 'Nouveau ticket',
122
+ 'modal.editTicket': 'Modifier',
123
+ 'modal.title': 'Titre *',
124
+ 'modal.titlePlaceholder': 'Ex: Corriger le bug de connexion',
125
+ 'modal.description': 'Description',
126
+ 'modal.descriptionPlaceholder': 'Décrivez le contexte et les détails...',
127
+ 'modal.priority': 'Priorité',
128
+ 'modal.releaseType': 'Type de release',
129
+ 'modal.labels': 'Labels',
130
+ 'modal.selectLabel': 'Sélectionner un label...',
131
+ 'modal.acceptanceCriteria': 'Critères d\\'acceptation',
132
+ 'modal.addCriteria': 'Ajouter un critère',
133
+ 'modal.attachments': 'Pièces jointes',
134
+ 'modal.addAttachment': 'Ajouter un fichier',
135
+ 'modal.noAttachments': 'Aucune pièce jointe',
136
+ 'modal.comments': 'Commentaires',
137
+ 'modal.noComments': 'Aucun commentaire',
138
+ 'modal.addCommentPlaceholder': 'Ajouter un commentaire...',
139
+ 'modal.claudeTerminal': 'Terminal Claude',
140
+ // Priority
141
+ 'priority.p0': 'P0 - Critique',
142
+ 'priority.p1': 'P1 - Haute',
143
+ 'priority.p2': 'P2 - Normale',
144
+ 'priority.p3': 'P3 - Basse',
145
+ // Semver
146
+ 'semver.patch': 'Patch (x.x.X) - Bug fix',
147
+ 'semver.minor': 'Minor (x.X.0) - Fonctionnalité',
148
+ 'semver.major': 'Major (X.0.0) - Breaking',
149
+ 'semver.none': 'Aucun déploiement',
150
+ // Status
151
+ 'status.waiting': 'En attente',
152
+ 'status.processing': 'En cours...',
153
+ 'status.completed': 'Terminé',
154
+ 'status.failed': 'Échoué',
155
+ 'status.connected': 'Connecté',
156
+ // Stats
157
+ 'stats.total': 'Total',
158
+ // Board
159
+ 'board.empty': 'Vide',
160
+ // Action modal
161
+ 'action.instructions': 'Instructions',
162
+ 'action.noInstructions': 'Aucune instruction disponible',
163
+ 'action.noFile': 'Aucun fichier ACTION. Cliquez sur "Modifier" pour le créer.',
164
+ 'action.modifiedOn': 'Modifié le',
165
+ // Notifications
166
+ 'notify.titleRequired': 'Titre requis',
167
+ 'notify.titleMandatory': 'Le titre est obligatoire',
168
+ 'notify.ticketUpdated': 'Ticket mis à jour',
169
+ 'notify.ticketCreated': 'Ticket créé',
170
+ 'notify.ticketAdvanced': 'Ticket avancé',
171
+ 'notify.ticketArchived': 'Ticket archivé',
172
+ 'notify.ticketMoved': 'déplacé',
173
+ 'notify.moveTo': 'Vers',
174
+ 'notify.commentAdded': 'Commentaire ajouté',
175
+ 'notify.actionUpdated': 'mis à jour',
176
+ 'notify.error': 'Erreur',
177
+ 'notify.unableToSave': 'Impossible de sauvegarder',
178
+ 'notify.loadingError': 'Erreur de chargement',
179
+ 'notify.claudeStarted': 'Claude démarré',
180
+ 'notify.claudeFinished': 'Claude terminé',
181
+ 'notify.claudeFailed': 'Claude échoué',
182
+ 'notify.processingSuccess': 'Traitement réussi',
183
+ 'notify.checkLogs': 'Voir les logs',
184
+ // Confirm
185
+ 'confirm.archive': 'Archiver',
186
+ // Button states
187
+ 'btn.updating': 'Mise à jour...',
188
+ 'btn.creating': 'Création...',
189
+ 'btn.moving': 'Déplacement...',
190
+ 'btn.archiving': 'Archivage...',
191
+ 'btn.sending': 'Envoi...',
192
+ 'btn.saving': 'Sauvegarde...',
193
+ }
194
+ };
195
+
196
+ let currentLang = localStorage.getItem('autocode-lang') || 'fr';
197
+
198
+ function t(key) {
199
+ return translations[currentLang][key] || translations['en'][key] || key;
200
+ }
201
+
202
+ function switchLanguage(lang) {
203
+ currentLang = lang;
204
+ currentActionLang = lang;
205
+ localStorage.setItem('autocode-lang', lang);
206
+ document.documentElement.lang = lang;
207
+
208
+ // Update lang switcher buttons
209
+ document.querySelectorAll('.lang-switcher .lang-btn').forEach(btn => {
210
+ btn.classList.toggle('active', btn.dataset.lang === lang);
211
+ });
212
+
213
+ // Update all elements with data-i18n
214
+ document.querySelectorAll('[data-i18n]').forEach(el => {
215
+ const key = el.getAttribute('data-i18n');
216
+ if (translations[lang] && translations[lang][key]) {
217
+ el.textContent = translations[lang][key];
218
+ }
219
+ });
220
+
221
+ // Update placeholders
222
+ document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
223
+ const key = el.getAttribute('data-i18n-placeholder');
224
+ if (translations[lang] && translations[lang][key]) {
225
+ el.placeholder = translations[lang][key];
226
+ }
227
+ });
228
+
229
+ // Update select options
230
+ document.querySelectorAll('select option[data-i18n]').forEach(el => {
231
+ const key = el.getAttribute('data-i18n');
232
+ if (translations[lang] && translations[lang][key]) {
233
+ el.textContent = translations[lang][key];
234
+ }
235
+ });
236
+
237
+ // Re-render board to update dynamic content
238
+ render();
239
+ }
240
+
241
+ // ========================================
242
+ // STATE
243
+ // ========================================
244
+ let filterPriority = '';
245
+ let filterSearch = '';
246
+ let selectedLabels = [];
247
+ let criteriaCount = 0;
248
+ let editingKey = null;
249
+ let currentComments = [];
250
+ let contextMenuTicket = null;
251
+ let draggedTicket = null;
252
+ let draggedFromColumn = null;
253
+ let currentActionSlug = null;
254
+ let currentActionLang = localStorage.getItem('autocode-lang') || 'fr';
255
+ let originalActionContent = '';
256
+ let claudeProcessingTickets = new Set();
257
+
258
+ // Initialize language switcher on page load
259
+ (function initLangSwitcher() {
260
+ document.querySelectorAll('.lang-switcher .lang-btn').forEach(btn => {
261
+ btn.addEventListener('click', () => switchLanguage(btn.dataset.lang));
262
+ });
263
+ switchLanguage(currentLang);
264
+ })();
265
+
266
+ // ========================================
267
+ // NOTIFICATIONS
268
+ // ========================================
269
+ function showNotification(type, title, message, duration = 5000) {
270
+ const container = document.getElementById('notifications');
271
+ const id = 'notif-' + Date.now();
272
+ const icons = { info: '\\u{1F535}', success: '\\u2705', warning: '\\u26A0\\uFE0F', error: '\\u274C', claude: '\\u{1F916}' };
273
+
274
+ const notif = document.createElement('div');
275
+ notif.className = 'notification ' + (type === 'claude' ? 'info' : type);
276
+ notif.id = id;
277
+ notif.innerHTML = '<span class="notification-icon">' + (icons[type] || icons.info) + '</span>' +
278
+ '<div class="notification-content"><div class="notification-title">' + title + '</div>' +
279
+ (message ? '<div class="notification-message">' + message + '</div>' : '') + '</div>' +
280
+ '<button class="notification-close" onclick="closeNotification(\\'' + id + '\\')">&times;</button>';
281
+
282
+ container.appendChild(notif);
283
+ setTimeout(() => closeNotification(id), duration);
284
+ }
285
+
286
+ function closeNotification(id) {
287
+ const notif = document.getElementById(id);
288
+ if (notif) notif.remove();
289
+ }
290
+
291
+ // ========================================
292
+ // RENDER
293
+ // ========================================
294
+ function render() {
295
+ const board = document.getElementById('board');
296
+ board.innerHTML = '';
297
+ let stats = { total: 0, byPriority: { P0: 0, P1: 0, P2: 0, P3: 0 } };
298
+
299
+ COLUMNS.forEach(col => {
300
+ let tickets = TICKETS.filter(t => t.column_slug === col.slug);
301
+ if (filterPriority) tickets = tickets.filter(t => t.priority === filterPriority);
302
+ if (filterSearch) {
303
+ const s = filterSearch.toLowerCase();
304
+ tickets = tickets.filter(t => t.title.toLowerCase().includes(s) || t.key.toLowerCase().includes(s));
305
+ }
306
+ tickets.sort((a, b) => {
307
+ const p = { P0: 0, P1: 1, P2: 2, P3: 3 };
308
+ return p[a.priority] - p[b.priority];
309
+ });
310
+
311
+ stats.total += tickets.length;
312
+ tickets.forEach(t => stats.byPriority[t.priority] = (stats.byPriority[t.priority] || 0) + 1);
313
+
314
+ const div = document.createElement('div');
315
+ div.className = 'column';
316
+ div.ondragover = onDragOver;
317
+ div.ondragenter = onDragEnter;
318
+ div.ondragleave = onDragLeave;
319
+ div.ondrop = (e) => onDrop(e, col.slug, col.name);
320
+
321
+ div.innerHTML = '<div class="column-header">' +
322
+ '<span class="column-title">' + col.name + '</span>' +
323
+ '<div class="column-header-actions">' +
324
+ '<a class="btn-action" title="ACTION.md" href="/column/' + col.slug + '/edit">\\u{1F4D8}</a>' +
325
+ '<span class="column-count">' + tickets.length + '</span>' +
326
+ '</div></div>' +
327
+ '<div class="column-body">' +
328
+ (tickets.length ? tickets.map(tk => {
329
+ const isProcessing = claudeProcessingTickets.has(tk.key);
330
+ return '<div class="ticket' + (isProcessing ? ' claude-processing' : '') + '" draggable="true" ' +
331
+ 'onclick="onTicketClick(\\'' + tk.key + '\\')" ' +
332
+ 'oncontextmenu="showContextMenu(event,\\'' + tk.key + '\\')" ' +
333
+ 'ondragstart="onDragStart(event,\\'' + tk.key + '\\',\\'' + col.slug + '\\')" ' +
334
+ 'ondragend="onDragEnd(event)">' +
335
+ '<div class="ticket-key">' + tk.key + '</div>' +
336
+ '<div class="ticket-title">' + escapeHtml(tk.title) + '</div>' +
337
+ '<div class="ticket-meta"><span class="priority ' + tk.priority + '">' + tk.priority + '</span></div>' +
338
+ (isProcessing ? '<div class="ticket-claude-indicator"><span class="claude-dot"></span>\\u{1F916} ' + t('status.processing') + '</div>' : '') +
339
+ '</div>';
340
+ }).join('') : '<div class="empty">' + t('board.empty') + '</div>') +
341
+ '</div>';
342
+
343
+ board.appendChild(div);
344
+ });
345
+
346
+ document.getElementById('stats').innerHTML =
347
+ '<span class="stat connected">\\u{1F7E2} ' + t('status.connected') + '</span>' +
348
+ '<span class="stat">' + t('stats.total') + ': ' + stats.total + '</span>' +
349
+ '<span class="stat P0">P0: ' + (stats.byPriority.P0 || 0) + '</span>' +
350
+ '<span class="stat P1">P1: ' + (stats.byPriority.P1 || 0) + '</span>' +
351
+ '<span class="stat P2">P2: ' + (stats.byPriority.P2 || 0) + '</span>';
352
+ }
353
+
354
+ function escapeHtml(text) {
355
+ const div = document.createElement('div');
356
+ div.textContent = text;
357
+ return div.innerHTML;
358
+ }
359
+
360
+ // ========================================
361
+ // FILTERS
362
+ // ========================================
363
+ document.getElementById('filter-priority').onchange = e => { filterPriority = e.target.value; render(); };
364
+ document.getElementById('filter-search').oninput = e => { filterSearch = e.target.value.toLowerCase(); render(); };
365
+
366
+ // ========================================
367
+ // MODAL TICKET
368
+ // ========================================
369
+ function openModal(key = null) {
370
+ editingKey = key;
371
+ document.getElementById('modal-overlay').classList.add('active');
372
+ document.body.style.overflow = 'hidden';
373
+
374
+ const modalTitle = document.getElementById('modal-title');
375
+ const saveBtn = document.getElementById('btn-save');
376
+ const nextBtn = document.getElementById('btn-next');
377
+ const archiveBtn = document.getElementById('btn-archive');
378
+ const commentsSection = document.getElementById('comments-section');
379
+ const attachmentsSection = document.getElementById('attachments-section');
380
+
381
+ if (key) {
382
+ modalTitle.textContent = t('modal.editTicket') + ' ' + key;
383
+ saveBtn.textContent = t('btn.update');
384
+ nextBtn.style.display = 'inline-block';
385
+ archiveBtn.style.display = 'inline-block';
386
+ commentsSection.style.display = 'block';
387
+ attachmentsSection.style.display = 'block';
388
+ loadTicketForEdit(key);
389
+ } else {
390
+ modalTitle.textContent = t('modal.newTicket');
391
+ saveBtn.textContent = t('btn.createTicket');
392
+ nextBtn.style.display = 'none';
393
+ archiveBtn.style.display = 'none';
394
+ commentsSection.style.display = 'none';
395
+ attachmentsSection.style.display = 'none';
396
+ resetForm();
397
+ resetComments();
398
+ resetAttachments();
399
+ }
400
+ }
401
+
402
+ function closeModal() {
403
+ document.getElementById('modal-overlay').classList.remove('active');
404
+ document.body.style.overflow = '';
405
+ editingKey = null;
406
+ resetForm();
407
+ resetClaudeLog();
408
+ }
409
+
410
+ function resetForm() {
411
+ document.getElementById('ticket-title').value = '';
412
+ document.getElementById('ticket-description').value = '';
413
+ document.getElementById('ticket-priority').value = 'P2';
414
+ document.getElementById('ticket-semver').value = 'patch';
415
+ document.getElementById('ticket-labels').value = '';
416
+ selectedLabels = [];
417
+ criteriaCount = 0;
418
+ document.getElementById('selected-labels').innerHTML = '';
419
+ document.getElementById('criteria-list').innerHTML = '';
420
+ }
421
+
422
+ async function loadTicketForEdit(key) {
423
+ try {
424
+ const res = await fetch('/api/tickets/' + key);
425
+ if (!res.ok) throw new Error('Ticket not found');
426
+ const json = await res.json();
427
+ if (!json.success || !json.data) throw new Error('Ticket not found');
428
+ const ticket = json.data;
429
+
430
+ document.getElementById('ticket-title').value = ticket.title || '';
431
+ document.getElementById('ticket-description').value = ticket.description || '';
432
+ document.getElementById('ticket-priority').value = ticket.priority || 'P2';
433
+ document.getElementById('ticket-semver').value = ticket.semver || 'patch';
434
+
435
+ selectedLabels = Array.isArray(ticket.labels) ? [...ticket.labels] : [];
436
+ renderLabels();
437
+
438
+ criteriaCount = 0;
439
+ document.getElementById('criteria-list').innerHTML = '';
440
+ if (Array.isArray(ticket.acceptance_criteria)) {
441
+ ticket.acceptance_criteria.forEach(c => addCriteria(c));
442
+ }
443
+
444
+ renderComments(ticket.comments || []);
445
+ loadAttachments(key);
446
+ fetchLog(key);
447
+ } catch (e) {
448
+ console.error('Error:', e);
449
+ showNotification('error', 'Error', e.message);
450
+ closeModal();
451
+ }
452
+ }
453
+
454
+ async function saveTicket() {
455
+ const title = document.getElementById('ticket-title').value.trim();
456
+ const description = document.getElementById('ticket-description').value.trim();
457
+ const priority = document.getElementById('ticket-priority').value;
458
+ const semver = document.getElementById('ticket-semver').value;
459
+ const criteria = getCriteria();
460
+
461
+ if (!title) {
462
+ showNotification('warning', t('notify.titleRequired'), t('notify.titleMandatory'));
463
+ return;
464
+ }
465
+
466
+ const btn = document.getElementById('btn-save');
467
+ btn.disabled = true;
468
+
469
+ try {
470
+ if (editingKey) {
471
+ btn.textContent = t('btn.updating');
472
+ await fetch('/api/tickets/' + editingKey, {
473
+ method: 'PATCH',
474
+ headers: { 'Content-Type': 'application/json' },
475
+ body: JSON.stringify({ title, description, priority, semver, labels: selectedLabels, acceptance_criteria: criteria })
476
+ });
477
+ showNotification('success', t('notify.ticketUpdated'), editingKey);
478
+ } else {
479
+ btn.textContent = t('btn.creating');
480
+ const res = await fetch('/api/tickets', {
481
+ method: 'POST',
482
+ headers: { 'Content-Type': 'application/json' },
483
+ body: JSON.stringify({ title, priority, labels: selectedLabels, description, semver, acceptance_criteria: criteria })
484
+ });
485
+ const data = await res.json();
486
+ showNotification('success', t('notify.ticketCreated'), data.key || '');
487
+ }
488
+ closeModal();
489
+ loadTicketsFromAPI();
490
+ } catch (e) {
491
+ showNotification('error', t('notify.error'), e.message);
492
+ btn.disabled = false;
493
+ btn.textContent = editingKey ? t('btn.update') : t('btn.createTicket');
494
+ }
495
+ }
496
+
497
+ async function advanceTicket() {
498
+ if (!editingKey) return;
499
+ const btn = document.getElementById('btn-next');
500
+ btn.disabled = true;
501
+ btn.textContent = t('btn.moving');
502
+ try {
503
+ await fetch('/api/tickets/' + editingKey + '/next', {
504
+ method: 'POST',
505
+ headers: { 'Content-Type': 'application/json' },
506
+ body: JSON.stringify({ lang: currentLang })
507
+ });
508
+ showNotification('info', t('notify.ticketAdvanced'), editingKey);
509
+ closeModal();
510
+ loadTicketsFromAPI();
511
+ } catch (e) {
512
+ showNotification('error', t('notify.error'), e.message);
513
+ btn.disabled = false;
514
+ btn.textContent = t('btn.next');
515
+ }
516
+ }
517
+
518
+ async function archiveTicket() {
519
+ if (!editingKey) return;
520
+ if (!confirm(t('confirm.archive') + ' ' + editingKey + '?')) return;
521
+
522
+ const btn = document.getElementById('btn-archive');
523
+ btn.disabled = true;
524
+ btn.textContent = t('btn.archiving');
525
+ try {
526
+ const lastColumn = COLUMNS[COLUMNS.length - 1];
527
+ await fetch('/api/tickets/' + editingKey + '/move', {
528
+ method: 'POST',
529
+ headers: { 'Content-Type': 'application/json' },
530
+ body: JSON.stringify({ column: lastColumn.name, force: true, lang: currentLang })
531
+ });
532
+ showNotification('info', t('notify.ticketArchived'), editingKey);
533
+ closeModal();
534
+ loadTicketsFromAPI();
535
+ } catch (e) {
536
+ showNotification('error', t('notify.error'), e.message);
537
+ btn.disabled = false;
538
+ btn.textContent = t('btn.archive');
539
+ }
540
+ }
541
+
542
+ // ========================================
543
+ // LABELS
544
+ // ========================================
545
+ function addLabel(select) {
546
+ const value = select.value;
547
+ if (value && !selectedLabels.includes(value)) {
548
+ selectedLabels.push(value);
549
+ renderLabels();
550
+ }
551
+ select.value = '';
552
+ }
553
+
554
+ function removeLabel(label) {
555
+ selectedLabels = selectedLabels.filter(l => l !== label);
556
+ renderLabels();
557
+ }
558
+
559
+ function renderLabels() {
560
+ const container = document.getElementById('selected-labels');
561
+ container.innerHTML = selectedLabels.map(label =>
562
+ '<span class="label-tag">' + label + '<span class="remove-label" onclick="removeLabel(\\'' + label + '\\')">&times;</span></span>'
563
+ ).join('');
564
+ }
565
+
566
+ // ========================================
567
+ // CRITERIA
568
+ // ========================================
569
+ function addCriteria(value = '') {
570
+ criteriaCount++;
571
+ const id = criteriaCount;
572
+ const container = document.getElementById('criteria-list');
573
+ const div = document.createElement('div');
574
+ div.className = 'criteria-item';
575
+ div.id = 'criteria-' + id;
576
+ div.innerHTML = '<input type="text" placeholder="Ex: L\\'utilisateur peut..." value="' + escapeHtml(value) + '">' +
577
+ '<button type="button" class="btn-remove" onclick="removeCriteria(' + id + ')">&times;</button>';
578
+ container.appendChild(div);
579
+ }
580
+
581
+ function removeCriteria(id) {
582
+ const el = document.getElementById('criteria-' + id);
583
+ if (el) el.remove();
584
+ }
585
+
586
+ function getCriteria() {
587
+ const items = document.querySelectorAll('.criteria-item input');
588
+ return Array.from(items).map(i => i.value.trim()).filter(Boolean);
589
+ }
590
+
591
+ // ========================================
592
+ // COMMENTS
593
+ // ========================================
594
+ function resetComments() {
595
+ currentComments = [];
596
+ document.getElementById('comments-list').innerHTML = '<div class="no-comments">' + t('modal.noComments') + '</div>';
597
+ document.getElementById('comments-count').textContent = '0';
598
+ document.getElementById('new-comment').value = '';
599
+ }
600
+
601
+ function renderComments(comments) {
602
+ currentComments = comments || [];
603
+ const list = document.getElementById('comments-list');
604
+ const count = document.getElementById('comments-count');
605
+ count.textContent = currentComments.length;
606
+
607
+ if (currentComments.length === 0) {
608
+ list.innerHTML = '<div class="no-comments">' + t('modal.noComments') + '</div>';
609
+ return;
610
+ }
611
+
612
+ const sorted = [...currentComments].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
613
+ list.innerHTML = sorted.map((comment, index) => {
614
+ const date = new Date(comment.created_at);
615
+ const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
616
+ const source = comment.source || 'user';
617
+ const sourceBadge = source === 'claude'
618
+ ? '<span class="comment-source claude">Claude</span>'
619
+ : '<span class="comment-source user">User</span>';
620
+ return '<div class="comment" id="comment-' + index + '">' +
621
+ '<div class="comment-meta" onclick="toggleComment(' + index + ')">' + sourceBadge + '<span class="comment-column">' + (comment.column || 'N/A') + '</span>' +
622
+ '<span class="comment-date">' + dateStr + '</span></div>' +
623
+ '<div class="comment-text">' + renderMarkdown(comment.text) + '</div></div>';
624
+ }).join('');
625
+ }
626
+
627
+ function toggleComment(index) {
628
+ const comments = document.querySelectorAll('.comment');
629
+ comments.forEach((comment, i) => {
630
+ if (i === index) {
631
+ comment.classList.toggle('expanded');
632
+ } else {
633
+ comment.classList.remove('expanded');
634
+ }
635
+ });
636
+ }
637
+
638
+ function renderMarkdown(text) {
639
+ if (!text) return '';
640
+ let html = escapeHtml(text);
641
+ html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
642
+ html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
643
+ html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>');
644
+ html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
645
+ html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
646
+ html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
647
+ html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
648
+ html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
649
+ html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
650
+ html = html.replace(/\\n/g, '<br>');
651
+ return html;
652
+ }
653
+
654
+ async function addComment() {
655
+ if (!editingKey) return;
656
+ const textarea = document.getElementById('new-comment');
657
+ const text = textarea.value.trim();
658
+ if (!text) return;
659
+
660
+ const btn = document.querySelector('.btn-comment');
661
+ btn.disabled = true;
662
+ btn.textContent = t('btn.sending');
663
+
664
+ try {
665
+ const res = await fetch('/api/tickets/' + editingKey + '/comments', {
666
+ method: 'POST',
667
+ headers: { 'Content-Type': 'application/json' },
668
+ body: JSON.stringify({ text })
669
+ });
670
+ const result = await res.json();
671
+ textarea.value = '';
672
+ if (result.success && result.data && result.data.comments) {
673
+ renderComments(result.data.comments);
674
+ }
675
+ showNotification('success', t('notify.commentAdded'), '');
676
+ } catch (e) {
677
+ showNotification('error', t('notify.error'), e.message);
678
+ } finally {
679
+ btn.disabled = false;
680
+ btn.textContent = t('btn.add');
681
+ }
682
+ }
683
+
684
+ // ========================================
685
+ // ATTACHMENTS
686
+ // ========================================
687
+ let currentAttachments = [];
688
+
689
+ function resetAttachments() {
690
+ currentAttachments = [];
691
+ document.getElementById('attachments-list').innerHTML = '<div class="no-attachments">' + t('modal.noAttachments') + '</div>';
692
+ document.getElementById('attachments-count').textContent = '0';
693
+ document.getElementById('file-input').value = '';
694
+ }
695
+
696
+ async function loadAttachments(key) {
697
+ try {
698
+ const res = await fetch('/api/tickets/' + key + '/attachments');
699
+ const json = await res.json();
700
+ if (json.success) {
701
+ currentAttachments = json.data || [];
702
+ renderAttachments();
703
+ }
704
+ } catch (e) {
705
+ console.error('Error loading attachments:', e);
706
+ }
707
+ }
708
+
709
+ function renderAttachments() {
710
+ const list = document.getElementById('attachments-list');
711
+ const count = document.getElementById('attachments-count');
712
+ count.textContent = currentAttachments.length;
713
+
714
+ if (currentAttachments.length === 0) {
715
+ list.innerHTML = '<div class="no-attachments">' + t('modal.noAttachments') + '</div>';
716
+ return;
717
+ }
718
+
719
+ list.innerHTML = currentAttachments.map(filename => {
720
+ const ext = filename.split('.').pop().toLowerCase();
721
+ const icon = getFileIcon(ext);
722
+ return '<div class="attachment-item">' +
723
+ '<span class="attachment-icon">' + icon + '</span>' +
724
+ '<span class="attachment-name" title="' + escapeHtml(filename) + '">' + escapeHtml(filename) + '</span>' +
725
+ '<button class="attachment-delete" onclick="deleteAttachment(\\'' + escapeHtml(filename) + '\\')" title="Delete">&times;</button>' +
726
+ '</div>';
727
+ }).join('');
728
+ }
729
+
730
+ function getFileIcon(ext) {
731
+ const icons = {
732
+ pdf: '\\u{1F4C4}', doc: '\\u{1F4DD}', docx: '\\u{1F4DD}', txt: '\\u{1F4DD}',
733
+ png: '\\u{1F5BC}', jpg: '\\u{1F5BC}', jpeg: '\\u{1F5BC}', gif: '\\u{1F5BC}', svg: '\\u{1F5BC}', webp: '\\u{1F5BC}',
734
+ mp4: '\\u{1F3AC}', mov: '\\u{1F3AC}', avi: '\\u{1F3AC}',
735
+ mp3: '\\u{1F3B5}', wav: '\\u{1F3B5}',
736
+ zip: '\\u{1F4E6}', rar: '\\u{1F4E6}', tar: '\\u{1F4E6}', gz: '\\u{1F4E6}',
737
+ js: '\\u{1F4DC}', ts: '\\u{1F4DC}', py: '\\u{1F4DC}', json: '\\u{1F4DC}', md: '\\u{1F4DC}',
738
+ };
739
+ return icons[ext] || '\\u{1F4CE}';
740
+ }
741
+
742
+ async function uploadFiles(files) {
743
+ if (!editingKey || files.length === 0) return;
744
+
745
+ const formData = new FormData();
746
+ for (const file of files) {
747
+ formData.append('files', file, file.name);
748
+ }
749
+
750
+ try {
751
+ const res = await fetch('/api/tickets/' + editingKey + '/attachments', {
752
+ method: 'POST',
753
+ body: formData
754
+ });
755
+ const json = await res.json();
756
+ if (json.success) {
757
+ showNotification('success', 'Files uploaded', json.data.join(', '));
758
+ loadAttachments(editingKey);
759
+ } else {
760
+ showNotification('error', 'Upload failed', json.error);
761
+ }
762
+ } catch (e) {
763
+ showNotification('error', 'Upload error', e.message);
764
+ }
765
+
766
+ document.getElementById('file-input').value = '';
767
+ }
768
+
769
+ async function deleteAttachment(filename) {
770
+ if (!editingKey) return;
771
+ if (!confirm('Delete ' + filename + '?')) return;
772
+
773
+ try {
774
+ const res = await fetch('/api/tickets/' + editingKey + '/attachments/' + encodeURIComponent(filename), {
775
+ method: 'DELETE'
776
+ });
777
+ const json = await res.json();
778
+ if (json.success) {
779
+ showNotification('info', 'File deleted', filename);
780
+ loadAttachments(editingKey);
781
+ } else {
782
+ showNotification('error', 'Delete failed', json.error);
783
+ }
784
+ } catch (e) {
785
+ showNotification('error', 'Delete error', e.message);
786
+ }
787
+ }
788
+
789
+ // ========================================
790
+ // DRAG & DROP
791
+ // ========================================
792
+ function onDragStart(e, key, columnSlug) {
793
+ draggedTicket = key;
794
+ draggedFromColumn = columnSlug;
795
+ e.target.classList.add('dragging');
796
+ e.dataTransfer.effectAllowed = 'move';
797
+ e.dataTransfer.setData('text/plain', key);
798
+ }
799
+
800
+ function onDragEnd(e) {
801
+ e.target.classList.remove('dragging');
802
+ document.querySelectorAll('.column').forEach(c => c.classList.remove('drag-over'));
803
+ }
804
+
805
+ function onDragOver(e) {
806
+ e.preventDefault();
807
+ e.dataTransfer.dropEffect = 'move';
808
+ }
809
+
810
+ function onDragEnter(e) {
811
+ e.preventDefault();
812
+ const column = e.target.closest('.column');
813
+ if (column) column.classList.add('drag-over');
814
+ }
815
+
816
+ function onDragLeave(e) {
817
+ const column = e.target.closest('.column');
818
+ if (column && !column.contains(e.relatedTarget)) {
819
+ column.classList.remove('drag-over');
820
+ }
821
+ }
822
+
823
+ async function onDrop(e, targetColumnSlug, targetColumnName) {
824
+ e.preventDefault();
825
+ document.querySelectorAll('.column').forEach(c => c.classList.remove('drag-over'));
826
+
827
+ if (!draggedTicket) return;
828
+ if (draggedFromColumn === targetColumnSlug) {
829
+ draggedTicket = null;
830
+ draggedFromColumn = null;
831
+ return;
832
+ }
833
+
834
+ const key = draggedTicket;
835
+ draggedTicket = null;
836
+ draggedFromColumn = null;
837
+
838
+ try {
839
+ await fetch('/api/tickets/' + key + '/move', {
840
+ method: 'POST',
841
+ headers: { 'Content-Type': 'application/json' },
842
+ body: JSON.stringify({ column: targetColumnName, force: true, lang: currentLang })
843
+ });
844
+ showNotification('info', key + ' ' + t('notify.ticketMoved'), t('notify.moveTo') + ' "' + targetColumnName + '"');
845
+ loadTicketsFromAPI();
846
+ } catch (err) {
847
+ showNotification('error', t('notify.error'), err.message);
848
+ }
849
+ }
850
+
851
+ // ========================================
852
+ // CONTEXT MENU
853
+ // ========================================
854
+ function showContextMenu(e, key) {
855
+ e.preventDefault();
856
+ contextMenuTicket = key;
857
+ const menu = document.getElementById('context-menu');
858
+ menu.style.left = e.clientX + 'px';
859
+ menu.style.top = e.clientY + 'px';
860
+ menu.classList.add('active');
861
+ }
862
+
863
+ function hideContextMenu() {
864
+ document.getElementById('context-menu').classList.remove('active');
865
+ contextMenuTicket = null;
866
+ }
867
+
868
+ function editFromContext() {
869
+ if (contextMenuTicket) openModal(contextMenuTicket);
870
+ hideContextMenu();
871
+ }
872
+
873
+ async function archiveFromContext() {
874
+ if (!contextMenuTicket) return;
875
+ const key = contextMenuTicket;
876
+ hideContextMenu();
877
+
878
+ if (confirm(t('confirm.archive') + ' ' + key + '?')) {
879
+ try {
880
+ const lastColumn = COLUMNS[COLUMNS.length - 1];
881
+ await fetch('/api/tickets/' + key + '/move', {
882
+ method: 'POST',
883
+ headers: { 'Content-Type': 'application/json' },
884
+ body: JSON.stringify({ column: lastColumn.name, force: true, lang: currentLang })
885
+ });
886
+ showNotification('info', t('notify.ticketArchived'), key);
887
+ loadTicketsFromAPI();
888
+ } catch (err) {
889
+ showNotification('error', t('notify.error'), err.message);
890
+ }
891
+ }
892
+ }
893
+
894
+ document.addEventListener('click', hideContextMenu);
895
+
896
+ // ========================================
897
+ // ACTION.md MODAL
898
+ // ========================================
899
+ function openActionModal(slug) {
900
+ currentActionSlug = slug;
901
+ const col = COLUMNS.find(c => c.slug === slug);
902
+ document.getElementById('action-modal-title').textContent = col?.name || slug;
903
+ document.getElementById('action-modal').classList.add('active');
904
+ document.body.style.overflow = 'hidden';
905
+ reloadActionContent();
906
+ }
907
+
908
+ function closeActionModal() {
909
+ document.getElementById('action-modal').classList.remove('active');
910
+ document.body.style.overflow = '';
911
+ currentActionSlug = null;
912
+ originalActionContent = '';
913
+ setActionEditMode(false);
914
+ document.getElementById('action-content').value = '';
915
+ document.getElementById('action-empty').style.display = 'none';
916
+ document.getElementById('action-meta').textContent = '';
917
+ }
918
+
919
+ function setActionEditMode(isEdit) {
920
+ const textarea = document.getElementById('action-content');
921
+ const saveBtn = document.getElementById('action-save-btn');
922
+ const editBtn = document.getElementById('action-edit-btn');
923
+ textarea.readOnly = !isEdit;
924
+ saveBtn.disabled = !isEdit;
925
+ editBtn.style.display = isEdit ? 'none' : 'inline-block';
926
+ }
927
+
928
+ async function reloadActionContent() {
929
+ if (!currentActionSlug) return;
930
+ setActionEditMode(false);
931
+ const textarea = document.getElementById('action-content');
932
+ const empty = document.getElementById('action-empty');
933
+ const meta = document.getElementById('action-meta');
934
+
935
+ empty.style.display = 'none';
936
+ textarea.style.display = 'block';
937
+ textarea.value = '...';
938
+
939
+ try {
940
+ const res = await fetch('/api/columns/' + currentActionSlug + '/actions?lang=' + currentActionLang);
941
+ const data = res.ok ? await res.json() : {};
942
+
943
+ if (!res.ok || !data.success) {
944
+ if (res.status === 404) {
945
+ textarea.value = '';
946
+ textarea.style.display = 'none';
947
+ empty.textContent = t('action.noFile');
948
+ empty.style.display = 'block';
949
+ meta.textContent = '';
950
+ return;
951
+ }
952
+ throw new Error(data.error || t('notify.loadingError'));
953
+ }
954
+
955
+ const actionData = data.data || {};
956
+ originalActionContent = actionData.content || '';
957
+ textarea.value = originalActionContent;
958
+ const updated = actionData.updated_at ? new Date(actionData.updated_at).toLocaleString(currentLang === 'fr' ? 'fr-FR' : 'en-US') : '';
959
+ meta.textContent = (actionData.path || '') + (updated ? ' - ' + t('action.modifiedOn') + ' ' + updated : '');
960
+ } catch (e) {
961
+ empty.textContent = t('notify.error') + ': ' + e.message;
962
+ empty.style.display = 'block';
963
+ textarea.style.display = 'none';
964
+ meta.textContent = '';
965
+ }
966
+ }
967
+
968
+ function enterActionEdit() {
969
+ if (!currentActionSlug) return;
970
+ const textarea = document.getElementById('action-content');
971
+ const empty = document.getElementById('action-empty');
972
+ if (empty.style.display === 'block' && !textarea.value) {
973
+ textarea.value = '# ' + document.getElementById('action-modal-title').textContent + '\\n\\n';
974
+ empty.style.display = 'none';
975
+ textarea.style.display = 'block';
976
+ }
977
+ setActionEditMode(true);
978
+ textarea.focus();
979
+ }
980
+
981
+ async function saveActionContent() {
982
+ if (!currentActionSlug) return;
983
+ const btn = document.getElementById('action-save-btn');
984
+ const textarea = document.getElementById('action-content');
985
+ btn.disabled = true;
986
+ btn.textContent = t('btn.saving');
987
+
988
+ try {
989
+ const res = await fetch('/api/columns/' + currentActionSlug + '/actions?lang=' + currentActionLang, {
990
+ method: 'POST',
991
+ headers: { 'Content-Type': 'application/json' },
992
+ body: JSON.stringify({ content: textarea.value })
993
+ });
994
+ const data = await res.json();
995
+ if (!res.ok || !data.success) throw new Error(data.error || t('notify.error'));
996
+
997
+ originalActionContent = textarea.value;
998
+ setActionEditMode(false);
999
+ showNotification('success', 'ACTION.' + currentActionLang + '.md ' + t('notify.actionUpdated'), currentActionSlug);
1000
+ } catch (e) {
1001
+ showNotification('error', t('notify.unableToSave'), e.message);
1002
+ } finally {
1003
+ btn.disabled = false;
1004
+ btn.textContent = t('btn.save');
1005
+ }
1006
+ }
1007
+
1008
+ // ========================================
1009
+ // WEBSOCKET
1010
+ // ========================================
1011
+ let ws;
1012
+
1013
+ function connectWebSocket() {
1014
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
1015
+ ws = new WebSocket(protocol + '//' + location.host + '/ws');
1016
+
1017
+ ws.onmessage = (event) => {
1018
+ try {
1019
+ const data = JSON.parse(event.data);
1020
+ switch (data.type) {
1021
+ case 'refresh':
1022
+ case 'ticket_updated':
1023
+ case 'ticket_created':
1024
+ case 'ticket_moved':
1025
+ loadTicketsFromAPI();
1026
+ break;
1027
+ case 'claude_start':
1028
+ onClaudeStart(data.ticket);
1029
+ break;
1030
+ case 'claude_stream':
1031
+ onClaudeStream(data.ticket);
1032
+ break;
1033
+ case 'claude_end':
1034
+ onClaudeEnd(data.ticket, data.success, data.duration);
1035
+ loadTicketsFromAPI();
1036
+ break;
1037
+ case 'claude_complete':
1038
+ if (data.success) {
1039
+ showNotification('claude', t('notify.claudeFinished') + ' ' + data.ticket, t('notify.processingSuccess'));
1040
+ } else {
1041
+ showNotification('error', t('notify.claudeFailed') + ' ' + data.ticket, t('notify.checkLogs'));
1042
+ }
1043
+ break;
1044
+ }
1045
+ } catch {}
1046
+ };
1047
+
1048
+ ws.onclose = () => setTimeout(connectWebSocket, 2000);
1049
+ ws.onerror = () => ws.close();
1050
+ }
1051
+
1052
+ // ========================================
1053
+ // CLAUDE LOG (in modal)
1054
+ // ========================================
1055
+ let logPollingInterval = null;
1056
+ let claudeProcessingTicket = null;
1057
+
1058
+ function startLogPolling(key) {
1059
+ stopLogPolling();
1060
+ logPollingInterval = setInterval(() => fetchLog(key), 1000);
1061
+ fetchLog(key);
1062
+ }
1063
+
1064
+ function stopLogPolling() {
1065
+ if (logPollingInterval) {
1066
+ clearInterval(logPollingInterval);
1067
+ logPollingInterval = null;
1068
+ }
1069
+ }
1070
+
1071
+ function formatCodeBlock(content, filename) {
1072
+ const lines = content.split(/\\\\n|\\n/);
1073
+ let lang = 'plaintext';
1074
+ if (filename) {
1075
+ const ext = filename.split('.').pop().toLowerCase();
1076
+ const langMap = {
1077
+ 'js': 'javascript', 'ts': 'typescript', 'vue': 'html', 'jsx': 'javascript',
1078
+ 'tsx': 'typescript', 'py': 'python', 'rb': 'ruby', 'java': 'java',
1079
+ 'go': 'go', 'rs': 'rust', 'cpp': 'cpp', 'c': 'c', 'h': 'c',
1080
+ 'css': 'css', 'scss': 'scss', 'html': 'html', 'json': 'json',
1081
+ 'md': 'markdown', 'sh': 'bash', 'yml': 'yaml', 'yaml': 'yaml'
1082
+ };
1083
+ lang = langMap[ext] || 'plaintext';
1084
+ }
1085
+ let codeLines = [];
1086
+ for (const line of lines) {
1087
+ const match = line.match(/^\\s*\\d+[→|](.*)$/);
1088
+ if (match) {
1089
+ codeLines.push(match[1]);
1090
+ } else if (line.trim()) {
1091
+ codeLines.push(line);
1092
+ }
1093
+ }
1094
+ const codeContent = codeLines.join('\\n');
1095
+ const lineCount = codeLines.length;
1096
+ const preview = filename || (lineCount + ' lignes');
1097
+
1098
+ let html = '<details class="log-code-block">';
1099
+ html += '<summary class="log-code-header"><span class="code-lang">' + lang + '</span> ' + escapeHtml(preview) + '</summary>';
1100
+ html += '<pre><code class="language-' + lang + '">' + escapeHtml(codeContent) + '</code></pre>';
1101
+ html += '</details>';
1102
+ return html;
1103
+ }
1104
+
1105
+ function formatLogContent(rawContent) {
1106
+ if (!rawContent) return '';
1107
+ let cleanedContent = rawContent.replace(/<system-reminder>[\\s\\S]*?<\\/system-reminder>/gi, '');
1108
+ const lines = cleanedContent.split('\\n');
1109
+ const entries = [];
1110
+ let textBuffer = [];
1111
+
1112
+ function flushTextBuffer() {
1113
+ if (textBuffer.length > 0) {
1114
+ const text = textBuffer.join('\\n');
1115
+ entries.push('<div class="log-message-card"><div class="log-message-header assistant-header">Assistant</div><div class="log-message-body">' + renderMarkdown(text) + '</div></div>');
1116
+ textBuffer = [];
1117
+ }
1118
+ }
1119
+
1120
+ for (const line of lines) {
1121
+ if (!line.trim()) continue;
1122
+
1123
+ const timestampMatch = line.match(/^\\[(\\d{4}-\\d{2}-\\d{2}T[^\\]]+)\\]\\s*(.*)$/);
1124
+ if (timestampMatch) {
1125
+ flushTextBuffer();
1126
+ const date = new Date(timestampMatch[1]);
1127
+ const timeStr = date.toLocaleTimeString();
1128
+ entries.push('<div class="log-entry timestamp">' + timeStr + ' - ' + escapeHtml(timestampMatch[2]) + '</div>');
1129
+ continue;
1130
+ }
1131
+
1132
+ if (line.startsWith('[RAW] ')) {
1133
+ const raw = line.slice(6);
1134
+ if (!raw.startsWith('{')) continue;
1135
+
1136
+ const codeMatch = raw.match(/"content":"(\\s*\\d+[→|][^"]*)/);
1137
+ if (codeMatch) {
1138
+ const contentMatch = raw.match(/"content":"([^"]+)/);
1139
+ if (contentMatch) {
1140
+ const decoded = contentMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"');
1141
+ entries.push('<div class="log-message-card"><div class="log-message-header result-header">Tool Result</div><div class="log-message-body">' + formatCodeBlock(decoded) + '</div></div>');
1142
+ continue;
1143
+ }
1144
+ }
1145
+
1146
+ const textMatch = raw.match(/"type":"text","text":"([^"]+)"/);
1147
+ if (textMatch) {
1148
+ const decoded = textMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"');
1149
+ entries.push('<div class="log-message-card"><div class="log-message-header assistant-header">Assistant</div><div class="log-message-body">' + renderMarkdown(decoded) + '</div></div>');
1150
+ continue;
1151
+ }
1152
+
1153
+ const toolMatch = raw.match(/"type":"tool_use"[^}]*"name":"([^"]+)"/);
1154
+ if (toolMatch) {
1155
+ entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(toolMatch[1]) + '</span></div>');
1156
+ continue;
1157
+ }
1158
+ continue;
1159
+ }
1160
+
1161
+ if (line.startsWith('[SYSTEM]')) {
1162
+ entries.push('<div class="log-entry system"><span class="log-label">System</span><div class="log-content">' + escapeHtml(line.slice(9)) + '</div></div>');
1163
+ continue;
1164
+ }
1165
+ if (line.startsWith('[ASSISTANT]')) {
1166
+ entries.push('<div class="log-message-card"><div class="log-message-header assistant-header">Assistant</div><div class="log-message-body">' + renderMarkdown(line.slice(12)) + '</div></div>');
1167
+ continue;
1168
+ }
1169
+ if (line.startsWith('[TOOL]')) {
1170
+ entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(line.slice(7)) + '</span></div>');
1171
+ continue;
1172
+ }
1173
+ if (line.startsWith('[RESULT]')) {
1174
+ const content = line.slice(9);
1175
+ if (/^\\s*\\d+[→|]/.test(content)) {
1176
+ entries.push('<div class="log-message-card"><div class="log-message-header result-header">Result</div><div class="log-message-body">' + formatCodeBlock(content) + '</div></div>');
1177
+ } else {
1178
+ entries.push('<div class="log-message-card"><div class="log-message-header result-header">Result</div><div class="log-message-body">' + escapeHtml(content.slice(0, 1000)) + (content.length > 1000 ? '...' : '') + '</div></div>');
1179
+ }
1180
+ continue;
1181
+ }
1182
+ if (line.startsWith('[ERROR]')) {
1183
+ entries.push('<div class="log-entry error"><span class="log-label">Error</span><div class="log-content">' + escapeHtml(line.slice(8)) + '</div></div>');
1184
+ continue;
1185
+ }
1186
+
1187
+ entries.push('<div class="log-entry system">' + escapeHtml(line) + '</div>');
1188
+ }
1189
+
1190
+ return entries.join('');
1191
+ }
1192
+
1193
+ function resetClaudeLog() {
1194
+ stopLogPolling();
1195
+ document.getElementById('claude-log-section').style.display = 'none';
1196
+ document.getElementById('claude-log').innerHTML = '';
1197
+ document.getElementById('claude-log-status').className = 'claude-log-status';
1198
+ document.getElementById('claude-log-status').textContent = t('status.waiting');
1199
+ }
1200
+
1201
+ async function fetchLog(key) {
1202
+ try {
1203
+ const res = await fetch('/api/tickets/' + key + '/log');
1204
+ const json = await res.json();
1205
+ if (json.success && json.data) {
1206
+ const section = document.getElementById('claude-log-section');
1207
+ const log = document.getElementById('claude-log');
1208
+
1209
+ if (json.data.exists || json.data.content) {
1210
+ section.style.display = 'block';
1211
+ log.innerHTML = formatLogContent(json.data.content || '');
1212
+ log.scrollTop = log.scrollHeight;
1213
+ }
1214
+ }
1215
+ } catch (e) {
1216
+ console.error('Log fetch error:', e);
1217
+ }
1218
+ }
1219
+
1220
+ function onClaudeStart(ticket) {
1221
+ claudeProcessingTicket = ticket;
1222
+ claudeProcessingTickets.add(ticket);
1223
+ render();
1224
+
1225
+ const status = document.getElementById('claude-log-status');
1226
+ if (status) {
1227
+ status.className = 'claude-log-status processing';
1228
+ status.textContent = '\\u{1F916} ' + t('status.processing');
1229
+ }
1230
+ if (editingKey === ticket) {
1231
+ document.getElementById('claude-log-section').style.display = 'block';
1232
+ startLogPolling(ticket);
1233
+ }
1234
+ showNotification('claude', t('notify.claudeStarted'), ticket);
1235
+ }
1236
+
1237
+ function onClaudeStream(ticket) {
1238
+ if (editingKey === ticket) {
1239
+ fetchLog(ticket);
1240
+ }
1241
+ }
1242
+
1243
+ function onClaudeEnd(ticket, success, duration) {
1244
+ claudeProcessingTicket = null;
1245
+ claudeProcessingTickets.delete(ticket);
1246
+ render();
1247
+
1248
+ const status = document.getElementById('claude-log-status');
1249
+ if (status && editingKey === ticket) {
1250
+ if (success) {
1251
+ status.className = 'claude-log-status success';
1252
+ status.textContent = '\\u2705 ' + t('status.completed') + ' (' + (duration / 1000).toFixed(1) + 's)';
1253
+ } else {
1254
+ status.className = 'claude-log-status error';
1255
+ status.textContent = '\\u274C ' + t('status.failed');
1256
+ }
1257
+ fetchLog(ticket);
1258
+ stopLogPolling();
1259
+ }
1260
+ if (success) {
1261
+ showNotification('success', t('notify.claudeFinished'), ticket);
1262
+ } else {
1263
+ showNotification('error', t('notify.claudeFailed'), ticket);
1264
+ }
1265
+ }
1266
+
1267
+ // ========================================
1268
+ // API
1269
+ // ========================================
1270
+ async function loadTicketsFromAPI() {
1271
+ try {
1272
+ const res = await fetch('/api/tickets');
1273
+ const json = await res.json();
1274
+ if (json.success && json.data) {
1275
+ TICKETS.length = 0;
1276
+ (json.data.tickets || []).forEach(tk => TICKETS.push(tk));
1277
+ COLUMNS.length = 0;
1278
+ (json.data.columns || []).forEach(c => COLUMNS.push(c));
1279
+ render();
1280
+ }
1281
+ } catch (e) {
1282
+ console.error(t('notify.loadingError') + ':', e);
1283
+ }
1284
+ }
1285
+
1286
+ async function loadProcessingStatus() {
1287
+ try {
1288
+ const res = await fetch('/api/status');
1289
+ const json = await res.json();
1290
+ if (json.success && json.data && json.data.processingTickets) {
1291
+ claudeProcessingTickets = new Set(json.data.processingTickets);
1292
+ render();
1293
+ }
1294
+ } catch (e) {
1295
+ console.error('Failed to load processing status:', e);
1296
+ }
1297
+ }
1298
+
1299
+ function onTicketClick(key) {
1300
+ window.location.href = '/ticket/' + key;
1301
+ }
1302
+
1303
+ // ========================================
1304
+ // KEYBOARD
1305
+ // ========================================
1306
+ document.addEventListener('keydown', e => {
1307
+ if (e.key === 'Escape') {
1308
+ const actionModal = document.getElementById('action-modal');
1309
+ if (actionModal?.classList.contains('active')) {
1310
+ closeActionModal();
1311
+ return;
1312
+ }
1313
+ closeModal();
1314
+ }
1315
+ });
1316
+
1317
+ // ========================================
1318
+ // INIT
1319
+ // ========================================
1320
+ render();
1321
+ loadProcessingStatus();
1322
+ connectWebSocket();
1323
+ `;
1324
+ }
1325
+ //# sourceMappingURL=index.js.map