@autocode-cli/autocode 0.0.43 → 0.1.4

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 (106) hide show
  1. package/LICENSE +5 -5
  2. package/LICENSE.fr.md +203 -0
  3. package/README.md +48 -15
  4. package/dist/cli/parser.d.ts +1 -1
  5. package/dist/cli/parser.d.ts.map +1 -1
  6. package/dist/cli/parser.js +5 -4
  7. package/dist/cli/parser.js.map +1 -1
  8. package/dist/index.js +4 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/server/api.d.ts.map +1 -1
  11. package/dist/server/api.js +47 -7
  12. package/dist/server/api.js.map +1 -1
  13. package/dist/server/dashboard/index.d.ts +10 -0
  14. package/dist/server/dashboard/index.d.ts.map +1 -0
  15. package/dist/server/dashboard/index.js +16 -0
  16. package/dist/server/dashboard/index.js.map +1 -0
  17. package/dist/server/dashboard/pages/column-edit.d.ts +8 -0
  18. package/dist/server/dashboard/pages/column-edit.d.ts.map +1 -0
  19. package/dist/server/dashboard/pages/column-edit.js +303 -0
  20. package/dist/server/dashboard/pages/column-edit.js.map +1 -0
  21. package/dist/server/dashboard/pages/column-prompt.d.ts +8 -0
  22. package/dist/server/dashboard/pages/column-prompt.d.ts.map +1 -0
  23. package/dist/server/dashboard/pages/column-prompt.js +228 -0
  24. package/dist/server/dashboard/pages/column-prompt.js.map +1 -0
  25. package/dist/server/dashboard/pages/column-terminal.d.ts +8 -0
  26. package/dist/server/dashboard/pages/column-terminal.d.ts.map +1 -0
  27. package/dist/server/dashboard/pages/column-terminal.js +529 -0
  28. package/dist/server/dashboard/pages/column-terminal.js.map +1 -0
  29. package/dist/server/dashboard/pages/index.d.ts +12 -0
  30. package/dist/server/dashboard/pages/index.d.ts.map +1 -0
  31. package/dist/server/dashboard/pages/index.js +12 -0
  32. package/dist/server/dashboard/pages/index.js.map +1 -0
  33. package/dist/server/dashboard/pages/main-dashboard.d.ts +11 -0
  34. package/dist/server/dashboard/pages/main-dashboard.d.ts.map +1 -0
  35. package/dist/server/dashboard/pages/main-dashboard.js +209 -0
  36. package/dist/server/dashboard/pages/main-dashboard.js.map +1 -0
  37. package/dist/server/dashboard/pages/shared.d.ts +8 -0
  38. package/dist/server/dashboard/pages/shared.d.ts.map +1 -0
  39. package/dist/server/dashboard/pages/shared.js +58 -0
  40. package/dist/server/dashboard/pages/shared.js.map +1 -0
  41. package/dist/server/dashboard/pages/ticket-view.d.ts +8 -0
  42. package/dist/server/dashboard/pages/ticket-view.d.ts.map +1 -0
  43. package/dist/server/dashboard/pages/ticket-view.js +1364 -0
  44. package/dist/server/dashboard/pages/ticket-view.js.map +1 -0
  45. package/dist/server/dashboard/scripts/index.d.ts +11 -0
  46. package/dist/server/dashboard/scripts/index.d.ts.map +1 -0
  47. package/dist/server/dashboard/scripts/index.js +1325 -0
  48. package/dist/server/dashboard/scripts/index.js.map +1 -0
  49. package/dist/server/dashboard/styles/base.d.ts +5 -0
  50. package/dist/server/dashboard/styles/base.d.ts.map +1 -0
  51. package/dist/server/dashboard/styles/base.js +110 -0
  52. package/dist/server/dashboard/styles/base.js.map +1 -0
  53. package/dist/server/dashboard/styles/board.d.ts +5 -0
  54. package/dist/server/dashboard/styles/board.d.ts.map +1 -0
  55. package/dist/server/dashboard/styles/board.js +168 -0
  56. package/dist/server/dashboard/styles/board.js.map +1 -0
  57. package/dist/server/dashboard/styles/comments.d.ts +5 -0
  58. package/dist/server/dashboard/styles/comments.d.ts.map +1 -0
  59. package/dist/server/dashboard/styles/comments.js +249 -0
  60. package/dist/server/dashboard/styles/comments.js.map +1 -0
  61. package/dist/server/dashboard/styles/components.d.ts +5 -0
  62. package/dist/server/dashboard/styles/components.d.ts.map +1 -0
  63. package/dist/server/dashboard/styles/components.js +190 -0
  64. package/dist/server/dashboard/styles/components.js.map +1 -0
  65. package/dist/server/dashboard/styles/footer.d.ts +5 -0
  66. package/dist/server/dashboard/styles/footer.d.ts.map +1 -0
  67. package/dist/server/dashboard/styles/footer.js +32 -0
  68. package/dist/server/dashboard/styles/footer.js.map +1 -0
  69. package/dist/server/dashboard/styles/index.d.ts +8 -0
  70. package/dist/server/dashboard/styles/index.d.ts.map +1 -0
  71. package/dist/server/dashboard/styles/index.js +27 -0
  72. package/dist/server/dashboard/styles/index.js.map +1 -0
  73. package/dist/server/dashboard/styles/logs.d.ts +5 -0
  74. package/dist/server/dashboard/styles/logs.d.ts.map +1 -0
  75. package/dist/server/dashboard/styles/logs.js +89 -0
  76. package/dist/server/dashboard/styles/logs.js.map +1 -0
  77. package/dist/server/dashboard/styles/notifications.d.ts +5 -0
  78. package/dist/server/dashboard/styles/notifications.d.ts.map +1 -0
  79. package/dist/server/dashboard/styles/notifications.js +51 -0
  80. package/dist/server/dashboard/styles/notifications.js.map +1 -0
  81. package/dist/server/dashboard/styles/variables.d.ts +5 -0
  82. package/dist/server/dashboard/styles/variables.d.ts.map +1 -0
  83. package/dist/server/dashboard/styles/variables.js +29 -0
  84. package/dist/server/dashboard/styles/variables.js.map +1 -0
  85. package/dist/server/dashboard/utils.d.ts +8 -0
  86. package/dist/server/dashboard/utils.d.ts.map +1 -0
  87. package/dist/server/dashboard/utils.js +14 -0
  88. package/dist/server/dashboard/utils.js.map +1 -0
  89. package/dist/server/dashboard.d.ts +5 -21
  90. package/dist/server/dashboard.d.ts.map +1 -1
  91. package/dist/server/dashboard.js +5 -4843
  92. package/dist/server/dashboard.js.map +1 -1
  93. package/dist/server/websocket.d.ts +12 -0
  94. package/dist/server/websocket.d.ts.map +1 -1
  95. package/dist/server/websocket.js +19 -0
  96. package/dist/server/websocket.js.map +1 -1
  97. package/dist/services/claude.d.ts.map +1 -1
  98. package/dist/services/claude.js +4 -1
  99. package/dist/services/claude.js.map +1 -1
  100. package/dist/utils/config.d.ts +1 -1
  101. package/dist/utils/config.js +1 -1
  102. package/dist/utils/version-check.d.ts +26 -0
  103. package/dist/utils/version-check.d.ts.map +1 -0
  104. package/dist/utils/version-check.js +234 -0
  105. package/dist/utils/version-check.js.map +1 -0
  106. package/package.json +2 -2
@@ -1,4846 +1,8 @@
1
1
  /**
2
- * Dashboard HTML generation - Full feature parity with legacy autocode.sh
2
+ * Dashboard HTML generation
3
+ *
4
+ * This is a wrapper module that re-exports from the modular structure.
5
+ * See ./dashboard/ for the module organization.
3
6
  */
4
- import { createRequire } from 'module';
5
- import { getColumns } from '../core/column.js';
6
- import { listTickets, getTicket } from '../core/ticket.js';
7
- import { getWorkflowSummary } from '../core/workflow.js';
8
- import { getConfig } from '../utils/config.js';
9
- const require = createRequire(import.meta.url);
10
- const pkg = require('../../package.json');
11
- /**
12
- * Generate the full dashboard HTML
13
- */
14
- export function generateDashboard() {
15
- const config = getConfig();
16
- const tickets = listTickets(config.root);
17
- const columns = getColumns();
18
- const summary = getWorkflowSummary(tickets);
19
- // Prepare data for client-side rendering
20
- const columnsJson = JSON.stringify(columns);
21
- const ticketsJson = JSON.stringify(tickets);
22
- const timestamp = new Date().toLocaleString('en-US');
23
- return `<!DOCTYPE html>
24
- <html lang="en">
25
- <head>
26
- <meta charset="UTF-8">
27
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
28
- <title>AutoCode Dashboard</title>
29
- <style>
30
- ${getStyles()}
31
- </style>
32
- </head>
33
- <body>
34
- <div id="notifications" class="notifications"></div>
35
-
36
- <header>
37
- <div class="header-logo">
38
- <a href="#" class="logo-link"><span>⚡</span> AutoCode</a>
39
- <span class="header-title">Dashboard</span>
40
- </div>
41
- <div class="stats" id="stats"></div>
42
- <div class="filters">
43
- <select id="filter-priority">
44
- <option value="" data-i18n="filter.allPriorities">All priorities</option>
45
- <option value="P0">P0</option>
46
- <option value="P1">P1</option>
47
- <option value="P2">P2</option>
48
- <option value="P3">P3</option>
49
- </select>
50
- <input type="text" id="filter-search" placeholder="Search..." data-i18n-placeholder="filter.search">
51
- <div class="lang-switcher" id="lang-switcher">
52
- <button class="lang-btn" data-lang="en">EN</button>
53
- <button class="lang-btn" data-lang="fr">FR</button>
54
- </div>
55
- <button class="btn btn-primary" onclick="openModal()" data-i18n="btn.newTicket">+ New ticket</button>
56
- </div>
57
- </header>
58
-
59
- <main class="board" id="board"></main>
60
-
61
- <!-- Modal Ticket (create/edit) -->
62
- <div class="modal-overlay" id="modal-overlay">
63
- <div class="modal">
64
- <div class="modal-header">
65
- <h2 id="modal-title" data-i18n="modal.newTicket">New ticket</h2>
66
- <button class="modal-close" onclick="closeModal()">&times;</button>
67
- </div>
68
- <div class="modal-body">
69
- <div class="form-group">
70
- <label for="ticket-title" data-i18n="modal.title">Title *</label>
71
- <input type="text" id="ticket-title" placeholder="E.g.: Fix the login bug" data-i18n-placeholder="modal.titlePlaceholder">
72
- </div>
73
- <div class="form-group">
74
- <label for="ticket-description" data-i18n="modal.description">Description</label>
75
- <textarea id="ticket-description" placeholder="Describe the context and details..." data-i18n-placeholder="modal.descriptionPlaceholder"></textarea>
76
- </div>
77
- <div class="form-row">
78
- <div class="form-group">
79
- <label for="ticket-priority" data-i18n="modal.priority">Priority</label>
80
- <select id="ticket-priority">
81
- <option value="P2" data-i18n="priority.p2">P2 - Normal</option>
82
- <option value="P0" data-i18n="priority.p0">P0 - Critical</option>
83
- <option value="P1" data-i18n="priority.p1">P1 - High</option>
84
- <option value="P3" data-i18n="priority.p3">P3 - Low</option>
85
- </select>
86
- </div>
87
- <div class="form-group">
88
- <label for="ticket-semver" data-i18n="modal.releaseType">Release type</label>
89
- <select id="ticket-semver">
90
- <option value="patch" data-i18n="semver.patch">Patch (x.x.X) - Bug fix</option>
91
- <option value="minor" data-i18n="semver.minor">Minor (x.X.0) - Feature</option>
92
- <option value="major" data-i18n="semver.major">Major (X.0.0) - Breaking</option>
93
- <option value="none" data-i18n="semver.none">No deployment</option>
94
- </select>
95
- </div>
96
- </div>
97
- <div class="form-group labels-select">
98
- <label for="ticket-labels" data-i18n="modal.labels">Labels</label>
99
- <select id="ticket-labels" onchange="addLabel(this)">
100
- <option value="" data-i18n="modal.selectLabel">Select a label...</option>
101
- <option value="bug">bug</option>
102
- <option value="feature">feature</option>
103
- <option value="enhancement">enhancement</option>
104
- <option value="documentation">documentation</option>
105
- <option value="urgent">urgent</option>
106
- <option value="refactoring">refactoring</option>
107
- <option value="security">security</option>
108
- <option value="performance">performance</option>
109
- <option value="ui">ui</option>
110
- <option value="api">api</option>
111
- </select>
112
- <div class="selected-labels" id="selected-labels"></div>
113
- </div>
114
- <div class="form-group">
115
- <label data-i18n="modal.acceptanceCriteria">Acceptance criteria</label>
116
- <div class="criteria-list" id="criteria-list"></div>
117
- <button type="button" class="btn-add" onclick="addCriteria()">
118
- <span>+</span> <span data-i18n="modal.addCriteria">Add criteria</span>
119
- </button>
120
- </div>
121
- <div class="attachments-section" id="attachments-section" style="display:none">
122
- <div class="attachments-header">
123
- <h3 data-i18n="modal.attachments">Attachments</h3>
124
- <span class="attachments-count" id="attachments-count">0</span>
125
- </div>
126
- <div class="attachments-list" id="attachments-list"></div>
127
- <div class="attachments-upload">
128
- <input type="file" id="file-input" multiple style="display:none" onchange="uploadFiles(this.files)">
129
- <button type="button" class="btn-add" onclick="document.getElementById('file-input').click()">
130
- <span>📎</span> <span data-i18n="modal.addAttachment">Add file</span>
131
- </button>
132
- </div>
133
- </div>
134
- <div class="comments-section" id="comments-section" style="display:none">
135
- <div class="comments-header">
136
- <h3 data-i18n="modal.comments">Comments</h3>
137
- <span class="comments-count" id="comments-count">0</span>
138
- </div>
139
- <div class="comments-list" id="comments-list">
140
- <div class="no-comments" data-i18n="modal.noComments">No comments</div>
141
- </div>
142
- <div class="add-comment">
143
- <textarea id="new-comment" placeholder="Add a comment..." data-i18n-placeholder="modal.addCommentPlaceholder"></textarea>
144
- <button class="btn-comment" onclick="addComment()" data-i18n="btn.add">Add</button>
145
- </div>
146
- </div>
147
- <div class="claude-log-section" id="claude-log-section" style="display:none">
148
- <div class="claude-log-header">
149
- <h3 data-i18n="modal.claudeTerminal">Claude Terminal</h3>
150
- <span class="claude-log-status" id="claude-log-status" data-i18n="status.waiting">Waiting</span>
151
- </div>
152
- <pre class="claude-log" id="claude-log"></pre>
153
- </div>
154
- <div class="modal-actions">
155
- <button class="btn btn-secondary" onclick="closeModal()" data-i18n="btn.cancel">Cancel</button>
156
- <button class="btn btn-danger" id="btn-archive" style="display:none" onclick="archiveTicket()" data-i18n="btn.archive">Archive</button>
157
- <button class="btn btn-next" id="btn-next" style="display:none" onclick="advanceTicket()" data-i18n="btn.next">Next</button>
158
- <button class="btn btn-primary" id="btn-save" onclick="saveTicket()" data-i18n="btn.createTicket">Create ticket</button>
159
- </div>
160
- </div>
161
- </div>
162
- </div>
163
-
164
- <!-- Modal ACTION.md -->
165
- <div class="modal-overlay" id="action-modal">
166
- <div class="modal action-modal">
167
- <div class="modal-header">
168
- <div>
169
- <span class="pill">ACTION.md</span>
170
- <h2 id="action-modal-title" data-i18n="action.instructions">Instructions</h2>
171
- </div>
172
- <button class="modal-close" onclick="closeActionModal()">&times;</button>
173
- </div>
174
- <div class="modal-body">
175
- <div class="action-toolbar">
176
- <button class="btn btn-secondary" onclick="reloadActionContent()" data-i18n="btn.reload">Reload</button>
177
- <button class="btn btn-secondary" id="action-edit-btn" onclick="enterActionEdit()" data-i18n="btn.edit">Edit</button>
178
- <button class="btn btn-primary" id="action-save-btn" onclick="saveActionContent()" disabled data-i18n="btn.save">Save</button>
179
- </div>
180
- <div class="action-meta" id="action-meta"></div>
181
- <div class="action-empty" id="action-empty" style="display:none"></div>
182
- <textarea id="action-content" readonly placeholder="No instructions available" data-i18n-placeholder="action.noInstructions"></textarea>
183
- </div>
184
- </div>
185
- </div>
186
-
187
- <!-- Context menu -->
188
- <div class="context-menu" id="context-menu">
189
- <div class="context-menu-item" onclick="editFromContext()" data-i18n="btn.edit">Edit</div>
190
- <div class="context-menu-item danger" onclick="archiveFromContext()" data-i18n="btn.archive">Archive</div>
191
- </div>
192
-
193
- <footer>
194
- <span>AutoCode v${pkg.version} | <span id="time">${timestamp}</span> | <code title="autodoc root">${config.root}</code></span>
195
- </footer>
196
-
197
- <script>
198
- const COLUMNS = ${columnsJson};
199
- const TICKETS = ${ticketsJson};
200
- ${getScript()}
201
- </script>
202
- </body>
203
- </html>`;
204
- }
205
- /**
206
- * Escape HTML entities
207
- */
208
- function escapeHtml(text) {
209
- return text
210
- .replace(/&/g, '&amp;')
211
- .replace(/</g, '&lt;')
212
- .replace(/>/g, '&gt;')
213
- .replace(/"/g, '&quot;');
214
- }
215
- /**
216
- * Get CSS styles
217
- */
218
- function getStyles() {
219
- return `
220
- :root {
221
- --bg-primary: #0a0a0f;
222
- --bg-secondary: #12121a;
223
- --bg-tertiary: #1a1a25;
224
- --bg: #0a0a0f;
225
- --card: #12121a;
226
- --border: #2a2a3a;
227
- --text: #f1f5f9;
228
- --text-primary: #f1f5f9;
229
- --text-secondary: #94a3b8;
230
- --muted: #64748b;
231
- --accent: #6366f1;
232
- --accent-light: #818cf8;
233
- --accent-hover: #818cf8;
234
- --accent-glow: rgba(99, 102, 241, 0.3);
235
- --blue: #3b82f6;
236
- --green: #22c55e;
237
- --yellow: #f59e0b;
238
- --red: #ef4444;
239
- --gradient-1: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);
240
- }
241
-
242
- * { box-sizing: border-box; margin: 0; padding: 0; }
243
-
244
- body {
245
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
246
- background: var(--bg);
247
- color: var(--text);
248
- min-height: 100vh;
249
- }
250
-
251
- /* Header */
252
- header {
253
- background: var(--bg-secondary);
254
- padding: 12px 24px;
255
- border-bottom: 1px solid var(--border);
256
- display: flex;
257
- justify-content: space-between;
258
- align-items: center;
259
- flex-wrap: wrap;
260
- gap: 12px;
261
- height: 60px;
262
- }
263
-
264
- .header-logo {
265
- display: flex;
266
- align-items: center;
267
- gap: 12px;
268
- }
269
-
270
- .logo-link {
271
- font-size: 1.25rem;
272
- font-weight: 700;
273
- background: var(--gradient-1);
274
- -webkit-background-clip: text;
275
- -webkit-text-fill-color: transparent;
276
- background-clip: text;
277
- text-decoration: none;
278
- display: flex;
279
- align-items: center;
280
- gap: 8px;
281
- }
282
-
283
- .header-title {
284
- color: var(--text-secondary);
285
- font-size: 0.9rem;
286
- padding-left: 12px;
287
- border-left: 1px solid var(--border);
288
- }
289
-
290
- h1 { font-size: 20px; font-weight: 600; color: var(--accent); }
291
-
292
- .stats { display: flex; gap: 8px; flex-wrap: wrap; }
293
- .stat {
294
- background: var(--bg-tertiary);
295
- padding: 6px 10px;
296
- border-radius: 6px;
297
- font-size: 13px;
298
- }
299
- .stat.connected { color: var(--green); }
300
- .stat.P0 { color: var(--red); }
301
- .stat.P1 { color: var(--yellow); }
302
- .stat.P2 { color: var(--blue); }
303
-
304
- .filters { display: flex; gap: 8px; align-items: center; }
305
-
306
- select, input, textarea {
307
- background: var(--bg-primary);
308
- border: 1px solid var(--border);
309
- color: var(--text);
310
- padding: 6px 10px;
311
- border-radius: 8px;
312
- font-size: 13px;
313
- transition: border-color 0.3s;
314
- }
315
- select:focus, input:focus, textarea:focus {
316
- outline: none;
317
- border-color: var(--accent);
318
- }
319
-
320
- .lang-switcher {
321
- display: flex;
322
- gap: 4px;
323
- }
324
- .lang-switcher .lang-btn {
325
- background: var(--bg-primary);
326
- border: 1px solid var(--border);
327
- color: var(--text-muted);
328
- padding: 6px 12px;
329
- border-radius: 4px;
330
- cursor: pointer;
331
- font-size: 13px;
332
- font-weight: 600;
333
- transition: all 0.2s;
334
- }
335
- .lang-switcher .lang-btn:hover:not(.active) {
336
- border-color: var(--accent);
337
- color: var(--text);
338
- }
339
- .lang-switcher .lang-btn.active {
340
- background: var(--accent);
341
- color: white;
342
- border-color: var(--accent);
343
- }
344
-
345
- .btn {
346
- padding: 10px 24px;
347
- border: none;
348
- border-radius: 8px;
349
- font-size: 0.9rem;
350
- font-weight: 600;
351
- cursor: pointer;
352
- transition: all 0.3s;
353
- }
354
- .btn-primary { background: var(--accent); color: #fff; }
355
- .btn-primary:hover { background: var(--accent-light); transform: translateY(-2px); }
356
- .btn-secondary { background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border); }
357
- .btn-secondary:hover { border-color: var(--accent); }
358
- .btn-danger { background: var(--red); color: #fff; }
359
- .btn-next { background: var(--blue); color: #fff; }
360
-
361
- /* Board */
362
- .board {
363
- display: flex;
364
- gap: 16px;
365
- padding: 20px;
366
- overflow-x: auto;
367
- height: calc(100vh - 60px - 50px);
368
- padding-bottom: 10px;
369
- }
370
-
371
- .column {
372
- min-width: 300px;
373
- max-width: 350px;
374
- flex: 1;
375
- background: var(--bg-secondary);
376
- border: 1px solid var(--border);
377
- border-radius: 12px;
378
- display: flex;
379
- flex-direction: column;
380
- transition: border-color 0.2s;
381
- }
382
- .column.drag-over {
383
- border-color: var(--accent);
384
- background: rgba(99, 102, 241, 0.1);
385
- }
386
-
387
- .column-header {
388
- padding: 16px;
389
- display: flex;
390
- justify-content: space-between;
391
- align-items: center;
392
- border-bottom: 1px solid var(--border);
393
- }
394
- .column-title { font-size: 14px; font-weight: 600; }
395
- .column-header-actions { display: flex; gap: 10px; align-items: center; }
396
- .column-count {
397
- background: var(--bg-tertiary);
398
- color: var(--text-secondary);
399
- padding: 2px 10px;
400
- border-radius: 10px;
401
- font-size: 0.85rem;
402
- font-weight: 600;
403
- }
404
- .btn-action {
405
- background: none;
406
- border: none;
407
- cursor: pointer;
408
- font-size: 16px;
409
- padding: 4px;
410
- opacity: 0.6;
411
- transition: opacity 0.3s;
412
- text-decoration: none;
413
- display: inline-block;
414
- }
415
- .btn-action:hover { opacity: 1; }
416
-
417
- .column-body {
418
- padding: 12px;
419
- flex: 1;
420
- overflow-y: auto;
421
- display: flex;
422
- flex-direction: column;
423
- gap: 10px;
424
- }
425
-
426
- .empty {
427
- text-align: center;
428
- color: var(--muted);
429
- padding: 24px;
430
- font-size: 13px;
431
- }
432
-
433
- /* Tickets */
434
- .ticket {
435
- background: var(--bg-primary);
436
- border: 1px solid var(--border);
437
- border-radius: 8px;
438
- padding: 14px;
439
- cursor: pointer;
440
- transition: all 0.3s ease;
441
- }
442
- .ticket:hover {
443
- border-color: var(--accent);
444
- transform: translateY(-2px);
445
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
446
- }
447
- .ticket.dragging {
448
- opacity: 0.5;
449
- transform: rotate(3deg) scale(1.05);
450
- box-shadow: 0 15px 40px var(--accent-glow);
451
- z-index: 100;
452
- }
453
-
454
- .ticket-key {
455
- font-family: 'JetBrains Mono', monospace;
456
- font-size: 0.8rem;
457
- color: var(--accent-light);
458
- font-weight: 600;
459
- margin-bottom: 4px;
460
- }
461
- .ticket-title {
462
- font-size: 0.95rem;
463
- font-weight: 500;
464
- line-height: 1.4;
465
- margin-bottom: 10px;
466
- }
467
- .ticket-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
468
-
469
- .priority {
470
- font-size: 0.7rem;
471
- font-weight: 700;
472
- padding: 3px 8px;
473
- border-radius: 4px;
474
- }
475
- .priority.P0 { background: #ef444420; color: #ef4444; }
476
- .priority.P1 { background: #f59e0b20; color: #f59e0b; }
477
- .priority.P2 { background: #3b82f620; color: #3b82f6; }
478
- .priority.P3 { background: #64748b20; color: #64748b; }
479
-
480
- /* Claude Processing Indicator */
481
- .ticket.claude-processing {
482
- border-color: #a855f7;
483
- animation: claudePulse 2s ease-in-out infinite;
484
- box-shadow: 0 0 20px rgba(168, 85, 247, 0.3);
485
- }
486
-
487
- @keyframes claudePulse {
488
- 0%, 100% {
489
- box-shadow: 0 0 10px rgba(168, 85, 247, 0.2);
490
- border-color: #a855f7;
491
- }
492
- 50% {
493
- box-shadow: 0 0 25px rgba(168, 85, 247, 0.5);
494
- border-color: #c084fc;
495
- }
496
- }
497
-
498
- .ticket-claude-indicator {
499
- margin-top: 10px;
500
- padding-top: 10px;
501
- border-top: 1px solid var(--border);
502
- display: flex;
503
- align-items: center;
504
- gap: 8px;
505
- font-size: 0.8rem;
506
- color: #a855f7;
507
- }
508
-
509
- .claude-dot {
510
- width: 8px;
511
- height: 8px;
512
- background: #a855f7;
513
- border-radius: 50%;
514
- animation: claudeDotPulse 1.5s ease infinite;
515
- }
516
-
517
- @keyframes claudeDotPulse {
518
- 0%, 100% { opacity: 1; transform: scale(1); }
519
- 50% { opacity: 0.5; transform: scale(1.3); }
520
- }
521
-
522
- /* Modal */
523
- .modal-overlay {
524
- position: fixed;
525
- inset: 0;
526
- background: rgba(0,0,0,0.8);
527
- backdrop-filter: blur(5px);
528
- z-index: 1000;
529
- display: none;
530
- overflow-y: auto;
531
- }
532
- .modal-overlay.active { display: block; }
533
-
534
- .modal {
535
- width: 100%;
536
- min-height: 100vh;
537
- padding: 24px 48px;
538
- max-width: 100%;
539
- margin: 0;
540
- background: var(--bg-secondary);
541
- }
542
-
543
- .modal-header {
544
- display: flex;
545
- justify-content: space-between;
546
- align-items: center;
547
- margin-bottom: 24px;
548
- padding-bottom: 16px;
549
- border-bottom: 1px solid var(--border);
550
- }
551
- .modal-header h2 { font-size: 1.1rem; font-weight: 600; }
552
- .modal-close {
553
- background: none;
554
- border: none;
555
- color: var(--text-secondary);
556
- font-size: 1.5rem;
557
- cursor: pointer;
558
- line-height: 1;
559
- padding: 0;
560
- }
561
- .modal-close:hover { color: var(--text-primary); }
562
-
563
- .modal-body { display: flex; flex-direction: column; gap: 16px; }
564
-
565
- .form-group { display: flex; flex-direction: column; gap: 6px; }
566
- .form-group label { font-size: 13px; color: var(--muted); font-weight: 500; }
567
- .form-group input, .form-group textarea, .form-group select {
568
- padding: 10px 12px;
569
- font-size: 14px;
570
- }
571
- .form-group textarea { min-height: 100px; resize: vertical; }
572
-
573
- .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
574
-
575
- .labels-select .selected-labels {
576
- display: flex;
577
- flex-wrap: wrap;
578
- gap: 6px;
579
- margin-top: 8px;
580
- }
581
- .label-tag {
582
- background: #21262d;
583
- padding: 4px 8px;
584
- border-radius: 4px;
585
- font-size: 12px;
586
- display: flex;
587
- align-items: center;
588
- gap: 6px;
589
- }
590
- .remove-label {
591
- cursor: pointer;
592
- opacity: 0.6;
593
- }
594
- .remove-label:hover { opacity: 1; color: var(--red); }
595
-
596
- .criteria-list { display: flex; flex-direction: column; gap: 8px; }
597
- .criteria-item {
598
- display: flex;
599
- gap: 8px;
600
- align-items: center;
601
- }
602
- .criteria-item input { flex: 1; }
603
- .btn-remove {
604
- background: none;
605
- border: none;
606
- color: var(--red);
607
- font-size: 18px;
608
- cursor: pointer;
609
- }
610
- .btn-add {
611
- background: none;
612
- border: 1px dashed var(--border);
613
- color: var(--muted);
614
- padding: 8px 12px;
615
- border-radius: 6px;
616
- cursor: pointer;
617
- font-size: 13px;
618
- margin-top: 8px;
619
- }
620
- .btn-add:hover { border-color: var(--accent); color: var(--accent); }
621
-
622
- .comments-section {
623
- border-top: 1px solid var(--border);
624
- padding-top: 16px;
625
- margin-top: 8px;
626
- }
627
- .comments-header {
628
- display: flex;
629
- align-items: center;
630
- gap: 8px;
631
- margin-bottom: 12px;
632
- }
633
- .comments-header h3 { font-size: 14px; font-weight: 600; }
634
- .comments-count {
635
- background: var(--accent);
636
- color: #fff;
637
- padding: 2px 8px;
638
- border-radius: 10px;
639
- font-size: 11px;
640
- font-weight: 600;
641
- }
642
- .comments-list {
643
- display: flex;
644
- flex-direction: column;
645
- gap: 10px;
646
- }
647
- .no-comments {
648
- color: var(--muted);
649
- font-size: 13px;
650
- text-align: center;
651
- padding: 24px;
652
- background: var(--bg);
653
- border-radius: 8px;
654
- border: 1px dashed var(--border);
655
- }
656
- .comment {
657
- background: var(--bg);
658
- border-radius: 8px;
659
- border-left: 3px solid var(--accent);
660
- padding: 12px 14px;
661
- transition: all 0.2s ease;
662
- }
663
- .comment:hover {
664
- border-left-color: var(--blue);
665
- }
666
- .comment-meta {
667
- display: flex;
668
- align-items: center;
669
- flex-wrap: wrap;
670
- gap: 8px;
671
- cursor: pointer;
672
- user-select: none;
673
- }
674
- .comment-meta::before {
675
- content: '▶';
676
- font-size: 10px;
677
- color: var(--muted);
678
- transition: transform 0.2s ease;
679
- }
680
- .comment.expanded .comment-meta::before {
681
- transform: rotate(90deg);
682
- }
683
- .comment-text {
684
- max-height: 0;
685
- overflow: hidden;
686
- transition: max-height 0.3s ease, margin-top 0.3s ease, padding-top 0.3s ease;
687
- margin-top: 0;
688
- padding-top: 0;
689
- border-top: none;
690
- }
691
- .comment.expanded .comment-text {
692
- max-height: 500px;
693
- margin-top: 8px;
694
- padding-top: 8px;
695
- border-top: 1px solid var(--border);
696
- }
697
- .comment-column {
698
- font-size: 10px;
699
- font-weight: 600;
700
- text-transform: uppercase;
701
- letter-spacing: 0.5px;
702
- color: var(--blue);
703
- padding: 3px 8px;
704
- background: rgba(77,171,247,0.15);
705
- border-radius: 4px;
706
- }
707
- .comment-source {
708
- font-size: 10px;
709
- padding: 3px 8px;
710
- border-radius: 4px;
711
- text-transform: uppercase;
712
- font-weight: 600;
713
- letter-spacing: 0.5px;
714
- }
715
- .comment-source.user {
716
- background: #3b82f6;
717
- color: white;
718
- }
719
- .comment-source.claude {
720
- background: #8b5cf6;
721
- color: white;
722
- }
723
- .comment-date {
724
- font-size: 11px;
725
- color: var(--muted);
726
- display: flex;
727
- align-items: center;
728
- gap: 4px;
729
- }
730
- .comment-date::before {
731
- content: '🕐';
732
- font-size: 10px;
733
- }
734
- .comment-text {
735
- font-size: 13px;
736
- line-height: 1.6;
737
- color: var(--fg);
738
- }
739
- .comment-text h1, .comment-text h2, .comment-text h3, .comment-text h4 {
740
- margin: 12px 0 6px;
741
- font-weight: 600;
742
- color: var(--fg);
743
- }
744
- .comment-text h1 { font-size: 16px; }
745
- .comment-text h2 { font-size: 15px; }
746
- .comment-text h3 { font-size: 14px; }
747
- .comment-text p { margin: 6px 0; }
748
- .comment-text code {
749
- background: #21262d;
750
- padding: 2px 6px;
751
- border-radius: 4px;
752
- font-size: 12px;
753
- font-family: 'JetBrains Mono', monospace;
754
- }
755
- .comment-text pre {
756
- background: #21262d;
757
- padding: 10px 12px;
758
- border-radius: 6px;
759
- overflow-x: auto;
760
- margin: 8px 0;
761
- }
762
- .comment-text pre code {
763
- background: none;
764
- padding: 0;
765
- }
766
- .comment-text ul, .comment-text ol {
767
- margin: 6px 0 6px 20px;
768
- }
769
- .comment-text li { margin: 4px 0; }
770
- .comment-text blockquote {
771
- border-left: 3px solid var(--accent);
772
- padding-left: 12px;
773
- margin: 8px 0;
774
- color: var(--muted);
775
- font-style: italic;
776
- }
777
- .comment-text a {
778
- color: var(--blue);
779
- text-decoration: none;
780
- }
781
- .comment-text a:hover { text-decoration: underline; }
782
- .comment-text strong { font-weight: 600; }
783
- .comment-text em { font-style: italic; }
784
-
785
- .add-comment { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
786
- .add-comment textarea { min-height: 60px; }
787
- .btn-comment {
788
- align-self: flex-end;
789
- background: var(--accent);
790
- color: #fff;
791
- border: none;
792
- padding: 8px 16px;
793
- border-radius: 6px;
794
- cursor: pointer;
795
- font-size: 13px;
796
- }
797
-
798
- /* Attachments Section */
799
- .attachments-section {
800
- border-top: 1px solid var(--border);
801
- padding-top: 16px;
802
- margin-top: 8px;
803
- }
804
- .attachments-header {
805
- display: flex;
806
- align-items: center;
807
- gap: 8px;
808
- margin-bottom: 12px;
809
- }
810
- .attachments-header h3 { font-size: 14px; font-weight: 600; }
811
- .attachments-count {
812
- background: var(--blue);
813
- color: #fff;
814
- padding: 2px 8px;
815
- border-radius: 10px;
816
- font-size: 11px;
817
- font-weight: 600;
818
- }
819
- .attachments-list {
820
- display: flex;
821
- flex-wrap: wrap;
822
- gap: 8px;
823
- margin-bottom: 12px;
824
- }
825
- .attachment-item {
826
- background: var(--bg);
827
- border: 1px solid var(--border);
828
- border-radius: 6px;
829
- padding: 8px 12px;
830
- display: flex;
831
- align-items: center;
832
- gap: 8px;
833
- font-size: 13px;
834
- }
835
- .attachment-item:hover { border-color: var(--accent); }
836
- .attachment-icon { font-size: 16px; }
837
- .attachment-name {
838
- max-width: 150px;
839
- overflow: hidden;
840
- text-overflow: ellipsis;
841
- white-space: nowrap;
842
- }
843
- .attachment-delete {
844
- background: none;
845
- border: none;
846
- color: var(--red);
847
- cursor: pointer;
848
- padding: 2px;
849
- font-size: 14px;
850
- opacity: 0.7;
851
- }
852
- .attachment-delete:hover { opacity: 1; }
853
- .attachments-upload { margin-top: 8px; }
854
- .no-attachments {
855
- color: var(--muted);
856
- font-size: 13px;
857
- text-align: center;
858
- padding: 16px;
859
- background: var(--bg);
860
- border-radius: 8px;
861
- border: 1px dashed var(--border);
862
- }
863
-
864
- .modal-actions {
865
- display: flex;
866
- gap: 12px;
867
- justify-content: flex-end;
868
- margin-top: 24px;
869
- padding-top: 24px;
870
- border-top: 1px solid var(--border);
871
- }
872
-
873
- /* Action Modal */
874
- .action-modal textarea {
875
- width: 100%;
876
- min-height: 300px;
877
- font-family: monospace;
878
- font-size: 13px;
879
- background: var(--bg);
880
- resize: vertical;
881
- }
882
- .action-toolbar {
883
- display: flex;
884
- gap: 8px;
885
- margin-bottom: 12px;
886
- }
887
- .action-meta {
888
- font-size: 12px;
889
- color: var(--muted);
890
- margin-bottom: 8px;
891
- }
892
- .action-empty {
893
- background: var(--bg);
894
- padding: 24px;
895
- text-align: center;
896
- border-radius: 6px;
897
- color: var(--muted);
898
- }
899
- .pill {
900
- background: var(--accent);
901
- color: #fff;
902
- padding: 4px 8px;
903
- border-radius: 4px;
904
- font-size: 11px;
905
- font-weight: 600;
906
- margin-right: 8px;
907
- }
908
-
909
- /* Context Menu */
910
- .context-menu {
911
- position: fixed;
912
- background: var(--bg-tertiary);
913
- border: 1px solid var(--border);
914
- border-radius: 8px;
915
- padding: 4px 0;
916
- min-width: 140px;
917
- z-index: 1001;
918
- display: none;
919
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
920
- }
921
- .context-menu.active { display: block; }
922
- .context-menu-item {
923
- padding: 8px 16px;
924
- font-size: 0.85rem;
925
- cursor: pointer;
926
- transition: background 0.2s;
927
- }
928
- .context-menu-item:hover { background: var(--bg-secondary); }
929
- .context-menu-item.danger { color: var(--red); }
930
-
931
- /* Notifications */
932
- .notifications {
933
- position: fixed;
934
- top: 80px;
935
- right: 20px;
936
- z-index: 1001;
937
- display: flex;
938
- flex-direction: column;
939
- gap: 10px;
940
- max-width: 350px;
941
- }
942
- .notification {
943
- background: var(--bg-tertiary);
944
- border: 1px solid var(--border);
945
- border-left: 4px solid var(--accent);
946
- border-radius: 8px;
947
- padding: 12px 16px;
948
- display: flex;
949
- align-items: flex-start;
950
- gap: 12px;
951
- animation: slideInRight 0.4s ease;
952
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
953
- }
954
- .notification.success { border-left-color: var(--green); }
955
- .notification.error { border-left-color: var(--red); }
956
- .notification.warning { border-left-color: var(--yellow); }
957
- .notification.claude { border-left-color: #a855f7; }
958
- .notification-icon { font-size: 1.2rem; flex-shrink: 0; }
959
- .notification-content { flex: 1; }
960
- .notification-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 2px; }
961
- .notification-message { font-size: 0.8rem; color: var(--text-secondary); }
962
- .notification-close {
963
- background: none;
964
- border: none;
965
- color: var(--text-secondary);
966
- cursor: pointer;
967
- font-size: 16px;
968
- }
969
-
970
- @keyframes slideInRight {
971
- from { transform: translateX(100%); opacity: 0; }
972
- to { transform: translateX(0); opacity: 1; }
973
- }
974
-
975
- /* Claude Log Section (in modal) */
976
- .claude-log-section {
977
- border-top: 1px solid var(--border);
978
- padding-top: 16px;
979
- margin-top: 8px;
980
- }
981
- .claude-log-header {
982
- display: flex;
983
- align-items: center;
984
- gap: 8px;
985
- margin-bottom: 12px;
986
- }
987
- .claude-log-header h3 { font-size: 14px; font-weight: 600; }
988
- .claude-log-status {
989
- font-size: 12px;
990
- padding: 2px 8px;
991
- border-radius: 10px;
992
- background: #21262d;
993
- }
994
- .claude-log-status.processing { color: var(--yellow); animation: pulse 1s infinite; }
995
- .claude-log-status.success { color: var(--green); }
996
- .claude-log-status.error { color: var(--red); }
997
- .claude-log {
998
- background: #0d1117;
999
- border: 1px solid var(--border);
1000
- border-radius: 6px;
1001
- padding: 16px;
1002
- max-height: 500px;
1003
- overflow-y: auto;
1004
- font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
1005
- font-size: 13px;
1006
- line-height: 1.6;
1007
- white-space: pre-wrap;
1008
- word-break: break-word;
1009
- color: var(--text);
1010
- margin: 0;
1011
- }
1012
- /* Formatted log entries */
1013
- .log-entry { margin-bottom: 8px; padding: 8px 12px; border-radius: 4px; border-left: 3px solid transparent; flex-shrink: 0; }
1014
- .log-entry.timestamp { color: #8b949e; font-size: 11px; border-left-color: #484f58; background: transparent; padding: 4px 12px; }
1015
- .log-entry.system { color: #8b949e; border-left-color: #484f58; background: rgba(139,148,158,0.1); }
1016
- .log-entry.user { color: #58a6ff; border-left-color: #58a6ff; background: rgba(88,166,255,0.1); }
1017
- .log-entry.assistant { color: #7ee787; border-left-color: #7ee787; background: rgba(126,231,135,0.1); }
1018
- .log-entry.tool-call { color: #d2a8ff; border-left-color: #d2a8ff; background: rgba(210,168,255,0.1); padding: 12px; }
1019
- .log-entry.tool-result { color: #ffa657; border-left-color: #ffa657; background: rgba(255,166,87,0.1); }
1020
- .log-entry.error { color: #f85149; border-left-color: #f85149; background: rgba(248,81,73,0.1); }
1021
- .log-entry.success { color: #7ee787; border-left-color: #7ee787; background: rgba(126,231,135,0.1); }
1022
- .log-label { font-weight: 600; font-size: 11px; text-transform: uppercase; margin-bottom: 4px; display: block; opacity: 0.8; }
1023
- .log-content { white-space: pre-wrap; word-break: break-word; }
1024
-
1025
- /* Code blocks with line numbers */
1026
- .log-code-block { background: #161b22; border-radius: 6px; overflow: hidden; margin: 8px 0; border: 1px solid #30363d; }
1027
- .log-code-header { background: #21262d; padding: 8px 12px; font-size: 12px; color: #8b949e; border-bottom: 1px solid #30363d; display: flex; align-items: center; gap: 8px; }
1028
- .log-code-header::before { content: ''; display: inline-block; width: 12px; height: 12px; background: #ffa657; border-radius: 50%; }
1029
- .log-code-content { padding: 12px; overflow-x: auto; font-family: 'Fira Code', 'SF Mono', Monaco, monospace; font-size: 12px; line-height: 1.5; max-height: 400px; overflow-y: auto; }
1030
- .log-code-line { display: flex; min-height: 20px; }
1031
- .log-line-number { color: #484f58; text-align: right; padding-right: 16px; user-select: none; min-width: 40px; flex-shrink: 0; }
1032
- .log-line-content { color: #c9d1d9; white-space: pre; }
1033
-
1034
- /* Message cards */
1035
- .log-message-card { background: #161b22; border-radius: 8px; margin: 12px 0; overflow: hidden; border: 1px solid #30363d; flex-shrink: 0; }
1036
- .log-message-header { padding: 10px 14px; display: flex; align-items: center; gap: 8px; font-weight: 500; font-size: 13px; }
1037
- .log-message-header.assistant-header { background: linear-gradient(135deg, #238636 0%, #2ea043 100%); color: white; }
1038
- .log-message-header.user-header { background: linear-gradient(135deg, #1f6feb 0%, #388bfd 100%); color: white; }
1039
- .log-message-header.tool-header { background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%); color: white; }
1040
- .log-message-header.result-header { background: linear-gradient(135deg, #f97316 0%, #fb923c 100%); color: white; }
1041
- .log-message-body { padding: 14px; border-top: 1px solid #30363d; color: #c9d1d9; white-space: pre-wrap; word-break: break-word; }
1042
-
1043
- /* Tool badges */
1044
- .log-tool-badge { display: inline-flex; align-items: center; gap: 6px; background: rgba(139,92,246,0.2); color: #a78bfa; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
1045
- .log-tool-input { background: #0d1117; border-radius: 4px; padding: 8px 12px; margin-top: 8px; font-family: 'Fira Code', monospace; font-size: 11px; color: #8b949e; max-height: 150px; overflow: auto; white-space: pre-wrap; }
1046
-
1047
- /* Collapsible raw JSON */
1048
- .log-raw-details { margin: 8px 0; }
1049
- .log-raw-summary { cursor: pointer; color: #8b949e; font-size: 12px; padding: 8px; background: rgba(139,148,158,0.1); border-radius: 4px; }
1050
- .log-raw-summary:hover { background: rgba(139,148,158,0.2); }
1051
- .log-raw-json { background: #0d1117; border-radius: 4px; padding: 12px; margin-top: 8px; font-size: 11px; color: #8b949e; max-height: 200px; overflow: auto; white-space: pre-wrap; }
1052
- @keyframes pulse {
1053
- 0%, 100% { opacity: 1; }
1054
- 50% { opacity: 0.5; }
1055
- }
1056
-
1057
- /* Footer */
1058
- footer {
1059
- position: fixed;
1060
- bottom: 0;
1061
- left: 0;
1062
- right: 0;
1063
- padding: 16px 24px;
1064
- text-align: center;
1065
- font-size: 0.9rem;
1066
- color: var(--text-secondary);
1067
- background: var(--bg-secondary);
1068
- border-top: 1px solid var(--border);
1069
- display: flex;
1070
- align-items: center;
1071
- justify-content: center;
1072
- gap: 16px;
1073
- height: 50px;
1074
- }
1075
-
1076
- @media (max-width: 768px) {
1077
- header { flex-direction: column; align-items: stretch; }
1078
- .filters { flex-wrap: wrap; }
1079
- .form-row { grid-template-columns: 1fr; }
1080
- }
1081
- `;
1082
- }
1083
- /**
1084
- * Get JavaScript for interactivity
1085
- */
1086
- function getScript() {
1087
- return `
1088
- // i18n Translations
1089
- const translations = {
1090
- en: {
1091
- // Filter
1092
- 'filter.allPriorities': 'All priorities',
1093
- 'filter.search': 'Search...',
1094
- // Buttons
1095
- 'btn.newTicket': '+ New ticket',
1096
- 'btn.createTicket': 'Create ticket',
1097
- 'btn.update': 'Update',
1098
- 'btn.cancel': 'Cancel',
1099
- 'btn.archive': 'Archive',
1100
- 'btn.next': 'Next',
1101
- 'btn.add': 'Add',
1102
- 'btn.edit': 'Edit',
1103
- 'btn.save': 'Save',
1104
- 'btn.reload': 'Reload',
1105
- // Modal
1106
- 'modal.newTicket': 'New ticket',
1107
- 'modal.editTicket': 'Edit',
1108
- 'modal.title': 'Title *',
1109
- 'modal.titlePlaceholder': 'E.g.: Fix the login bug',
1110
- 'modal.description': 'Description',
1111
- 'modal.descriptionPlaceholder': 'Describe the context and details...',
1112
- 'modal.priority': 'Priority',
1113
- 'modal.releaseType': 'Release type',
1114
- 'modal.labels': 'Labels',
1115
- 'modal.selectLabel': 'Select a label...',
1116
- 'modal.acceptanceCriteria': 'Acceptance criteria',
1117
- 'modal.addCriteria': 'Add criteria',
1118
- 'modal.attachments': 'Attachments',
1119
- 'modal.addAttachment': 'Add file',
1120
- 'modal.noAttachments': 'No attachments',
1121
- 'modal.comments': 'Comments',
1122
- 'modal.noComments': 'No comments',
1123
- 'modal.addCommentPlaceholder': 'Add a comment...',
1124
- 'modal.claudeTerminal': 'Claude Terminal',
1125
- // Priority
1126
- 'priority.p0': 'P0 - Critical',
1127
- 'priority.p1': 'P1 - High',
1128
- 'priority.p2': 'P2 - Normal',
1129
- 'priority.p3': 'P3 - Low',
1130
- // Semver
1131
- 'semver.patch': 'Patch (x.x.X) - Bug fix',
1132
- 'semver.minor': 'Minor (x.X.0) - Feature',
1133
- 'semver.major': 'Major (X.0.0) - Breaking',
1134
- 'semver.none': 'No deployment',
1135
- // Status
1136
- 'status.waiting': 'Waiting',
1137
- 'status.processing': 'Running...',
1138
- 'status.completed': 'Completed',
1139
- 'status.failed': 'Failed',
1140
- 'status.connected': 'Connected',
1141
- // Stats
1142
- 'stats.total': 'Total',
1143
- // Board
1144
- 'board.empty': 'Empty',
1145
- // Action modal
1146
- 'action.instructions': 'Instructions',
1147
- 'action.noInstructions': 'No instructions available',
1148
- 'action.noFile': 'No ACTION file. Click "Edit" to create it.',
1149
- 'action.modifiedOn': 'Modified on',
1150
- // Notifications
1151
- 'notify.titleRequired': 'Title required',
1152
- 'notify.titleMandatory': 'Title is mandatory',
1153
- 'notify.ticketUpdated': 'Ticket updated',
1154
- 'notify.ticketCreated': 'Ticket created',
1155
- 'notify.ticketAdvanced': 'Ticket advanced',
1156
- 'notify.ticketArchived': 'Ticket archived',
1157
- 'notify.ticketMoved': 'moved',
1158
- 'notify.moveTo': 'To',
1159
- 'notify.commentAdded': 'Comment added',
1160
- 'notify.actionUpdated': 'updated',
1161
- 'notify.error': 'Error',
1162
- 'notify.unableToSave': 'Unable to save',
1163
- 'notify.loadingError': 'Loading error',
1164
- 'notify.claudeStarted': 'Claude started',
1165
- 'notify.claudeFinished': 'Claude finished',
1166
- 'notify.claudeFailed': 'Claude failed',
1167
- 'notify.processingSuccess': 'Processing successful',
1168
- 'notify.checkLogs': 'Check logs',
1169
- // Confirm
1170
- 'confirm.archive': 'Archive',
1171
- // Button states
1172
- 'btn.updating': 'Updating...',
1173
- 'btn.creating': 'Creating...',
1174
- 'btn.moving': 'Moving...',
1175
- 'btn.archiving': 'Archiving...',
1176
- 'btn.sending': 'Sending...',
1177
- 'btn.saving': 'Saving...',
1178
- },
1179
- fr: {
1180
- // Filter
1181
- 'filter.allPriorities': 'Toutes priorités',
1182
- 'filter.search': 'Rechercher...',
1183
- // Buttons
1184
- 'btn.newTicket': '+ Nouveau ticket',
1185
- 'btn.createTicket': 'Créer le ticket',
1186
- 'btn.update': 'Mettre à jour',
1187
- 'btn.cancel': 'Annuler',
1188
- 'btn.archive': 'Archiver',
1189
- 'btn.next': 'Suivant',
1190
- 'btn.add': 'Ajouter',
1191
- 'btn.edit': 'Modifier',
1192
- 'btn.save': 'Enregistrer',
1193
- 'btn.reload': 'Recharger',
1194
- // Modal
1195
- 'modal.newTicket': 'Nouveau ticket',
1196
- 'modal.editTicket': 'Modifier',
1197
- 'modal.title': 'Titre *',
1198
- 'modal.titlePlaceholder': 'Ex: Corriger le bug de connexion',
1199
- 'modal.description': 'Description',
1200
- 'modal.descriptionPlaceholder': 'Décrivez le contexte et les détails...',
1201
- 'modal.priority': 'Priorité',
1202
- 'modal.releaseType': 'Type de release',
1203
- 'modal.labels': 'Labels',
1204
- 'modal.selectLabel': 'Sélectionner un label...',
1205
- 'modal.acceptanceCriteria': 'Critères d\\'acceptation',
1206
- 'modal.addCriteria': 'Ajouter un critère',
1207
- 'modal.attachments': 'Pièces jointes',
1208
- 'modal.addAttachment': 'Ajouter un fichier',
1209
- 'modal.noAttachments': 'Aucune pièce jointe',
1210
- 'modal.comments': 'Commentaires',
1211
- 'modal.noComments': 'Aucun commentaire',
1212
- 'modal.addCommentPlaceholder': 'Ajouter un commentaire...',
1213
- 'modal.claudeTerminal': 'Terminal Claude',
1214
- // Priority
1215
- 'priority.p0': 'P0 - Critique',
1216
- 'priority.p1': 'P1 - Haute',
1217
- 'priority.p2': 'P2 - Normale',
1218
- 'priority.p3': 'P3 - Basse',
1219
- // Semver
1220
- 'semver.patch': 'Patch (x.x.X) - Bug fix',
1221
- 'semver.minor': 'Minor (x.X.0) - Fonctionnalité',
1222
- 'semver.major': 'Major (X.0.0) - Breaking',
1223
- 'semver.none': 'Aucun déploiement',
1224
- // Status
1225
- 'status.waiting': 'En attente',
1226
- 'status.processing': 'En cours...',
1227
- 'status.completed': 'Terminé',
1228
- 'status.failed': 'Échoué',
1229
- 'status.connected': 'Connecté',
1230
- // Stats
1231
- 'stats.total': 'Total',
1232
- // Board
1233
- 'board.empty': 'Vide',
1234
- // Action modal
1235
- 'action.instructions': 'Instructions',
1236
- 'action.noInstructions': 'Aucune instruction disponible',
1237
- 'action.noFile': 'Aucun fichier ACTION. Cliquez sur "Modifier" pour le créer.',
1238
- 'action.modifiedOn': 'Modifié le',
1239
- // Notifications
1240
- 'notify.titleRequired': 'Titre requis',
1241
- 'notify.titleMandatory': 'Le titre est obligatoire',
1242
- 'notify.ticketUpdated': 'Ticket mis à jour',
1243
- 'notify.ticketCreated': 'Ticket créé',
1244
- 'notify.ticketAdvanced': 'Ticket avancé',
1245
- 'notify.ticketArchived': 'Ticket archivé',
1246
- 'notify.ticketMoved': 'déplacé',
1247
- 'notify.moveTo': 'Vers',
1248
- 'notify.commentAdded': 'Commentaire ajouté',
1249
- 'notify.actionUpdated': 'mis à jour',
1250
- 'notify.error': 'Erreur',
1251
- 'notify.unableToSave': 'Impossible de sauvegarder',
1252
- 'notify.loadingError': 'Erreur de chargement',
1253
- 'notify.claudeStarted': 'Claude démarré',
1254
- 'notify.claudeFinished': 'Claude terminé',
1255
- 'notify.claudeFailed': 'Claude échoué',
1256
- 'notify.processingSuccess': 'Traitement réussi',
1257
- 'notify.checkLogs': 'Voir les logs',
1258
- // Confirm
1259
- 'confirm.archive': 'Archiver',
1260
- // Button states
1261
- 'btn.updating': 'Mise à jour...',
1262
- 'btn.creating': 'Création...',
1263
- 'btn.moving': 'Déplacement...',
1264
- 'btn.archiving': 'Archivage...',
1265
- 'btn.sending': 'Envoi...',
1266
- 'btn.saving': 'Sauvegarde...',
1267
- }
1268
- };
1269
-
1270
- let currentLang = localStorage.getItem('autocode-lang') || 'fr';
1271
-
1272
- function t(key) {
1273
- return translations[currentLang][key] || translations['en'][key] || key;
1274
- }
1275
-
1276
- function switchLanguage(lang) {
1277
- currentLang = lang;
1278
- currentActionLang = lang;
1279
- localStorage.setItem('autocode-lang', lang);
1280
- document.documentElement.lang = lang;
1281
-
1282
- // Update lang switcher buttons
1283
- document.querySelectorAll('.lang-switcher .lang-btn').forEach(btn => {
1284
- btn.classList.toggle('active', btn.dataset.lang === lang);
1285
- });
1286
-
1287
- // Update all elements with data-i18n
1288
- document.querySelectorAll('[data-i18n]').forEach(el => {
1289
- const key = el.getAttribute('data-i18n');
1290
- if (translations[lang] && translations[lang][key]) {
1291
- el.textContent = translations[lang][key];
1292
- }
1293
- });
1294
-
1295
- // Update placeholders
1296
- document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
1297
- const key = el.getAttribute('data-i18n-placeholder');
1298
- if (translations[lang] && translations[lang][key]) {
1299
- el.placeholder = translations[lang][key];
1300
- }
1301
- });
1302
-
1303
- // Update select options
1304
- document.querySelectorAll('select option[data-i18n]').forEach(el => {
1305
- const key = el.getAttribute('data-i18n');
1306
- if (translations[lang] && translations[lang][key]) {
1307
- el.textContent = translations[lang][key];
1308
- }
1309
- });
1310
-
1311
- // Re-render board to update dynamic content
1312
- render();
1313
- }
1314
-
1315
- // State
1316
- let filterPriority = '';
1317
- let filterSearch = '';
1318
- let selectedLabels = [];
1319
- let criteriaCount = 0;
1320
- let editingKey = null;
1321
- let currentComments = [];
1322
- let contextMenuTicket = null;
1323
- let draggedTicket = null;
1324
- let draggedFromColumn = null;
1325
- let currentActionSlug = null;
1326
- let currentActionLang = localStorage.getItem('autocode-lang') || 'fr';
1327
- let originalActionContent = '';
1328
- let claudeProcessingTickets = new Set(); // Tickets currently being processed by Claude
1329
-
1330
- // Initialize language switcher on page load
1331
- (function initLangSwitcher() {
1332
- // Add click handlers to lang buttons
1333
- document.querySelectorAll('.lang-switcher .lang-btn').forEach(btn => {
1334
- btn.addEventListener('click', () => switchLanguage(btn.dataset.lang));
1335
- });
1336
- // Apply saved language
1337
- switchLanguage(currentLang);
1338
- })();
1339
-
1340
- // ========================================
1341
- // NOTIFICATIONS
1342
- // ========================================
1343
- function showNotification(type, title, message, duration = 5000) {
1344
- const container = document.getElementById('notifications');
1345
- const id = 'notif-' + Date.now();
1346
- const icons = { info: '\\u{1F535}', success: '\\u2705', warning: '\\u26A0\\uFE0F', error: '\\u274C', claude: '\\u{1F916}' };
1347
-
1348
- const notif = document.createElement('div');
1349
- notif.className = 'notification ' + (type === 'claude' ? 'info' : type);
1350
- notif.id = id;
1351
- notif.innerHTML = '<span class="notification-icon">' + (icons[type] || icons.info) + '</span>' +
1352
- '<div class="notification-content"><div class="notification-title">' + title + '</div>' +
1353
- (message ? '<div class="notification-message">' + message + '</div>' : '') + '</div>' +
1354
- '<button class="notification-close" onclick="closeNotification(\\'' + id + '\\')">&times;</button>';
1355
-
1356
- container.appendChild(notif);
1357
- setTimeout(() => closeNotification(id), duration);
1358
- }
1359
-
1360
- function closeNotification(id) {
1361
- const notif = document.getElementById(id);
1362
- if (notif) notif.remove();
1363
- }
1364
-
1365
- // ========================================
1366
- // RENDER
1367
- // ========================================
1368
- function render() {
1369
- const board = document.getElementById('board');
1370
- board.innerHTML = '';
1371
- let stats = { total: 0, byPriority: { P0: 0, P1: 0, P2: 0, P3: 0 } };
1372
-
1373
- COLUMNS.forEach(col => {
1374
- let tickets = TICKETS.filter(t => t.column_slug === col.slug);
1375
- if (filterPriority) tickets = tickets.filter(t => t.priority === filterPriority);
1376
- if (filterSearch) {
1377
- const s = filterSearch.toLowerCase();
1378
- tickets = tickets.filter(t => t.title.toLowerCase().includes(s) || t.key.toLowerCase().includes(s));
1379
- }
1380
- tickets.sort((a, b) => {
1381
- const p = { P0: 0, P1: 1, P2: 2, P3: 3 };
1382
- return p[a.priority] - p[b.priority];
1383
- });
1384
-
1385
- stats.total += tickets.length;
1386
- tickets.forEach(t => stats.byPriority[t.priority] = (stats.byPriority[t.priority] || 0) + 1);
1387
-
1388
- const div = document.createElement('div');
1389
- div.className = 'column';
1390
- div.ondragover = onDragOver;
1391
- div.ondragenter = onDragEnter;
1392
- div.ondragleave = onDragLeave;
1393
- div.ondrop = (e) => onDrop(e, col.slug, col.name);
1394
-
1395
- div.innerHTML = '<div class="column-header">' +
1396
- '<span class="column-title">' + col.name + '</span>' +
1397
- '<div class="column-header-actions">' +
1398
- '<a class="btn-action" title="ACTION.md" href="/column/' + col.slug + '/edit">\\u{1F4D8}</a>' +
1399
- '<span class="column-count">' + tickets.length + '</span>' +
1400
- '</div></div>' +
1401
- '<div class="column-body">' +
1402
- (tickets.length ? tickets.map(tk => {
1403
- const isProcessing = claudeProcessingTickets.has(tk.key);
1404
- return '<div class="ticket' + (isProcessing ? ' claude-processing' : '') + '" draggable="true" ' +
1405
- 'onclick="onTicketClick(\\'' + tk.key + '\\')" ' +
1406
- 'oncontextmenu="showContextMenu(event,\\'' + tk.key + '\\')" ' +
1407
- 'ondragstart="onDragStart(event,\\'' + tk.key + '\\',\\'' + col.slug + '\\')" ' +
1408
- 'ondragend="onDragEnd(event)">' +
1409
- '<div class="ticket-key">' + tk.key + '</div>' +
1410
- '<div class="ticket-title">' + escapeHtml(tk.title) + '</div>' +
1411
- '<div class="ticket-meta"><span class="priority ' + tk.priority + '">' + tk.priority + '</span></div>' +
1412
- (isProcessing ? '<div class="ticket-claude-indicator"><span class="claude-dot"></span>\\u{1F916} ' + t('status.processing') + '</div>' : '') +
1413
- '</div>';
1414
- }).join('') : '<div class="empty">' + t('board.empty') + '</div>') +
1415
- '</div>';
1416
-
1417
- board.appendChild(div);
1418
- });
1419
-
1420
- document.getElementById('stats').innerHTML =
1421
- '<span class="stat connected">\\u{1F7E2} ' + t('status.connected') + '</span>' +
1422
- '<span class="stat">' + t('stats.total') + ': ' + stats.total + '</span>' +
1423
- '<span class="stat P0">P0: ' + (stats.byPriority.P0 || 0) + '</span>' +
1424
- '<span class="stat P1">P1: ' + (stats.byPriority.P1 || 0) + '</span>' +
1425
- '<span class="stat P2">P2: ' + (stats.byPriority.P2 || 0) + '</span>';
1426
- }
1427
-
1428
- function escapeHtml(text) {
1429
- const div = document.createElement('div');
1430
- div.textContent = text;
1431
- return div.innerHTML;
1432
- }
1433
-
1434
- // ========================================
1435
- // FILTERS
1436
- // ========================================
1437
- document.getElementById('filter-priority').onchange = e => { filterPriority = e.target.value; render(); };
1438
- document.getElementById('filter-search').oninput = e => { filterSearch = e.target.value.toLowerCase(); render(); };
1439
-
1440
- // ========================================
1441
- // MODAL TICKET
1442
- // ========================================
1443
- function openModal(key = null) {
1444
- editingKey = key;
1445
- document.getElementById('modal-overlay').classList.add('active');
1446
- document.body.style.overflow = 'hidden';
1447
-
1448
- const modalTitle = document.getElementById('modal-title');
1449
- const saveBtn = document.getElementById('btn-save');
1450
- const nextBtn = document.getElementById('btn-next');
1451
- const archiveBtn = document.getElementById('btn-archive');
1452
- const commentsSection = document.getElementById('comments-section');
1453
- const attachmentsSection = document.getElementById('attachments-section');
1454
-
1455
- if (key) {
1456
- modalTitle.textContent = t('modal.editTicket') + ' ' + key;
1457
- saveBtn.textContent = t('btn.update');
1458
- nextBtn.style.display = 'inline-block';
1459
- archiveBtn.style.display = 'inline-block';
1460
- commentsSection.style.display = 'block';
1461
- attachmentsSection.style.display = 'block';
1462
- loadTicketForEdit(key);
1463
- } else {
1464
- modalTitle.textContent = t('modal.newTicket');
1465
- saveBtn.textContent = t('btn.createTicket');
1466
- nextBtn.style.display = 'none';
1467
- archiveBtn.style.display = 'none';
1468
- commentsSection.style.display = 'none';
1469
- attachmentsSection.style.display = 'none';
1470
- resetForm();
1471
- resetComments();
1472
- resetAttachments();
1473
- }
1474
- }
1475
-
1476
- function closeModal() {
1477
- document.getElementById('modal-overlay').classList.remove('active');
1478
- document.body.style.overflow = '';
1479
- editingKey = null;
1480
- resetForm();
1481
- resetClaudeLog();
1482
- }
1483
-
1484
- function resetForm() {
1485
- document.getElementById('ticket-title').value = '';
1486
- document.getElementById('ticket-description').value = '';
1487
- document.getElementById('ticket-priority').value = 'P2';
1488
- document.getElementById('ticket-semver').value = 'patch';
1489
- document.getElementById('ticket-labels').value = '';
1490
- selectedLabels = [];
1491
- criteriaCount = 0;
1492
- document.getElementById('selected-labels').innerHTML = '';
1493
- document.getElementById('criteria-list').innerHTML = '';
1494
- }
1495
-
1496
- async function loadTicketForEdit(key) {
1497
- try {
1498
- const res = await fetch('/api/tickets/' + key);
1499
- if (!res.ok) throw new Error('Ticket not found');
1500
- const json = await res.json();
1501
- if (!json.success || !json.data) throw new Error('Ticket not found');
1502
- const ticket = json.data;
1503
-
1504
- document.getElementById('ticket-title').value = ticket.title || '';
1505
- document.getElementById('ticket-description').value = ticket.description || '';
1506
- document.getElementById('ticket-priority').value = ticket.priority || 'P2';
1507
- document.getElementById('ticket-semver').value = ticket.semver || 'patch';
1508
-
1509
- selectedLabels = Array.isArray(ticket.labels) ? [...ticket.labels] : [];
1510
- renderLabels();
1511
-
1512
- criteriaCount = 0;
1513
- document.getElementById('criteria-list').innerHTML = '';
1514
- if (Array.isArray(ticket.acceptance_criteria)) {
1515
- ticket.acceptance_criteria.forEach(c => addCriteria(c));
1516
- }
1517
-
1518
- renderComments(ticket.comments || []);
1519
-
1520
- // Fetch attachments
1521
- loadAttachments(key);
1522
-
1523
- // Fetch Claude log if exists
1524
- fetchLog(key);
1525
- } catch (e) {
1526
- console.error('Error:', e);
1527
- showNotification('error', 'Error', e.message);
1528
- closeModal();
1529
- }
1530
- }
1531
-
1532
- async function saveTicket() {
1533
- const title = document.getElementById('ticket-title').value.trim();
1534
- const description = document.getElementById('ticket-description').value.trim();
1535
- const priority = document.getElementById('ticket-priority').value;
1536
- const semver = document.getElementById('ticket-semver').value;
1537
- const criteria = getCriteria();
1538
-
1539
- if (!title) {
1540
- showNotification('warning', t('notify.titleRequired'), t('notify.titleMandatory'));
1541
- return;
1542
- }
1543
-
1544
- const btn = document.getElementById('btn-save');
1545
- btn.disabled = true;
1546
-
1547
- try {
1548
- if (editingKey) {
1549
- btn.textContent = t('btn.updating');
1550
- await fetch('/api/tickets/' + editingKey, {
1551
- method: 'PATCH',
1552
- headers: { 'Content-Type': 'application/json' },
1553
- body: JSON.stringify({ title, description, priority, semver, labels: selectedLabels, acceptance_criteria: criteria })
1554
- });
1555
- showNotification('success', t('notify.ticketUpdated'), editingKey);
1556
- } else {
1557
- btn.textContent = t('btn.creating');
1558
- const res = await fetch('/api/tickets', {
1559
- method: 'POST',
1560
- headers: { 'Content-Type': 'application/json' },
1561
- body: JSON.stringify({ title, priority, labels: selectedLabels, description, semver, acceptance_criteria: criteria })
1562
- });
1563
- const data = await res.json();
1564
- showNotification('success', t('notify.ticketCreated'), data.key || '');
1565
- }
1566
- closeModal();
1567
- loadTicketsFromAPI();
1568
- } catch (e) {
1569
- showNotification('error', t('notify.error'), e.message);
1570
- btn.disabled = false;
1571
- btn.textContent = editingKey ? t('btn.update') : t('btn.createTicket');
1572
- }
1573
- }
1574
-
1575
- async function advanceTicket() {
1576
- if (!editingKey) return;
1577
- const btn = document.getElementById('btn-next');
1578
- btn.disabled = true;
1579
- btn.textContent = t('btn.moving');
1580
- try {
1581
- await fetch('/api/tickets/' + editingKey + '/next', {
1582
- method: 'POST',
1583
- headers: { 'Content-Type': 'application/json' },
1584
- body: JSON.stringify({ lang: currentLang })
1585
- });
1586
- showNotification('info', t('notify.ticketAdvanced'), editingKey);
1587
- closeModal();
1588
- loadTicketsFromAPI();
1589
- } catch (e) {
1590
- showNotification('error', t('notify.error'), e.message);
1591
- btn.disabled = false;
1592
- btn.textContent = t('btn.next');
1593
- }
1594
- }
1595
-
1596
- async function archiveTicket() {
1597
- if (!editingKey) return;
1598
- if (!confirm(t('confirm.archive') + ' ' + editingKey + '?')) return;
1599
-
1600
- const btn = document.getElementById('btn-archive');
1601
- btn.disabled = true;
1602
- btn.textContent = t('btn.archiving');
1603
- try {
1604
- const lastColumn = COLUMNS[COLUMNS.length - 1];
1605
- await fetch('/api/tickets/' + editingKey + '/move', {
1606
- method: 'POST',
1607
- headers: { 'Content-Type': 'application/json' },
1608
- body: JSON.stringify({ column: lastColumn.name, force: true, lang: currentLang })
1609
- });
1610
- showNotification('info', t('notify.ticketArchived'), editingKey);
1611
- closeModal();
1612
- loadTicketsFromAPI();
1613
- } catch (e) {
1614
- showNotification('error', t('notify.error'), e.message);
1615
- btn.disabled = false;
1616
- btn.textContent = t('btn.archive');
1617
- }
1618
- }
1619
-
1620
- // Labels
1621
- function addLabel(select) {
1622
- const value = select.value;
1623
- if (value && !selectedLabels.includes(value)) {
1624
- selectedLabels.push(value);
1625
- renderLabels();
1626
- }
1627
- select.value = '';
1628
- }
1629
-
1630
- function removeLabel(label) {
1631
- selectedLabels = selectedLabels.filter(l => l !== label);
1632
- renderLabels();
1633
- }
1634
-
1635
- function renderLabels() {
1636
- const container = document.getElementById('selected-labels');
1637
- container.innerHTML = selectedLabels.map(label =>
1638
- '<span class="label-tag">' + label + '<span class="remove-label" onclick="removeLabel(\\'' + label + '\\')">&times;</span></span>'
1639
- ).join('');
1640
- }
1641
-
1642
- // Criteria
1643
- function addCriteria(value = '') {
1644
- criteriaCount++;
1645
- const id = criteriaCount;
1646
- const container = document.getElementById('criteria-list');
1647
- const div = document.createElement('div');
1648
- div.className = 'criteria-item';
1649
- div.id = 'criteria-' + id;
1650
- div.innerHTML = '<input type="text" placeholder="Ex: L\\'utilisateur peut..." value="' + escapeHtml(value) + '">' +
1651
- '<button type="button" class="btn-remove" onclick="removeCriteria(' + id + ')">&times;</button>';
1652
- container.appendChild(div);
1653
- }
1654
-
1655
- function removeCriteria(id) {
1656
- const el = document.getElementById('criteria-' + id);
1657
- if (el) el.remove();
1658
- }
1659
-
1660
- function getCriteria() {
1661
- const items = document.querySelectorAll('.criteria-item input');
1662
- return Array.from(items).map(i => i.value.trim()).filter(Boolean);
1663
- }
1664
-
1665
- // ========================================
1666
- // COMMENTS
1667
- // ========================================
1668
- function resetComments() {
1669
- currentComments = [];
1670
- document.getElementById('comments-list').innerHTML = '<div class="no-comments">' + t('modal.noComments') + '</div>';
1671
- document.getElementById('comments-count').textContent = '0';
1672
- document.getElementById('new-comment').value = '';
1673
- }
1674
-
1675
- function renderComments(comments) {
1676
- currentComments = comments || [];
1677
- const list = document.getElementById('comments-list');
1678
- const count = document.getElementById('comments-count');
1679
- count.textContent = currentComments.length;
1680
-
1681
- if (currentComments.length === 0) {
1682
- list.innerHTML = '<div class="no-comments">' + t('modal.noComments') + '</div>';
1683
- return;
1684
- }
1685
-
1686
- const sorted = [...currentComments].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
1687
- list.innerHTML = sorted.map((comment, index) => {
1688
- const date = new Date(comment.created_at);
1689
- const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
1690
- const source = comment.source || 'user';
1691
- const sourceBadge = source === 'claude'
1692
- ? '<span class="comment-source claude">Claude</span>'
1693
- : '<span class="comment-source user">User</span>';
1694
- return '<div class="comment" id="comment-' + index + '">' +
1695
- '<div class="comment-meta" onclick="toggleComment(' + index + ')">' + sourceBadge + '<span class="comment-column">' + (comment.column || 'N/A') + '</span>' +
1696
- '<span class="comment-date">' + dateStr + '</span></div>' +
1697
- '<div class="comment-text">' + renderMarkdown(comment.text) + '</div></div>';
1698
- }).join('');
1699
- }
1700
-
1701
- function toggleComment(index) {
1702
- const comments = document.querySelectorAll('.comment');
1703
- comments.forEach((comment, i) => {
1704
- if (i === index) {
1705
- comment.classList.toggle('expanded');
1706
- } else {
1707
- comment.classList.remove('expanded');
1708
- }
1709
- });
1710
- }
1711
-
1712
- function renderMarkdown(text) {
1713
- if (!text) return '';
1714
- let html = escapeHtml(text);
1715
- html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
1716
- html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
1717
- html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>');
1718
- html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
1719
- html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
1720
- html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
1721
- html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
1722
- html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
1723
- html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
1724
- html = html.replace(/\\n/g, '<br>');
1725
- return html;
1726
- }
1727
-
1728
- async function addComment() {
1729
- if (!editingKey) return;
1730
- const textarea = document.getElementById('new-comment');
1731
- const text = textarea.value.trim();
1732
- if (!text) return;
1733
-
1734
- const btn = document.querySelector('.btn-comment');
1735
- btn.disabled = true;
1736
- btn.textContent = t('btn.sending');
1737
-
1738
- try {
1739
- const res = await fetch('/api/tickets/' + editingKey + '/comments', {
1740
- method: 'POST',
1741
- headers: { 'Content-Type': 'application/json' },
1742
- body: JSON.stringify({ text })
1743
- });
1744
- const result = await res.json();
1745
- textarea.value = '';
1746
- if (result.success && result.data && result.data.comments) {
1747
- renderComments(result.data.comments);
1748
- }
1749
- showNotification('success', t('notify.commentAdded'), '');
1750
- } catch (e) {
1751
- showNotification('error', t('notify.error'), e.message);
1752
- } finally {
1753
- btn.disabled = false;
1754
- btn.textContent = t('btn.add');
1755
- }
1756
- }
1757
-
1758
- // ========================================
1759
- // ATTACHMENTS
1760
- // ========================================
1761
- let currentAttachments = [];
1762
-
1763
- function resetAttachments() {
1764
- currentAttachments = [];
1765
- document.getElementById('attachments-list').innerHTML = '<div class="no-attachments">' + t('modal.noAttachments') + '</div>';
1766
- document.getElementById('attachments-count').textContent = '0';
1767
- document.getElementById('file-input').value = '';
1768
- }
1769
-
1770
- async function loadAttachments(key) {
1771
- try {
1772
- const res = await fetch('/api/tickets/' + key + '/attachments');
1773
- const json = await res.json();
1774
- if (json.success) {
1775
- currentAttachments = json.data || [];
1776
- renderAttachments();
1777
- }
1778
- } catch (e) {
1779
- console.error('Error loading attachments:', e);
1780
- }
1781
- }
1782
-
1783
- function renderAttachments() {
1784
- const list = document.getElementById('attachments-list');
1785
- const count = document.getElementById('attachments-count');
1786
- count.textContent = currentAttachments.length;
1787
-
1788
- if (currentAttachments.length === 0) {
1789
- list.innerHTML = '<div class="no-attachments">' + t('modal.noAttachments') + '</div>';
1790
- return;
1791
- }
1792
-
1793
- list.innerHTML = currentAttachments.map(filename => {
1794
- const ext = filename.split('.').pop().toLowerCase();
1795
- const icon = getFileIcon(ext);
1796
- return '<div class="attachment-item">' +
1797
- '<span class="attachment-icon">' + icon + '</span>' +
1798
- '<span class="attachment-name" title="' + escapeHtml(filename) + '">' + escapeHtml(filename) + '</span>' +
1799
- '<button class="attachment-delete" onclick="deleteAttachment(\\'' + escapeHtml(filename) + '\\')" title="Delete">&times;</button>' +
1800
- '</div>';
1801
- }).join('');
1802
- }
1803
-
1804
- function getFileIcon(ext) {
1805
- const icons = {
1806
- pdf: '📄', doc: '📝', docx: '📝', txt: '📝',
1807
- png: '🖼️', jpg: '🖼️', jpeg: '🖼️', gif: '🖼️', svg: '🖼️', webp: '🖼️',
1808
- mp4: '🎬', mov: '🎬', avi: '🎬',
1809
- mp3: '🎵', wav: '🎵',
1810
- zip: '📦', rar: '📦', tar: '📦', gz: '📦',
1811
- js: '📜', ts: '📜', py: '📜', json: '📜', md: '📜',
1812
- };
1813
- return icons[ext] || '📎';
1814
- }
1815
-
1816
- async function uploadFiles(files) {
1817
- if (!editingKey || files.length === 0) return;
1818
-
1819
- const formData = new FormData();
1820
- for (const file of files) {
1821
- formData.append('files', file, file.name);
1822
- }
1823
-
1824
- try {
1825
- const res = await fetch('/api/tickets/' + editingKey + '/attachments', {
1826
- method: 'POST',
1827
- body: formData
1828
- });
1829
- const json = await res.json();
1830
- if (json.success) {
1831
- showNotification('success', 'Files uploaded', json.data.join(', '));
1832
- loadAttachments(editingKey);
1833
- } else {
1834
- showNotification('error', 'Upload failed', json.error);
1835
- }
1836
- } catch (e) {
1837
- showNotification('error', 'Upload error', e.message);
1838
- }
1839
-
1840
- // Reset file input
1841
- document.getElementById('file-input').value = '';
1842
- }
1843
-
1844
- async function deleteAttachment(filename) {
1845
- if (!editingKey) return;
1846
- if (!confirm('Delete ' + filename + '?')) return;
1847
-
1848
- try {
1849
- const res = await fetch('/api/tickets/' + editingKey + '/attachments/' + encodeURIComponent(filename), {
1850
- method: 'DELETE'
1851
- });
1852
- const json = await res.json();
1853
- if (json.success) {
1854
- showNotification('info', 'File deleted', filename);
1855
- loadAttachments(editingKey);
1856
- } else {
1857
- showNotification('error', 'Delete failed', json.error);
1858
- }
1859
- } catch (e) {
1860
- showNotification('error', 'Delete error', e.message);
1861
- }
1862
- }
1863
-
1864
- // ========================================
1865
- // DRAG & DROP
1866
- // ========================================
1867
- function onDragStart(e, key, columnSlug) {
1868
- draggedTicket = key;
1869
- draggedFromColumn = columnSlug;
1870
- e.target.classList.add('dragging');
1871
- e.dataTransfer.effectAllowed = 'move';
1872
- e.dataTransfer.setData('text/plain', key);
1873
- }
1874
-
1875
- function onDragEnd(e) {
1876
- e.target.classList.remove('dragging');
1877
- document.querySelectorAll('.column').forEach(c => c.classList.remove('drag-over'));
1878
- }
1879
-
1880
- function onDragOver(e) {
1881
- e.preventDefault();
1882
- e.dataTransfer.dropEffect = 'move';
1883
- }
1884
-
1885
- function onDragEnter(e) {
1886
- e.preventDefault();
1887
- const column = e.target.closest('.column');
1888
- if (column) column.classList.add('drag-over');
1889
- }
1890
-
1891
- function onDragLeave(e) {
1892
- const column = e.target.closest('.column');
1893
- if (column && !column.contains(e.relatedTarget)) {
1894
- column.classList.remove('drag-over');
1895
- }
1896
- }
1897
-
1898
- async function onDrop(e, targetColumnSlug, targetColumnName) {
1899
- e.preventDefault();
1900
- document.querySelectorAll('.column').forEach(c => c.classList.remove('drag-over'));
1901
-
1902
- if (!draggedTicket) return;
1903
- if (draggedFromColumn === targetColumnSlug) {
1904
- draggedTicket = null;
1905
- draggedFromColumn = null;
1906
- return;
1907
- }
1908
-
1909
- const key = draggedTicket;
1910
- draggedTicket = null;
1911
- draggedFromColumn = null;
1912
-
1913
- try {
1914
- await fetch('/api/tickets/' + key + '/move', {
1915
- method: 'POST',
1916
- headers: { 'Content-Type': 'application/json' },
1917
- body: JSON.stringify({ column: targetColumnName, force: true, lang: currentLang })
1918
- });
1919
- showNotification('info', key + ' ' + t('notify.ticketMoved'), t('notify.moveTo') + ' "' + targetColumnName + '"');
1920
- loadTicketsFromAPI();
1921
- } catch (err) {
1922
- showNotification('error', t('notify.error'), err.message);
1923
- }
1924
- }
1925
-
1926
- // ========================================
1927
- // CONTEXT MENU
1928
- // ========================================
1929
- function showContextMenu(e, key) {
1930
- e.preventDefault();
1931
- contextMenuTicket = key;
1932
- const menu = document.getElementById('context-menu');
1933
- menu.style.left = e.clientX + 'px';
1934
- menu.style.top = e.clientY + 'px';
1935
- menu.classList.add('active');
1936
- }
1937
-
1938
- function hideContextMenu() {
1939
- document.getElementById('context-menu').classList.remove('active');
1940
- contextMenuTicket = null;
1941
- }
1942
-
1943
- function editFromContext() {
1944
- if (contextMenuTicket) openModal(contextMenuTicket);
1945
- hideContextMenu();
1946
- }
1947
-
1948
- async function archiveFromContext() {
1949
- if (!contextMenuTicket) return;
1950
- const key = contextMenuTicket;
1951
- hideContextMenu();
1952
-
1953
- if (confirm(t('confirm.archive') + ' ' + key + '?')) {
1954
- try {
1955
- const lastColumn = COLUMNS[COLUMNS.length - 1];
1956
- await fetch('/api/tickets/' + key + '/move', {
1957
- method: 'POST',
1958
- headers: { 'Content-Type': 'application/json' },
1959
- body: JSON.stringify({ column: lastColumn.name, force: true, lang: currentLang })
1960
- });
1961
- showNotification('info', t('notify.ticketArchived'), key);
1962
- loadTicketsFromAPI();
1963
- } catch (err) {
1964
- showNotification('error', t('notify.error'), err.message);
1965
- }
1966
- }
1967
- }
1968
-
1969
- document.addEventListener('click', hideContextMenu);
1970
-
1971
- // ========================================
1972
- // ACTION.md MODAL
1973
- // ========================================
1974
- function openActionModal(slug) {
1975
- currentActionSlug = slug;
1976
- const col = COLUMNS.find(c => c.slug === slug);
1977
- document.getElementById('action-modal-title').textContent = col?.name || slug;
1978
- document.getElementById('action-modal').classList.add('active');
1979
- document.body.style.overflow = 'hidden';
1980
- reloadActionContent();
1981
- }
1982
-
1983
- function closeActionModal() {
1984
- document.getElementById('action-modal').classList.remove('active');
1985
- document.body.style.overflow = '';
1986
- currentActionSlug = null;
1987
- originalActionContent = '';
1988
- setActionEditMode(false);
1989
- document.getElementById('action-content').value = '';
1990
- document.getElementById('action-empty').style.display = 'none';
1991
- document.getElementById('action-meta').textContent = '';
1992
- }
1993
-
1994
- function setActionEditMode(isEdit) {
1995
- const textarea = document.getElementById('action-content');
1996
- const saveBtn = document.getElementById('action-save-btn');
1997
- const editBtn = document.getElementById('action-edit-btn');
1998
- textarea.readOnly = !isEdit;
1999
- saveBtn.disabled = !isEdit;
2000
- editBtn.style.display = isEdit ? 'none' : 'inline-block';
2001
- }
2002
-
2003
- async function reloadActionContent() {
2004
- if (!currentActionSlug) return;
2005
- setActionEditMode(false);
2006
- const textarea = document.getElementById('action-content');
2007
- const empty = document.getElementById('action-empty');
2008
- const meta = document.getElementById('action-meta');
2009
-
2010
- empty.style.display = 'none';
2011
- textarea.style.display = 'block';
2012
- textarea.value = '...';
2013
-
2014
- try {
2015
- const res = await fetch('/api/columns/' + currentActionSlug + '/actions?lang=' + currentActionLang);
2016
- const data = res.ok ? await res.json() : {};
2017
-
2018
- if (!res.ok || !data.success) {
2019
- if (res.status === 404) {
2020
- textarea.value = '';
2021
- textarea.style.display = 'none';
2022
- empty.textContent = t('action.noFile');
2023
- empty.style.display = 'block';
2024
- meta.textContent = '';
2025
- return;
2026
- }
2027
- throw new Error(data.error || t('notify.loadingError'));
2028
- }
2029
-
2030
- const actionData = data.data || {};
2031
- originalActionContent = actionData.content || '';
2032
- textarea.value = originalActionContent;
2033
- const updated = actionData.updated_at ? new Date(actionData.updated_at).toLocaleString(currentLang === 'fr' ? 'fr-FR' : 'en-US') : '';
2034
- meta.textContent = (actionData.path || '') + (updated ? ' - ' + t('action.modifiedOn') + ' ' + updated : '');
2035
- } catch (e) {
2036
- empty.textContent = t('notify.error') + ': ' + e.message;
2037
- empty.style.display = 'block';
2038
- textarea.style.display = 'none';
2039
- meta.textContent = '';
2040
- }
2041
- }
2042
-
2043
- function enterActionEdit() {
2044
- if (!currentActionSlug) return;
2045
- const textarea = document.getElementById('action-content');
2046
- const empty = document.getElementById('action-empty');
2047
- if (empty.style.display === 'block' && !textarea.value) {
2048
- textarea.value = '# ' + document.getElementById('action-modal-title').textContent + '\\n\\n';
2049
- empty.style.display = 'none';
2050
- textarea.style.display = 'block';
2051
- }
2052
- setActionEditMode(true);
2053
- textarea.focus();
2054
- }
2055
-
2056
- async function saveActionContent() {
2057
- if (!currentActionSlug) return;
2058
- const btn = document.getElementById('action-save-btn');
2059
- const textarea = document.getElementById('action-content');
2060
- btn.disabled = true;
2061
- btn.textContent = t('btn.saving');
2062
-
2063
- try {
2064
- const res = await fetch('/api/columns/' + currentActionSlug + '/actions?lang=' + currentActionLang, {
2065
- method: 'POST',
2066
- headers: { 'Content-Type': 'application/json' },
2067
- body: JSON.stringify({ content: textarea.value })
2068
- });
2069
- const data = await res.json();
2070
- if (!res.ok || !data.success) throw new Error(data.error || t('notify.error'));
2071
-
2072
- originalActionContent = textarea.value;
2073
- setActionEditMode(false);
2074
- showNotification('success', 'ACTION.' + currentActionLang + '.md ' + t('notify.actionUpdated'), currentActionSlug);
2075
- } catch (e) {
2076
- showNotification('error', t('notify.unableToSave'), e.message);
2077
- } finally {
2078
- btn.disabled = false;
2079
- btn.textContent = t('btn.save');
2080
- }
2081
- }
2082
-
2083
- // ========================================
2084
- // WEBSOCKET
2085
- // ========================================
2086
- let ws;
2087
-
2088
- function connectWebSocket() {
2089
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
2090
- ws = new WebSocket(protocol + '//' + location.host + '/ws');
2091
-
2092
- ws.onmessage = (event) => {
2093
- try {
2094
- const data = JSON.parse(event.data);
2095
- switch (data.type) {
2096
- case 'refresh':
2097
- case 'ticket_updated':
2098
- case 'ticket_created':
2099
- case 'ticket_moved':
2100
- loadTicketsFromAPI();
2101
- break;
2102
- case 'claude_start':
2103
- onClaudeStart(data.ticket);
2104
- break;
2105
- case 'claude_stream':
2106
- onClaudeStream(data.ticket);
2107
- break;
2108
- case 'claude_end':
2109
- onClaudeEnd(data.ticket, data.success, data.duration);
2110
- loadTicketsFromAPI();
2111
- break;
2112
- case 'claude_complete':
2113
- if (data.success) {
2114
- showNotification('claude', t('notify.claudeFinished') + ' ' + data.ticket, t('notify.processingSuccess'));
2115
- } else {
2116
- showNotification('error', t('notify.claudeFailed') + ' ' + data.ticket, t('notify.checkLogs'));
2117
- }
2118
- break;
2119
- }
2120
- } catch {}
2121
- };
2122
-
2123
- ws.onclose = () => setTimeout(connectWebSocket, 2000);
2124
- ws.onerror = () => ws.close();
2125
- }
2126
-
2127
- // ========================================
2128
- // CLAUDE LOG (in modal)
2129
- // ========================================
2130
- let logPollingInterval = null;
2131
- let claudeProcessingTicket = null;
2132
-
2133
- function startLogPolling(key) {
2134
- stopLogPolling();
2135
- logPollingInterval = setInterval(() => fetchLog(key), 1000);
2136
- fetchLog(key); // Immediate first fetch
2137
- }
2138
-
2139
- function stopLogPolling() {
2140
- if (logPollingInterval) {
2141
- clearInterval(logPollingInterval);
2142
- logPollingInterval = null;
2143
- }
2144
- }
2145
-
2146
- function escapeHtml(str) {
2147
- if (typeof str !== 'string') return '';
2148
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2149
- }
2150
-
2151
- function formatCodeBlock(content, filename) {
2152
- const lines = content.split(/\\\\n|\\n/);
2153
- // Détecter le langage depuis le nom de fichier
2154
- let lang = 'plaintext';
2155
- if (filename) {
2156
- const ext = filename.split('.').pop().toLowerCase();
2157
- const langMap = {
2158
- 'js': 'javascript', 'ts': 'typescript', 'vue': 'html', 'jsx': 'javascript',
2159
- 'tsx': 'typescript', 'py': 'python', 'rb': 'ruby', 'java': 'java',
2160
- 'go': 'go', 'rs': 'rust', 'cpp': 'cpp', 'c': 'c', 'h': 'c',
2161
- 'css': 'css', 'scss': 'scss', 'html': 'html', 'json': 'json',
2162
- 'md': 'markdown', 'sh': 'bash', 'yml': 'yaml', 'yaml': 'yaml'
2163
- };
2164
- lang = langMap[ext] || 'plaintext';
2165
- }
2166
- // Extraire le code sans les numéros de ligne pour highlight.js
2167
- let codeLines = [];
2168
- for (const line of lines) {
2169
- const match = line.match(/^\\s*\\d+[→|](.*)$/);
2170
- if (match) {
2171
- codeLines.push(match[1]);
2172
- } else if (line.trim()) {
2173
- codeLines.push(line);
2174
- }
2175
- }
2176
- const codeContent = codeLines.join('\\n');
2177
- const lineCount = codeLines.length;
2178
- const preview = filename || (lineCount + ' lignes');
2179
-
2180
- let html = '<details class="log-code-block">';
2181
- html += '<summary class="log-code-header"><span class="code-lang">' + lang + '</span> ' + escapeHtml(preview) + '</summary>';
2182
- html += '<pre><code class="language-' + lang + '">' + escapeHtml(codeContent) + '</code></pre>';
2183
- html += '</details>';
2184
- return html;
2185
- }
2186
-
2187
- function formatLogContent(rawContent) {
2188
- if (!rawContent) return '';
2189
- // Supprimer les balises <system-reminder> et leur contenu
2190
- let cleanedContent = rawContent.replace(/<system-reminder>[\\s\\S]*?<\\/system-reminder>/gi, '');
2191
- const lines = cleanedContent.split('\\n');
2192
- const entries = [];
2193
-
2194
- for (const line of lines) {
2195
- if (!line.trim()) continue;
2196
-
2197
- // Timestamp line
2198
- const timestampMatch = line.match(/^\\[(\\d{4}-\\d{2}-\\d{2}T[^\\]]+)\\]\\s*(.*)$/);
2199
- if (timestampMatch) {
2200
- const date = new Date(timestampMatch[1]);
2201
- const timeStr = date.toLocaleTimeString();
2202
- entries.push('<div class="log-entry timestamp">' + timeStr + ' - ' + escapeHtml(timestampMatch[2]) + '</div>');
2203
- continue;
2204
- }
2205
-
2206
- // [RAW] - Parse avec regex (pas JSON.parse car les lignes sont coupées)
2207
- if (line.startsWith('[RAW] ')) {
2208
- const raw = line.slice(6);
2209
-
2210
- // Ignorer les lignes de continuation (ne commencent pas par {)
2211
- if (!raw.startsWith('{')) {
2212
- continue;
2213
- }
2214
-
2215
- // Extraire code source avec numéros de ligne (tool_result)
2216
- const codeMatch = raw.match(/"content":"(\\s*\\d+[→|][^"]*)/);
2217
- if (codeMatch) {
2218
- // Extraire tout le contenu entre "content":" et la fin
2219
- const contentMatch = raw.match(/"content":"([^"]+)/);
2220
- if (contentMatch) {
2221
- const decoded = contentMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"');
2222
- 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>');
2223
- continue;
2224
- }
2225
- }
2226
-
2227
- // Extraire message assistant texte
2228
- const textMatch = raw.match(/"type":"text","text":"([^"]+)"/);
2229
- if (textMatch) {
2230
- const decoded = textMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"');
2231
- entries.push('<div class="log-message-card"><div class="log-message-header assistant-header">Assistant</div><div class="log-message-body">' + escapeHtml(decoded) + '</div></div>');
2232
- continue;
2233
- }
2234
-
2235
- // Extraire tool_use
2236
- const toolMatch = raw.match(/"type":"tool_use"[^}]*"name":"([^"]+)"/);
2237
- if (toolMatch) {
2238
- entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(toolMatch[1]) + '</span></div>');
2239
- continue;
2240
- }
2241
-
2242
- // Ignorer les autres RAW (métadonnées, etc.)
2243
- continue;
2244
- }
2245
-
2246
- // Other prefixed messages
2247
- if (line.startsWith('[SYSTEM]')) {
2248
- entries.push('<div class="log-entry system"><span class="log-label">System</span><div class="log-content">' + escapeHtml(line.slice(9)) + '</div></div>');
2249
- continue;
2250
- }
2251
- if (line.startsWith('[ASSISTANT]')) {
2252
- entries.push('<div class="log-message-card"><div class="log-message-header assistant-header">Assistant</div><div class="log-message-body">' + escapeHtml(line.slice(12)) + '</div></div>');
2253
- continue;
2254
- }
2255
- if (line.startsWith('[TOOL]')) {
2256
- entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(line.slice(7)) + '</span></div>');
2257
- continue;
2258
- }
2259
- if (line.startsWith('[RESULT]')) {
2260
- const content = line.slice(9);
2261
- if (/^\\s*\\d+[→|]/.test(content)) {
2262
- 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>');
2263
- } else {
2264
- 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>');
2265
- }
2266
- continue;
2267
- }
2268
- if (line.startsWith('[ERROR]')) {
2269
- entries.push('<div class="log-entry error"><span class="log-label">Error</span><div class="log-content">' + escapeHtml(line.slice(8)) + '</div></div>');
2270
- continue;
2271
- }
2272
-
2273
- // Default
2274
- entries.push('<div class="log-entry system">' + escapeHtml(line) + '</div>');
2275
- }
2276
-
2277
- return entries.join('');
2278
- }
2279
-
2280
- function resetClaudeLog() {
2281
- stopLogPolling();
2282
- document.getElementById('claude-log-section').style.display = 'none';
2283
- document.getElementById('claude-log').innerHTML = '';
2284
- document.getElementById('claude-log-status').className = 'claude-log-status';
2285
- document.getElementById('claude-log-status').textContent = t('status.waiting');
2286
- }
2287
-
2288
- async function fetchLog(key) {
2289
- try {
2290
- const res = await fetch('/api/tickets/' + key + '/log');
2291
- const json = await res.json();
2292
- if (json.success && json.data) {
2293
- const section = document.getElementById('claude-log-section');
2294
- const log = document.getElementById('claude-log');
2295
-
2296
- if (json.data.exists || json.data.content) {
2297
- section.style.display = 'block';
2298
- log.innerHTML = formatLogContent(json.data.content || '');
2299
- // Auto-scroll
2300
- log.scrollTop = log.scrollHeight;
2301
- }
2302
- }
2303
- } catch (e) {
2304
- console.error('Log fetch error:', e);
2305
- }
2306
- }
2307
-
2308
- function onClaudeStart(ticket) {
2309
- claudeProcessingTicket = ticket;
2310
- claudeProcessingTickets.add(ticket); // Track this ticket as being processed
2311
- render(); // Refresh to show processing indicator
2312
-
2313
- const status = document.getElementById('claude-log-status');
2314
- if (status) {
2315
- status.className = 'claude-log-status processing';
2316
- status.textContent = '🤖 ' + t('status.processing');
2317
- }
2318
- // Start polling if modal is open for this ticket
2319
- if (editingKey === ticket) {
2320
- document.getElementById('claude-log-section').style.display = 'block';
2321
- startLogPolling(ticket);
2322
- }
2323
- showNotification('claude', t('notify.claudeStarted'), ticket);
2324
- }
2325
-
2326
- function onClaudeStream(ticket) {
2327
- // Refresh log if modal is open for this ticket
2328
- if (editingKey === ticket) {
2329
- fetchLog(ticket);
2330
- }
2331
- }
2332
-
2333
- function onClaudeEnd(ticket, success, duration) {
2334
- claudeProcessingTicket = null;
2335
- claudeProcessingTickets.delete(ticket); // Remove from processing set
2336
- render(); // Refresh to hide processing indicator
2337
-
2338
- const status = document.getElementById('claude-log-status');
2339
- if (status && editingKey === ticket) {
2340
- if (success) {
2341
- status.className = 'claude-log-status success';
2342
- status.textContent = '✅ ' + t('status.completed') + ' (' + (duration / 1000).toFixed(1) + 's)';
2343
- } else {
2344
- status.className = 'claude-log-status error';
2345
- status.textContent = '❌ ' + t('status.failed');
2346
- }
2347
- fetchLog(ticket); // Final fetch
2348
- stopLogPolling();
2349
- }
2350
- if (success) {
2351
- showNotification('success', t('notify.claudeFinished'), ticket);
2352
- } else {
2353
- showNotification('error', t('notify.claudeFailed'), ticket);
2354
- }
2355
- }
2356
-
2357
- // ========================================
2358
- // API
2359
- // ========================================
2360
- async function loadTicketsFromAPI() {
2361
- try {
2362
- const res = await fetch('/api/tickets');
2363
- const json = await res.json();
2364
- if (json.success && json.data) {
2365
- TICKETS.length = 0;
2366
- (json.data.tickets || []).forEach(tk => TICKETS.push(tk));
2367
- COLUMNS.length = 0;
2368
- (json.data.columns || []).forEach(c => COLUMNS.push(c));
2369
- render();
2370
- }
2371
- } catch (e) {
2372
- console.error(t('notify.loadingError') + ':', e);
2373
- }
2374
- }
2375
-
2376
- function onTicketClick(key) {
2377
- window.location.href = '/ticket/' + key;
2378
- }
2379
-
2380
- // ========================================
2381
- // KEYBOARD
2382
- // ========================================
2383
- document.addEventListener('keydown', e => {
2384
- if (e.key === 'Escape') {
2385
- const actionModal = document.getElementById('action-modal');
2386
- if (actionModal?.classList.contains('active')) {
2387
- closeActionModal();
2388
- return;
2389
- }
2390
- closeModal();
2391
- }
2392
- });
2393
-
2394
- // ========================================
2395
- // INIT
2396
- // ========================================
2397
- render();
2398
- connectWebSocket();
2399
- `;
2400
- }
2401
- /**
2402
- * Generate column edit page
2403
- */
2404
- export function generateColumnEditPage(slug, lang) {
2405
- const columns = getColumns();
2406
- const column = columns.find(c => c.slug === slug);
2407
- const columnName = column?.name || slug;
2408
- return `<!DOCTYPE html>
2409
- <html lang="${lang}">
2410
- <head>
2411
- <meta charset="UTF-8">
2412
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
2413
- <title>Edit: ${escapeHtml(columnName)} - AutoCode</title>
2414
- <style>
2415
- :root {
2416
- --bg: #0d1117;
2417
- --bg-card: #161b22;
2418
- --border: #30363d;
2419
- --fg: #c9d1d9;
2420
- --muted: #8b949e;
2421
- --accent: #7c3aed;
2422
- --blue: #4dabf7;
2423
- --green: #22c55e;
2424
- --red: #ef4444;
2425
- }
2426
- * { margin: 0; padding: 0; box-sizing: border-box; }
2427
- body {
2428
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2429
- background: var(--bg);
2430
- color: var(--fg);
2431
- height: 100vh;
2432
- display: flex;
2433
- flex-direction: column;
2434
- }
2435
- .header {
2436
- background: var(--bg-card);
2437
- border-bottom: 1px solid var(--border);
2438
- padding: 16px 24px;
2439
- display: flex;
2440
- align-items: center;
2441
- gap: 16px;
2442
- }
2443
- .back-btn {
2444
- background: none;
2445
- border: 1px solid var(--border);
2446
- color: var(--fg);
2447
- padding: 8px 12px;
2448
- border-radius: 6px;
2449
- cursor: pointer;
2450
- font-size: 14px;
2451
- display: flex;
2452
- align-items: center;
2453
- gap: 6px;
2454
- text-decoration: none;
2455
- }
2456
- .back-btn:hover { border-color: var(--accent); color: var(--accent); }
2457
- .title {
2458
- flex: 1;
2459
- font-size: 18px;
2460
- font-weight: 600;
2461
- }
2462
- .title span { color: var(--muted); font-weight: 400; }
2463
- .lang-selector {
2464
- display: flex;
2465
- gap: 4px;
2466
- }
2467
- .lang-btn {
2468
- background: var(--bg);
2469
- border: 1px solid var(--border);
2470
- color: var(--muted);
2471
- padding: 6px 12px;
2472
- border-radius: 4px;
2473
- cursor: pointer;
2474
- font-size: 13px;
2475
- font-weight: 600;
2476
- }
2477
- .lang-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
2478
- .lang-btn:hover:not(.active) { border-color: var(--accent); color: var(--fg); }
2479
- .actions {
2480
- display: flex;
2481
- gap: 8px;
2482
- }
2483
- .btn {
2484
- padding: 8px 16px;
2485
- border-radius: 6px;
2486
- font-size: 14px;
2487
- font-weight: 500;
2488
- cursor: pointer;
2489
- border: none;
2490
- }
2491
- .btn-save { background: var(--green); color: white; }
2492
- .btn-save:hover { opacity: 0.9; }
2493
- .btn-save:disabled { opacity: 0.5; cursor: not-allowed; }
2494
- .editor-container {
2495
- flex: 1;
2496
- padding: 16px 24px;
2497
- display: flex;
2498
- flex-direction: column;
2499
- overflow: hidden;
2500
- }
2501
- .editor {
2502
- flex: 1;
2503
- width: 100%;
2504
- background: var(--bg-card);
2505
- border: 1px solid var(--border);
2506
- border-radius: 8px;
2507
- color: var(--fg);
2508
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
2509
- font-size: 14px;
2510
- line-height: 1.6;
2511
- padding: 16px;
2512
- resize: none;
2513
- outline: none;
2514
- }
2515
- .editor:focus { border-color: var(--accent); }
2516
- .status-bar {
2517
- display: flex;
2518
- justify-content: space-between;
2519
- align-items: center;
2520
- padding: 8px 0;
2521
- font-size: 12px;
2522
- color: var(--muted);
2523
- }
2524
- .status-bar .modified { color: var(--accent); font-weight: 600; }
2525
- .status-bar .saved { color: var(--green); }
2526
- .notification {
2527
- position: fixed;
2528
- bottom: 24px;
2529
- right: 24px;
2530
- background: var(--bg-card);
2531
- border: 1px solid var(--border);
2532
- border-left: 3px solid var(--green);
2533
- padding: 12px 16px;
2534
- border-radius: 6px;
2535
- font-size: 14px;
2536
- opacity: 0;
2537
- transform: translateY(10px);
2538
- transition: all 0.3s;
2539
- z-index: 100;
2540
- }
2541
- .notification.show { opacity: 1; transform: translateY(0); }
2542
- .notification.error { border-left-color: var(--red); }
2543
- </style>
2544
- </head>
2545
- <body>
2546
- <header class="header">
2547
- <a href="/" class="back-btn">← Dashboard</a>
2548
- <h1 class="title">${escapeHtml(columnName)} <span>/ ACTION.md</span></h1>
2549
- <div class="lang-selector" id="lang-selector">
2550
- <button class="lang-btn" data-lang="en">EN</button>
2551
- <button class="lang-btn" data-lang="fr">FR</button>
2552
- </div>
2553
- <div class="actions">
2554
- <button class="btn btn-save" id="saveBtn" disabled>Save</button>
2555
- </div>
2556
- </header>
2557
- <div class="editor-container">
2558
- <div class="status-bar">
2559
- <span id="status">Loading...</span>
2560
- <span id="path">${slug}/ACTION.${lang}.md</span>
2561
- </div>
2562
- <textarea class="editor" id="editor" placeholder="Loading..."></textarea>
2563
- </div>
2564
- <div class="notification" id="notification"></div>
2565
-
2566
- <script>
2567
- const STORAGE_KEY = 'autocode-lang';
2568
- const slug = '${slug}';
2569
- let currentLang = localStorage.getItem(STORAGE_KEY) || 'fr';
2570
- let originalContent = '';
2571
- let hasChanges = false;
2572
-
2573
- const editor = document.getElementById('editor');
2574
- const saveBtn = document.getElementById('saveBtn');
2575
- const status = document.getElementById('status');
2576
- const pathEl = document.getElementById('path');
2577
- const notification = document.getElementById('notification');
2578
-
2579
- // Update UI to reflect current language
2580
- function updateLangUI() {
2581
- document.querySelectorAll('.lang-btn').forEach(btn => {
2582
- btn.classList.toggle('active', btn.dataset.lang === currentLang);
2583
- });
2584
- pathEl.textContent = slug + '/ACTION.' + currentLang + '.md';
2585
- }
2586
-
2587
- // Load content
2588
- async function loadContent() {
2589
- try {
2590
- status.textContent = 'Loading...';
2591
- status.className = '';
2592
- const res = await fetch('/api/columns/' + slug + '/actions?lang=' + currentLang);
2593
- const data = await res.json();
2594
- if (data.success) {
2595
- originalContent = data.data.content || '';
2596
- editor.value = originalContent;
2597
- pathEl.textContent = slug + '/ACTION.' + currentLang + '.md';
2598
- checkChanges();
2599
- status.textContent = 'Ready';
2600
- } else {
2601
- status.textContent = 'Error: ' + (data.error || 'Failed to load');
2602
- }
2603
- } catch (e) {
2604
- status.textContent = 'Error: ' + e.message;
2605
- }
2606
- }
2607
-
2608
- // Save content
2609
- async function saveContent() {
2610
- if (!hasChanges) return;
2611
- try {
2612
- saveBtn.disabled = true;
2613
- saveBtn.textContent = 'Saving...';
2614
- const res = await fetch('/api/columns/' + slug + '/actions?lang=' + currentLang, {
2615
- method: 'POST',
2616
- headers: { 'Content-Type': 'application/json' },
2617
- body: JSON.stringify({ content: editor.value })
2618
- });
2619
- const data = await res.json();
2620
- if (data.success) {
2621
- originalContent = editor.value;
2622
- hasChanges = false;
2623
- checkChanges();
2624
- showNotification('Saved successfully!', false);
2625
- } else {
2626
- showNotification('Error: ' + (data.error || 'Failed to save'), true);
2627
- }
2628
- } catch (e) {
2629
- showNotification('Error: ' + e.message, true);
2630
- } finally {
2631
- saveBtn.textContent = 'Save';
2632
- }
2633
- }
2634
-
2635
- // Check for changes
2636
- function checkChanges() {
2637
- hasChanges = editor.value !== originalContent;
2638
- saveBtn.disabled = !hasChanges;
2639
- if (hasChanges) {
2640
- status.textContent = 'Modified (unsaved)';
2641
- status.className = 'modified';
2642
- } else {
2643
- status.textContent = 'Saved';
2644
- status.className = 'saved';
2645
- }
2646
- }
2647
-
2648
- // Show notification
2649
- function showNotification(msg, isError) {
2650
- notification.textContent = msg;
2651
- notification.className = 'notification show' + (isError ? ' error' : '');
2652
- setTimeout(() => notification.className = 'notification', 3000);
2653
- }
2654
-
2655
- // Event listeners
2656
- editor.addEventListener('input', checkChanges);
2657
- saveBtn.addEventListener('click', saveContent);
2658
-
2659
- // Ctrl+S to save
2660
- document.addEventListener('keydown', e => {
2661
- if ((e.ctrlKey || e.metaKey) && e.key === 's') {
2662
- e.preventDefault();
2663
- if (hasChanges) saveContent();
2664
- }
2665
- });
2666
-
2667
- // Language switcher
2668
- document.querySelectorAll('.lang-btn').forEach(btn => {
2669
- btn.addEventListener('click', () => {
2670
- const newLang = btn.dataset.lang;
2671
- if (newLang !== currentLang) {
2672
- if (hasChanges && !confirm('You have unsaved changes. Switch language anyway?')) {
2673
- return;
2674
- }
2675
- currentLang = newLang;
2676
- localStorage.setItem(STORAGE_KEY, newLang);
2677
- updateLangUI();
2678
- loadContent();
2679
- }
2680
- });
2681
- });
2682
-
2683
- // Warn before leaving with unsaved changes
2684
- window.addEventListener('beforeunload', e => {
2685
- if (hasChanges) {
2686
- e.preventDefault();
2687
- e.returnValue = '';
2688
- }
2689
- });
2690
-
2691
- // Init
2692
- updateLangUI();
2693
- loadContent();
2694
- </script>
2695
- </body>
2696
- </html>`;
2697
- }
2698
- /**
2699
- * Generate ticket view page
2700
- */
2701
- export function generateTicketViewPage(ticketKey, lang) {
2702
- const config = getConfig();
2703
- const ticket = getTicket(config.root, ticketKey);
2704
- const columns = getColumns();
2705
- // 404 page if ticket not found
2706
- if (!ticket) {
2707
- return `<!DOCTYPE html>
2708
- <html lang="${lang}">
2709
- <head>
2710
- <meta charset="UTF-8">
2711
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
2712
- <title>Ticket Not Found - AutoCode</title>
2713
- <style>
2714
- :root {
2715
- --bg: #0a0a0f;
2716
- --text: #f1f5f9;
2717
- --accent: #6366f1;
2718
- }
2719
- * { margin: 0; padding: 0; box-sizing: border-box; }
2720
- body {
2721
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2722
- background: var(--bg);
2723
- color: var(--text);
2724
- min-height: 100vh;
2725
- display: flex;
2726
- align-items: center;
2727
- justify-content: center;
2728
- }
2729
- .not-found {
2730
- text-align: center;
2731
- padding: 48px;
2732
- }
2733
- h1 { font-size: 2rem; margin-bottom: 16px; }
2734
- p { color: #94a3b8; margin-bottom: 24px; }
2735
- a {
2736
- display: inline-block;
2737
- background: var(--accent);
2738
- color: white;
2739
- padding: 12px 24px;
2740
- border-radius: 8px;
2741
- text-decoration: none;
2742
- font-weight: 500;
2743
- }
2744
- a:hover { opacity: 0.9; }
2745
- </style>
2746
- </head>
2747
- <body>
2748
- <div class="not-found">
2749
- <h1>Ticket Not Found</h1>
2750
- <p>Ticket ${escapeHtml(ticketKey)} does not exist or has been archived.</p>
2751
- <a href="/">← Back to Dashboard</a>
2752
- </div>
2753
- </body>
2754
- </html>`;
2755
- }
2756
- const currentColumn = columns.find(c => c.slug === ticket.column_slug);
2757
- const ticketData = JSON.stringify(ticket);
2758
- const columnsData = JSON.stringify(columns);
2759
- return `<!DOCTYPE html>
2760
- <html lang="${lang}">
2761
- <head>
2762
- <meta charset="UTF-8">
2763
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
2764
- <title>${escapeHtml(ticket.title)} - ${ticketKey} - AutoCode</title>
2765
- <style>
2766
- :root {
2767
- --bg: #0a0a0f;
2768
- --bg-secondary: #12121a;
2769
- --bg-tertiary: #1a1a24;
2770
- --text: #f1f5f9;
2771
- --muted: #94a3b8;
2772
- --border: #2a2a3a;
2773
- --accent: #6366f1;
2774
- --blue: #4dabf7;
2775
- --green: #4ade80;
2776
- --yellow: #facc15;
2777
- --orange: #fb923c;
2778
- --red: #f87171;
2779
- --purple: #a78bfa;
2780
- }
2781
- * { margin: 0; padding: 0; box-sizing: border-box; }
2782
- body {
2783
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2784
- background: var(--bg);
2785
- color: var(--text);
2786
- min-height: 100vh;
2787
- }
2788
- .header {
2789
- display: flex;
2790
- align-items: center;
2791
- gap: 24px;
2792
- padding: 16px 24px;
2793
- background: var(--bg-secondary);
2794
- border-bottom: 1px solid var(--border);
2795
- position: sticky;
2796
- top: 0;
2797
- z-index: 100;
2798
- }
2799
- .back-btn {
2800
- color: var(--muted);
2801
- text-decoration: none;
2802
- font-size: 14px;
2803
- display: flex;
2804
- align-items: center;
2805
- gap: 8px;
2806
- }
2807
- .back-btn:hover { color: var(--text); }
2808
- .ticket-header-info {
2809
- flex: 1;
2810
- display: flex;
2811
- align-items: center;
2812
- gap: 16px;
2813
- }
2814
- .ticket-key {
2815
- font-family: 'SF Mono', Monaco, monospace;
2816
- font-size: 12px;
2817
- color: var(--accent);
2818
- background: rgba(99, 102, 241, 0.15);
2819
- padding: 4px 10px;
2820
- border-radius: 4px;
2821
- font-weight: 600;
2822
- }
2823
- .ticket-title {
2824
- font-size: 18px;
2825
- font-weight: 600;
2826
- }
2827
- .lang-selector {
2828
- display: flex;
2829
- gap: 4px;
2830
- }
2831
- .lang-btn {
2832
- background: transparent;
2833
- border: 1px solid var(--border);
2834
- color: var(--muted);
2835
- padding: 6px 12px;
2836
- border-radius: 4px;
2837
- cursor: pointer;
2838
- font-size: 12px;
2839
- font-weight: 500;
2840
- }
2841
- .lang-btn:hover { border-color: var(--accent); color: var(--text); }
2842
- .lang-btn.active { background: var(--accent); border-color: var(--accent); color: white; }
2843
- .ticket-title-input {
2844
- flex: 1;
2845
- background: transparent;
2846
- border: 1px solid transparent;
2847
- color: var(--text);
2848
- font-size: 18px;
2849
- font-weight: 600;
2850
- padding: 4px 8px;
2851
- border-radius: 4px;
2852
- font-family: inherit;
2853
- }
2854
- .ticket-title-input:hover { border-color: var(--border); }
2855
- .ticket-title-input:focus {
2856
- outline: none;
2857
- border-color: var(--accent);
2858
- background: var(--bg-tertiary);
2859
- }
2860
- .section-title {
2861
- display: flex;
2862
- align-items: center;
2863
- justify-content: space-between;
2864
- }
2865
- .btn-edit-toggle {
2866
- background: transparent;
2867
- border: none;
2868
- cursor: pointer;
2869
- font-size: 14px;
2870
- opacity: 0.5;
2871
- transition: opacity 0.2s;
2872
- }
2873
- .btn-edit-toggle:hover { opacity: 1; }
2874
- .btn-edit-toggle.active { opacity: 1; }
2875
- .description-view {
2876
- line-height: 1.7;
2877
- color: var(--text);
2878
- }
2879
- .description-view p { margin-bottom: 12px; }
2880
- .description-view h1, .description-view h2, .description-view h3, .description-view h4 {
2881
- margin: 16px 0 8px;
2882
- font-weight: 600;
2883
- }
2884
- .description-view code {
2885
- background: var(--bg-tertiary);
2886
- padding: 2px 6px;
2887
- border-radius: 4px;
2888
- font-family: 'SF Mono', Monaco, monospace;
2889
- font-size: 13px;
2890
- }
2891
- .description-view pre {
2892
- background: var(--bg-tertiary);
2893
- padding: 12px;
2894
- border-radius: 6px;
2895
- overflow-x: auto;
2896
- }
2897
- .description-view ul, .description-view ol {
2898
- margin: 8px 0;
2899
- padding-left: 24px;
2900
- }
2901
- .description-view a { color: var(--accent); }
2902
- .description-edit {
2903
- width: 100%;
2904
- min-height: 200px;
2905
- padding: 12px;
2906
- background: var(--bg-tertiary);
2907
- border: 1px solid var(--border);
2908
- border-radius: 8px;
2909
- color: var(--text);
2910
- font-family: 'SF Mono', Monaco, monospace;
2911
- font-size: 13px;
2912
- line-height: 1.6;
2913
- resize: vertical;
2914
- }
2915
- .description-edit:focus {
2916
- outline: none;
2917
- border-color: var(--accent);
2918
- }
2919
- .main-content {
2920
- display: flex;
2921
- flex-direction: column;
2922
- gap: 24px;
2923
- padding: 24px 48px;
2924
- }
2925
- .ticket-details { display: flex; flex-direction: column; gap: 24px; }
2926
- .ticket-bottom {
2927
- display: flex;
2928
- flex-direction: column;
2929
- gap: 24px;
2930
- }
2931
- .section {
2932
- background: var(--bg-secondary);
2933
- border: 1px solid var(--border);
2934
- border-radius: 12px;
2935
- padding: 20px;
2936
- }
2937
- .section-title {
2938
- font-size: 12px;
2939
- font-weight: 600;
2940
- text-transform: uppercase;
2941
- letter-spacing: 0.5px;
2942
- color: var(--muted);
2943
- margin-bottom: 16px;
2944
- }
2945
- .ticket-meta {
2946
- display: flex;
2947
- flex-wrap: wrap;
2948
- gap: 12px;
2949
- }
2950
- .meta-badge {
2951
- font-size: 11px;
2952
- font-weight: 600;
2953
- text-transform: uppercase;
2954
- letter-spacing: 0.5px;
2955
- padding: 5px 12px;
2956
- border-radius: 6px;
2957
- }
2958
- .priority-P0 { background: rgba(248, 113, 113, 0.2); color: var(--red); }
2959
- .priority-P1 { background: rgba(251, 146, 60, 0.2); color: var(--orange); }
2960
- .priority-P2 { background: rgba(250, 204, 21, 0.2); color: var(--yellow); }
2961
- .priority-P3 { background: rgba(148, 163, 184, 0.2); color: var(--muted); }
2962
- .column-badge { background: rgba(77, 171, 247, 0.15); color: var(--blue); }
2963
- .semver-badge { background: rgba(167, 139, 250, 0.15); color: var(--purple); }
2964
- .labels-list {
2965
- display: flex;
2966
- flex-wrap: wrap;
2967
- gap: 8px;
2968
- }
2969
- .label-tag {
2970
- font-size: 11px;
2971
- padding: 4px 10px;
2972
- border-radius: 12px;
2973
- background: var(--bg-tertiary);
2974
- color: var(--text);
2975
- border: 1px solid var(--border);
2976
- }
2977
- .description-content {
2978
- line-height: 1.7;
2979
- color: var(--text);
2980
- }
2981
- .description-content p { margin-bottom: 12px; }
2982
- .description-content code {
2983
- background: var(--bg-tertiary);
2984
- padding: 2px 6px;
2985
- border-radius: 4px;
2986
- font-family: 'SF Mono', Monaco, monospace;
2987
- font-size: 13px;
2988
- }
2989
- .criteria-list { list-style: none; }
2990
- .criteria-item {
2991
- padding: 12px 16px;
2992
- background: var(--bg-tertiary);
2993
- border-radius: 8px;
2994
- margin-bottom: 8px;
2995
- display: flex;
2996
- align-items: flex-start;
2997
- gap: 12px;
2998
- }
2999
- .criteria-item::before {
3000
- content: '☐';
3001
- color: var(--muted);
3002
- }
3003
- .history-list { list-style: none; }
3004
- .history-item {
3005
- padding: 12px 0;
3006
- border-bottom: 1px solid var(--border);
3007
- display: flex;
3008
- align-items: center;
3009
- gap: 12px;
3010
- font-size: 13px;
3011
- }
3012
- .history-item:last-child { border-bottom: none; }
3013
- .history-action {
3014
- font-weight: 600;
3015
- text-transform: capitalize;
3016
- }
3017
- .history-from, .history-to {
3018
- padding: 2px 8px;
3019
- background: var(--bg-tertiary);
3020
- border-radius: 4px;
3021
- font-size: 11px;
3022
- }
3023
- .history-date { color: var(--muted); margin-left: auto; font-size: 12px; }
3024
- .btn-prompt {
3025
- background: none;
3026
- border: none;
3027
- cursor: pointer;
3028
- font-size: 14px;
3029
- padding: 2px 6px;
3030
- opacity: 0.6;
3031
- transition: opacity 0.2s;
3032
- }
3033
- .btn-prompt:hover { opacity: 1; }
3034
- .prompt-modal {
3035
- display: none;
3036
- position: fixed;
3037
- top: 0;
3038
- left: 0;
3039
- right: 0;
3040
- bottom: 0;
3041
- background: rgba(0, 0, 0, 0.7);
3042
- z-index: 1000;
3043
- align-items: center;
3044
- justify-content: center;
3045
- }
3046
- .prompt-modal.visible { display: flex; }
3047
- .prompt-modal-content {
3048
- background: var(--bg-primary);
3049
- border-radius: 12px;
3050
- max-width: 900px;
3051
- max-height: 80vh;
3052
- width: 90%;
3053
- display: flex;
3054
- flex-direction: column;
3055
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
3056
- }
3057
- .prompt-modal-header {
3058
- display: flex;
3059
- justify-content: space-between;
3060
- align-items: center;
3061
- padding: 16px 20px;
3062
- border-bottom: 1px solid var(--border);
3063
- }
3064
- .prompt-modal-header h3 { margin: 0; }
3065
- .prompt-modal-close {
3066
- background: none;
3067
- border: none;
3068
- font-size: 24px;
3069
- cursor: pointer;
3070
- color: var(--muted);
3071
- }
3072
- .prompt-modal-close:hover { color: var(--text); }
3073
- .prompt-modal-body {
3074
- padding: 20px;
3075
- overflow-y: auto;
3076
- flex: 1;
3077
- }
3078
- .prompt-modal-body pre {
3079
- white-space: pre-wrap;
3080
- word-wrap: break-word;
3081
- font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
3082
- font-size: 13px;
3083
- line-height: 1.5;
3084
- margin: 0;
3085
- background: var(--bg-secondary);
3086
- padding: 16px;
3087
- border-radius: 8px;
3088
- }
3089
- .log-container {
3090
- max-height: 70vh;
3091
- overflow-y: auto;
3092
- display: flex;
3093
- flex-direction: column;
3094
- gap: 8px;
3095
- }
3096
- .actions-bar {
3097
- display: flex;
3098
- gap: 12px;
3099
- padding: 16px 0;
3100
- border-top: 1px solid var(--border);
3101
- margin-top: 8px;
3102
- }
3103
- .btn {
3104
- padding: 12px 20px;
3105
- border-radius: 8px;
3106
- font-weight: 500;
3107
- font-size: 14px;
3108
- cursor: pointer;
3109
- border: none;
3110
- text-align: center;
3111
- text-decoration: none;
3112
- }
3113
- .btn-primary { background: var(--accent); color: white; }
3114
- .btn-primary:hover { opacity: 0.9; }
3115
- .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
3116
- .btn-secondary { background: var(--bg-tertiary); color: var(--text); border: 1px solid var(--border); }
3117
- .btn-secondary:hover { border-color: var(--accent); }
3118
- .btn-danger { background: rgba(248, 113, 113, 0.15); color: var(--red); border: 1px solid transparent; }
3119
- .btn-danger:hover { border-color: var(--red); }
3120
- .comments-list {
3121
- display: flex;
3122
- flex-direction: column;
3123
- gap: 12px;
3124
- margin-bottom: 16px;
3125
- }
3126
- .comment {
3127
- padding: 16px;
3128
- background: var(--bg-tertiary);
3129
- border-radius: 8px;
3130
- border-left: 3px solid var(--border);
3131
- transition: all 0.2s ease;
3132
- }
3133
- .comment:hover {
3134
- border-left-color: var(--blue);
3135
- }
3136
- .comment-meta {
3137
- display: flex;
3138
- align-items: center;
3139
- flex-wrap: wrap;
3140
- gap: 8px;
3141
- cursor: pointer;
3142
- user-select: none;
3143
- }
3144
- .comment-meta::before {
3145
- content: '▶';
3146
- font-size: 10px;
3147
- color: var(--muted);
3148
- transition: transform 0.2s ease;
3149
- }
3150
- .comment.expanded .comment-meta::before {
3151
- transform: rotate(90deg);
3152
- }
3153
- .comment-source {
3154
- font-size: 10px;
3155
- padding: 3px 8px;
3156
- border-radius: 4px;
3157
- text-transform: uppercase;
3158
- font-weight: 600;
3159
- letter-spacing: 0.5px;
3160
- }
3161
- .comment-source.user { background: #3b82f6; color: white; }
3162
- .comment-source.claude { background: #8b5cf6; color: white; }
3163
- .comment-column {
3164
- font-size: 10px;
3165
- font-weight: 600;
3166
- text-transform: uppercase;
3167
- letter-spacing: 0.5px;
3168
- color: var(--blue);
3169
- padding: 3px 8px;
3170
- background: rgba(77,171,247,0.15);
3171
- border-radius: 4px;
3172
- }
3173
- .comment-date {
3174
- font-size: 11px;
3175
- color: var(--muted);
3176
- }
3177
- .comment-text {
3178
- font-size: 14px;
3179
- line-height: 1.6;
3180
- color: var(--text);
3181
- max-height: 0;
3182
- overflow: hidden;
3183
- transition: max-height 0.3s ease, margin-top 0.3s ease, padding-top 0.3s ease;
3184
- margin-top: 0;
3185
- padding-top: 0;
3186
- }
3187
- .comment.expanded .comment-text {
3188
- max-height: 500px;
3189
- margin-top: 12px;
3190
- padding-top: 12px;
3191
- border-top: 1px solid var(--border);
3192
- }
3193
- .comment-text code {
3194
- background: var(--bg);
3195
- padding: 2px 6px;
3196
- border-radius: 4px;
3197
- font-family: 'SF Mono', Monaco, monospace;
3198
- font-size: 12px;
3199
- }
3200
- .add-comment {
3201
- display: flex;
3202
- flex-direction: column;
3203
- gap: 8px;
3204
- }
3205
- .add-comment textarea {
3206
- width: 100%;
3207
- min-height: 80px;
3208
- padding: 12px;
3209
- background: var(--bg-tertiary);
3210
- border: 1px solid var(--border);
3211
- border-radius: 8px;
3212
- color: var(--text);
3213
- font-family: inherit;
3214
- font-size: 14px;
3215
- resize: vertical;
3216
- }
3217
- .add-comment textarea:focus {
3218
- outline: none;
3219
- border-color: var(--accent);
3220
- }
3221
- .btn-comment {
3222
- align-self: flex-end;
3223
- padding: 8px 16px;
3224
- background: var(--accent);
3225
- color: white;
3226
- border: none;
3227
- border-radius: 6px;
3228
- cursor: pointer;
3229
- font-size: 13px;
3230
- font-weight: 500;
3231
- }
3232
- .btn-comment:hover { opacity: 0.9; }
3233
- .btn-comment:disabled { opacity: 0.5; cursor: not-allowed; }
3234
- .no-comments {
3235
- text-align: center;
3236
- color: var(--muted);
3237
- padding: 24px;
3238
- font-size: 14px;
3239
- }
3240
- .notification {
3241
- position: fixed;
3242
- bottom: 24px;
3243
- right: 24px;
3244
- padding: 12px 20px;
3245
- background: var(--green);
3246
- color: #000;
3247
- border-radius: 8px;
3248
- font-weight: 500;
3249
- transform: translateY(100px);
3250
- opacity: 0;
3251
- transition: all 0.3s ease;
3252
- z-index: 1000;
3253
- }
3254
- .notification.show { transform: translateY(0); opacity: 1; }
3255
- .notification.error { background: var(--red); color: white; }
3256
-
3257
- /* Claude Terminal */
3258
- .claude-section .section-title {
3259
- display: flex;
3260
- align-items: center;
3261
- justify-content: space-between;
3262
- }
3263
- .claude-status {
3264
- font-size: 11px;
3265
- padding: 3px 10px;
3266
- border-radius: 12px;
3267
- background: var(--bg-tertiary);
3268
- color: var(--muted);
3269
- }
3270
- .claude-status.processing {
3271
- color: var(--yellow);
3272
- animation: pulse 1s infinite;
3273
- }
3274
- .claude-status.success { color: var(--green); }
3275
- .claude-status.error { color: var(--red); }
3276
- .claude-log {
3277
- background: #0d1117;
3278
- border: 1px solid var(--border);
3279
- border-radius: 8px;
3280
- padding: 16px;
3281
- max-height: 400px;
3282
- overflow-y: auto;
3283
- font-family: 'SF Mono', Monaco, 'Consolas', monospace;
3284
- font-size: 12px;
3285
- line-height: 1.6;
3286
- white-space: pre-wrap;
3287
- word-break: break-word;
3288
- color: var(--text);
3289
- margin: 0;
3290
- margin-top: 12px;
3291
- }
3292
- /* Formatted log entries */
3293
- .log-entry { margin-bottom: 8px; padding: 8px 12px; border-radius: 4px; border-left: 3px solid transparent; flex-shrink: 0; }
3294
- .log-entry.timestamp { color: #8b949e; font-size: 11px; border-left-color: #484f58; background: transparent; padding: 4px 12px; }
3295
- .log-entry.system { color: #8b949e; border-left-color: #484f58; background: rgba(139,148,158,0.1); }
3296
- .log-entry.user { color: #58a6ff; border-left-color: #58a6ff; background: rgba(88,166,255,0.1); }
3297
- .log-entry.assistant { color: #7ee787; border-left-color: #7ee787; background: rgba(126,231,135,0.1); }
3298
- .log-entry.tool-call { color: #d2a8ff; border-left-color: #d2a8ff; background: rgba(210,168,255,0.1); padding: 12px; }
3299
- .log-entry.tool-result { color: #ffa657; border-left-color: #ffa657; background: rgba(255,166,87,0.1); }
3300
- .log-entry.error { color: #f85149; border-left-color: #f85149; background: rgba(248,81,73,0.1); }
3301
- .log-entry.success { color: #7ee787; border-left-color: #7ee787; background: rgba(126,231,135,0.1); }
3302
- .log-label { font-weight: 600; font-size: 11px; text-transform: uppercase; margin-bottom: 4px; display: block; opacity: 0.8; }
3303
- .log-content { white-space: pre-wrap; word-break: break-word; }
3304
-
3305
- /* Code blocks with line numbers */
3306
- .log-code-block { background: #161b22; border-radius: 6px; overflow: hidden; margin: 8px 0; border: 1px solid #30363d; }
3307
- .log-code-header { background: #21262d; padding: 8px 12px; font-size: 12px; color: #8b949e; border-bottom: 1px solid #30363d; display: flex; align-items: center; gap: 8px; }
3308
- .log-code-header::before { content: ''; display: inline-block; width: 12px; height: 12px; background: #ffa657; border-radius: 50%; }
3309
- .log-code-content { padding: 12px; overflow-x: auto; font-family: 'Fira Code', 'SF Mono', Monaco, monospace; font-size: 12px; line-height: 1.5; max-height: 400px; overflow-y: auto; }
3310
- .log-code-line { display: flex; min-height: 20px; }
3311
- .log-line-number { color: #484f58; text-align: right; padding-right: 16px; user-select: none; min-width: 40px; flex-shrink: 0; }
3312
- .log-line-content { color: #c9d1d9; white-space: pre; }
3313
-
3314
- /* Message cards */
3315
- .log-message-card { background: #161b22; border-radius: 8px; margin: 12px 0; overflow: hidden; border: 1px solid #30363d; flex-shrink: 0; }
3316
- .log-message-header { padding: 10px 14px; display: flex; align-items: center; gap: 8px; font-weight: 500; font-size: 13px; }
3317
- .log-message-header.assistant-header { background: linear-gradient(135deg, #238636 0%, #2ea043 100%); color: white; }
3318
- .log-message-header.user-header { background: linear-gradient(135deg, #1f6feb 0%, #388bfd 100%); color: white; }
3319
- .log-message-header.tool-header { background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%); color: white; }
3320
- .log-message-header.result-header { background: linear-gradient(135deg, #f97316 0%, #fb923c 100%); color: white; }
3321
- .log-message-body { padding: 14px; border-top: 1px solid #30363d; color: #c9d1d9; white-space: pre-wrap; word-break: break-word; }
3322
-
3323
- /* Tool badges */
3324
- .log-tool-badge { display: inline-flex; align-items: center; gap: 6px; background: rgba(139,92,246,0.2); color: #a78bfa; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
3325
- .log-tool-input { background: #0d1117; border-radius: 4px; padding: 8px 12px; margin-top: 8px; font-family: 'Fira Code', monospace; font-size: 11px; color: #8b949e; max-height: 150px; overflow: auto; white-space: pre-wrap; }
3326
-
3327
- /* Collapsible raw JSON */
3328
- .log-raw-details { margin: 8px 0; }
3329
- .log-raw-summary { cursor: pointer; color: #8b949e; font-size: 12px; padding: 8px; background: rgba(139,148,158,0.1); border-radius: 4px; }
3330
- .log-raw-summary:hover { background: rgba(139,148,158,0.2); }
3331
- .log-raw-json { background: #0d1117; border-radius: 4px; padding: 12px; margin-top: 8px; font-size: 11px; color: #8b949e; max-height: 200px; overflow: auto; white-space: pre-wrap; }
3332
- @keyframes pulse {
3333
- 0%, 100% { opacity: 1; }
3334
- 50% { opacity: 0.5; }
3335
- }
3336
- </style>
3337
- </head>
3338
- <body>
3339
- <header class="header">
3340
- <a href="/" class="back-btn">← Dashboard</a>
3341
- <div class="ticket-header-info">
3342
- <span class="ticket-key">${escapeHtml(ticketKey)}</span>
3343
- <input type="text" class="ticket-title-input" id="ticket-title" value="${escapeHtml(ticket.title)}" />
3344
- </div>
3345
- <div class="lang-selector" id="lang-selector">
3346
- <button class="lang-btn" data-lang="en">EN</button>
3347
- <button class="lang-btn" data-lang="fr">FR</button>
3348
- </div>
3349
- </header>
3350
-
3351
- <main class="main-content">
3352
- <div class="ticket-details">
3353
- <!-- Meta info -->
3354
- <div class="section">
3355
- <div class="section-title" data-i18n="ticketView.meta">Meta</div>
3356
- <div class="ticket-meta">
3357
- <span class="meta-badge priority-${ticket.priority}">${ticket.priority}</span>
3358
- <span class="meta-badge column-badge">${escapeHtml(currentColumn?.name || ticket.column_slug)}</span>
3359
- <span class="meta-badge semver-badge">${ticket.semver}</span>
3360
- </div>
3361
- </div>
3362
-
3363
- <!-- Labels -->
3364
- ${ticket.labels && ticket.labels.length > 0 ? `
3365
- <div class="section">
3366
- <div class="section-title" data-i18n="ticketView.labels">Labels</div>
3367
- <div class="labels-list">
3368
- ${ticket.labels.map(label => `<span class="label-tag">${escapeHtml(label)}</span>`).join('')}
3369
- </div>
3370
- </div>
3371
- ` : ''}
3372
-
3373
- <!-- Description -->
3374
- <div class="section">
3375
- <div class="section-title">
3376
- <span data-i18n="ticketView.description">Description</span>
3377
- <button class="btn-edit-toggle" id="btn-edit-description" onclick="toggleDescriptionEdit()">✏️</button>
3378
- </div>
3379
- <div class="description-view" id="description-view"></div>
3380
- <textarea class="description-edit" id="description-edit" style="display:none" placeholder="Description (Markdown)">${escapeHtml(ticket.description || '')}</textarea>
3381
- </div>
3382
-
3383
- <!-- Acceptance Criteria -->
3384
- ${ticket.acceptance_criteria && ticket.acceptance_criteria.length > 0 ? `
3385
- <div class="section">
3386
- <div class="section-title" data-i18n="ticketView.criteria">Acceptance Criteria</div>
3387
- <ul class="criteria-list">
3388
- ${ticket.acceptance_criteria.map(c => `<li class="criteria-item">${escapeHtml(c)}</li>`).join('')}
3389
- </ul>
3390
- </div>
3391
- ` : ''}
3392
-
3393
- <!-- History -->
3394
- ${ticket.history && ticket.history.length > 0 ? `
3395
- <div class="section">
3396
- <div class="section-title" data-i18n="ticketView.history">History</div>
3397
- <ul class="history-list">
3398
- ${ticket.history.map(h => {
3399
- const date = new Date(h.at);
3400
- const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
3401
- const showPromptBtn = h.to && h.action !== 'created' ? `<a href="/ticket/${ticketKey}/${escapeHtml(h.to)}/prompt" class="btn-prompt" title="View prompt">📜</a>` : '';
3402
- const showLogBtn = h.to && h.action !== 'created' ? `<a href="/ticket/${ticketKey}/${escapeHtml(h.to)}/terminal" class="btn-prompt" title="View terminal log">🖥️</a>` : '';
3403
- return `<li class="history-item">
3404
- <span class="history-action">${h.action}</span>
3405
- ${h.from ? `<span class="history-from">${escapeHtml(h.from)}</span> →` : ''}
3406
- <span class="history-to">${escapeHtml(h.to)}</span>
3407
- ${showPromptBtn}
3408
- ${showLogBtn}
3409
- <span class="history-date">${dateStr}</span>
3410
- </li>`;
3411
- }).join('')}
3412
- </ul>
3413
- </div>
3414
- ` : ''}
3415
-
3416
- <!-- Actions bar -->
3417
- <div class="actions-bar">
3418
- <button class="btn btn-primary" id="btn-save" onclick="saveTicket()" data-i18n="ticketView.save">Save</button>
3419
- <button class="btn btn-secondary" onclick="advanceTicket()" data-i18n="ticketView.moveNext">Move to next column</button>
3420
- <button class="btn btn-danger" onclick="archiveTicket()" data-i18n="ticketView.archive">Archive</button>
3421
- </div>
3422
- </div>
3423
-
3424
- <!-- Bottom section: Comments & Claude Terminal -->
3425
- <div class="ticket-bottom">
3426
- <!-- Comments -->
3427
- <div class="section comments-section">
3428
- <div class="section-title"><span data-i18n="ticketView.comments">Comments</span> (<span id="comments-count">${ticket.comments?.length || 0}</span>)</div>
3429
- <div class="comments-list" id="comments-list"></div>
3430
- <div class="add-comment">
3431
- <textarea id="new-comment" placeholder="Add a comment..." data-i18n-placeholder="ticketView.addComment"></textarea>
3432
- <button class="btn-comment" onclick="addComment()" data-i18n="btn.add">Add</button>
3433
- </div>
3434
- </div>
3435
-
3436
- <!-- Claude Terminal -->
3437
- <div class="section claude-section" id="claude-section">
3438
- <div class="section-title">
3439
- <span data-i18n="ticketView.claudeTerminal">Claude Terminal</span>
3440
- <span class="claude-status" id="claude-status" data-i18n="status.waiting">Waiting</span>
3441
- </div>
3442
- <pre class="claude-log" id="claude-log"></pre>
3443
- </div>
3444
- </div>
3445
- </main>
3446
-
3447
- <div class="notification" id="notification"></div>
3448
-
3449
- <!-- Prompt Modal -->
3450
- <div class="prompt-modal" id="prompt-modal" onclick="closePromptModal(event)">
3451
- <div class="prompt-modal-content" onclick="event.stopPropagation()">
3452
- <div class="prompt-modal-header">
3453
- <h3 id="prompt-modal-title">Prompt</h3>
3454
- <button class="prompt-modal-close" onclick="closePromptModal()">&times;</button>
3455
- </div>
3456
- <div class="prompt-modal-body">
3457
- <div id="prompt-modal-content" class="log-container"></div>
3458
- </div>
3459
- </div>
3460
- </div>
3461
-
3462
- <script>
3463
- const TICKET_KEY = '${ticketKey}';
3464
- const TICKET = ${ticketData};
3465
- const COLUMNS = ${columnsData};
3466
- const STORAGE_KEY = 'autocode-lang';
3467
-
3468
- let currentLang = localStorage.getItem(STORAGE_KEY) || 'fr';
3469
- let currentComments = TICKET.comments || [];
3470
-
3471
- const translations = {
3472
- en: {
3473
- 'ticketView.meta': 'Meta',
3474
- 'ticketView.labels': 'Labels',
3475
- 'ticketView.description': 'Description',
3476
- 'ticketView.criteria': 'Acceptance Criteria',
3477
- 'ticketView.history': 'History',
3478
- 'ticketView.actions': 'Actions',
3479
- 'ticketView.save': 'Save',
3480
- 'ticketView.moveNext': 'Move to next column',
3481
- 'ticketView.archive': 'Archive',
3482
- 'ticketView.confirmMove': 'Move this ticket to the next column?',
3483
- 'ticketView.confirmArchive': 'Archive this ticket?',
3484
- 'ticketView.comments': 'Comments',
3485
- 'ticketView.addComment': 'Add a comment...',
3486
- 'ticketView.noComments': 'No comments yet',
3487
- 'ticketView.claudeTerminal': 'Claude Terminal',
3488
- 'ticketView.noDescription': 'No description',
3489
- 'ticketView.noLog': 'No log yet. Waiting for Claude processing...',
3490
- 'ticketView.loadingPrompt': 'Loading prompt...',
3491
- 'ticketView.promptError': 'Error',
3492
- 'btn.add': 'Add',
3493
- 'btn.sending': 'Sending...',
3494
- 'btn.saving': 'Saving...',
3495
- 'status.waiting': 'Waiting',
3496
- 'status.processing': 'Processing...',
3497
- 'status.completed': 'Completed',
3498
- 'status.failed': 'Failed',
3499
- 'notify.commentAdded': 'Comment added',
3500
- 'notify.ticketAdvanced': 'Ticket advanced',
3501
- 'notify.ticketArchived': 'Ticket archived',
3502
- 'notify.ticketSaved': 'Ticket saved',
3503
- 'notify.error': 'Error'
3504
- },
3505
- fr: {
3506
- 'ticketView.meta': 'Méta',
3507
- 'ticketView.labels': 'Labels',
3508
- 'ticketView.description': 'Description',
3509
- 'ticketView.criteria': 'Critères d\\'acceptation',
3510
- 'ticketView.history': 'Historique',
3511
- 'ticketView.actions': 'Actions',
3512
- 'ticketView.save': 'Sauvegarder',
3513
- 'ticketView.moveNext': 'Déplacer vers la colonne suivante',
3514
- 'ticketView.archive': 'Archiver',
3515
- 'ticketView.confirmMove': 'Déplacer ce ticket vers la colonne suivante ?',
3516
- 'ticketView.confirmArchive': 'Archiver ce ticket ?',
3517
- 'ticketView.comments': 'Commentaires',
3518
- 'ticketView.addComment': 'Ajouter un commentaire...',
3519
- 'ticketView.noComments': 'Aucun commentaire',
3520
- 'ticketView.claudeTerminal': 'Terminal Claude',
3521
- 'ticketView.noDescription': 'Aucune description',
3522
- 'ticketView.noLog': 'Aucun log. En attente du traitement Claude...',
3523
- 'ticketView.loadingPrompt': 'Chargement du prompt...',
3524
- 'ticketView.promptError': 'Erreur',
3525
- 'btn.add': 'Ajouter',
3526
- 'btn.sending': 'Envoi...',
3527
- 'btn.saving': 'Sauvegarde...',
3528
- 'status.waiting': 'En attente',
3529
- 'status.processing': 'En cours...',
3530
- 'status.completed': 'Terminé',
3531
- 'status.failed': 'Échec',
3532
- 'notify.commentAdded': 'Commentaire ajouté',
3533
- 'notify.ticketAdvanced': 'Ticket avancé',
3534
- 'notify.ticketArchived': 'Ticket archivé',
3535
- 'notify.ticketSaved': 'Ticket sauvegardé',
3536
- 'notify.error': 'Erreur'
3537
- }
3538
- };
3539
-
3540
- function t(key) {
3541
- return translations[currentLang]?.[key] || translations['en'][key] || key;
3542
- }
3543
-
3544
- function escapeHtml(text) {
3545
- if (!text) return '';
3546
- const div = document.createElement('div');
3547
- div.textContent = text;
3548
- return div.innerHTML;
3549
- }
3550
-
3551
- function renderMarkdown(text) {
3552
- if (!text) return '';
3553
- let html = escapeHtml(text);
3554
- html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
3555
- html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
3556
- html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>');
3557
- html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
3558
- html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
3559
- html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
3560
- html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
3561
- html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
3562
- html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
3563
- html = html.replace(/\\n/g, '<br>');
3564
- return html;
3565
- }
3566
-
3567
- function updateLangUI() {
3568
- document.querySelectorAll('.lang-btn').forEach(btn => {
3569
- btn.classList.toggle('active', btn.dataset.lang === currentLang);
3570
- });
3571
- document.querySelectorAll('[data-i18n]').forEach(el => {
3572
- el.textContent = t(el.dataset.i18n);
3573
- });
3574
- document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
3575
- el.placeholder = t(el.dataset.i18nPlaceholder);
3576
- });
3577
- }
3578
-
3579
- function renderComments() {
3580
- const list = document.getElementById('comments-list');
3581
- const count = document.getElementById('comments-count');
3582
- count.textContent = currentComments.length;
3583
-
3584
- if (currentComments.length === 0) {
3585
- list.innerHTML = '<div class="no-comments">' + t('ticketView.noComments') + '</div>';
3586
- return;
3587
- }
3588
-
3589
- const sorted = [...currentComments].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
3590
- list.innerHTML = sorted.map((comment, index) => {
3591
- const date = new Date(comment.created_at);
3592
- const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
3593
- const source = comment.source || 'user';
3594
- const sourceBadge = source === 'claude'
3595
- ? '<span class="comment-source claude">Claude</span>'
3596
- : '<span class="comment-source user">User</span>';
3597
- return '<div class="comment" id="comment-' + index + '">' +
3598
- '<div class="comment-meta" onclick="toggleComment(' + index + ')">' + sourceBadge + '<span class="comment-column">' + (comment.column || 'N/A') + '</span>' +
3599
- '<span class="comment-date">' + dateStr + '</span></div>' +
3600
- '<div class="comment-text">' + renderMarkdown(comment.text) + '</div></div>';
3601
- }).join('');
3602
- }
3603
-
3604
- function toggleComment(index) {
3605
- const comments = document.querySelectorAll('.comment');
3606
- comments.forEach((comment, i) => {
3607
- if (i === index) {
3608
- comment.classList.toggle('expanded');
3609
- } else {
3610
- comment.classList.remove('expanded');
3611
- }
3612
- });
3613
- }
3614
-
3615
- async function addComment() {
3616
- const textarea = document.getElementById('new-comment');
3617
- const text = textarea.value.trim();
3618
- if (!text) return;
3619
-
3620
- const btn = document.querySelector('.btn-comment');
3621
- btn.disabled = true;
3622
- btn.textContent = t('btn.sending');
3623
-
3624
- try {
3625
- const res = await fetch('/api/tickets/' + TICKET_KEY + '/comments', {
3626
- method: 'POST',
3627
- headers: { 'Content-Type': 'application/json' },
3628
- body: JSON.stringify({ text, source: 'user' })
3629
- });
3630
- const result = await res.json();
3631
- textarea.value = '';
3632
- if (result.success && result.data && result.data.comments) {
3633
- currentComments = result.data.comments;
3634
- renderComments();
3635
- }
3636
- showNotification(t('notify.commentAdded'));
3637
- } catch (e) {
3638
- showNotification(t('notify.error') + ': ' + e.message, true);
3639
- } finally {
3640
- btn.disabled = false;
3641
- btn.textContent = t('btn.add');
3642
- }
3643
- }
3644
-
3645
- let isEditingDescription = false;
3646
-
3647
- function renderDescription() {
3648
- const view = document.getElementById('description-view');
3649
- const edit = document.getElementById('description-edit');
3650
- const description = edit.value || '';
3651
- if (description) {
3652
- view.innerHTML = renderMarkdown(description);
3653
- } else {
3654
- view.innerHTML = '<span style="color: var(--muted)">' + t('ticketView.noDescription') + '</span>';
3655
- }
3656
- }
3657
-
3658
- function toggleDescriptionEdit() {
3659
- isEditingDescription = !isEditingDescription;
3660
- const view = document.getElementById('description-view');
3661
- const edit = document.getElementById('description-edit');
3662
- const btn = document.getElementById('btn-edit-description');
3663
-
3664
- if (isEditingDescription) {
3665
- view.style.display = 'none';
3666
- edit.style.display = 'block';
3667
- btn.classList.add('active');
3668
- btn.textContent = '✓';
3669
- edit.focus();
3670
- } else {
3671
- view.style.display = 'block';
3672
- edit.style.display = 'none';
3673
- btn.classList.remove('active');
3674
- btn.textContent = '✏️';
3675
- renderDescription();
3676
- }
3677
- }
3678
-
3679
- async function saveTicket() {
3680
- const btn = document.getElementById('btn-save');
3681
- btn.disabled = true;
3682
- btn.textContent = t('btn.saving');
3683
-
3684
- try {
3685
- const res = await fetch('/api/tickets/' + TICKET_KEY, {
3686
- method: 'PATCH',
3687
- headers: { 'Content-Type': 'application/json' },
3688
- body: JSON.stringify({
3689
- title: document.getElementById('ticket-title').value,
3690
- description: document.getElementById('description-edit').value
3691
- })
3692
- });
3693
- const result = await res.json();
3694
- if (result.success) {
3695
- showNotification(t('notify.ticketSaved'));
3696
- if (isEditingDescription) {
3697
- toggleDescriptionEdit();
3698
- }
3699
- } else {
3700
- showNotification(t('notify.error') + ': ' + result.error, true);
3701
- }
3702
- } catch (e) {
3703
- showNotification(t('notify.error') + ': ' + e.message, true);
3704
- } finally {
3705
- btn.disabled = false;
3706
- btn.textContent = t('ticketView.save');
3707
- }
3708
- }
3709
-
3710
- async function advanceTicket() {
3711
- if (!confirm(t('ticketView.confirmMove'))) return;
3712
- try {
3713
- const res = await fetch('/api/tickets/' + TICKET_KEY + '/next', {
3714
- method: 'POST',
3715
- headers: { 'Content-Type': 'application/json' },
3716
- body: JSON.stringify({ lang: currentLang })
3717
- });
3718
- const result = await res.json();
3719
- if (result.success) {
3720
- showNotification(t('notify.ticketAdvanced'));
3721
- setTimeout(() => location.reload(), 1000);
3722
- } else {
3723
- showNotification(t('notify.error') + ': ' + result.error, true);
3724
- }
3725
- } catch (e) {
3726
- showNotification(t('notify.error') + ': ' + e.message, true);
3727
- }
3728
- }
3729
-
3730
- async function archiveTicket() {
3731
- if (!confirm(t('ticketView.confirmArchive'))) return;
3732
- try {
3733
- const res = await fetch('/api/tickets/' + TICKET_KEY + '/archive', {
3734
- method: 'POST',
3735
- headers: { 'Content-Type': 'application/json' },
3736
- body: JSON.stringify({ lang: currentLang })
3737
- });
3738
- const result = await res.json();
3739
- if (result.success) {
3740
- showNotification(t('notify.ticketArchived'));
3741
- setTimeout(() => location.href = '/', 1000);
3742
- } else {
3743
- showNotification(t('notify.error') + ': ' + result.error, true);
3744
- }
3745
- } catch (e) {
3746
- showNotification(t('notify.error') + ': ' + e.message, true);
3747
- }
3748
- }
3749
-
3750
- function showNotification(msg, isError) {
3751
- const notification = document.getElementById('notification');
3752
- notification.textContent = msg;
3753
- notification.className = 'notification show' + (isError ? ' error' : '');
3754
- setTimeout(() => notification.className = 'notification', 3000);
3755
- }
3756
-
3757
- // Language switcher
3758
- document.querySelectorAll('.lang-btn').forEach(btn => {
3759
- btn.addEventListener('click', () => {
3760
- const newLang = btn.dataset.lang;
3761
- if (newLang !== currentLang) {
3762
- currentLang = newLang;
3763
- localStorage.setItem(STORAGE_KEY, newLang);
3764
- updateLangUI();
3765
- renderComments();
3766
- }
3767
- });
3768
- });
3769
-
3770
- // ========================================
3771
- // WEBSOCKET & CLAUDE LOG
3772
- // ========================================
3773
- let ws;
3774
- let logPollingInterval = null;
3775
-
3776
- function connectWebSocket() {
3777
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
3778
- ws = new WebSocket(protocol + '//' + location.host + '/ws');
3779
-
3780
- ws.onmessage = (event) => {
3781
- try {
3782
- const data = JSON.parse(event.data);
3783
- switch (data.type) {
3784
- case 'ticket_updated':
3785
- if (data.key === TICKET_KEY) {
3786
- location.reload();
3787
- }
3788
- break;
3789
- case 'claude_start':
3790
- if (data.ticket === TICKET_KEY) {
3791
- onClaudeStart();
3792
- }
3793
- break;
3794
- case 'claude_stream':
3795
- if (data.ticket === TICKET_KEY) {
3796
- fetchLog();
3797
- }
3798
- break;
3799
- case 'claude_end':
3800
- if (data.ticket === TICKET_KEY) {
3801
- onClaudeEnd(data.success, data.duration);
3802
- }
3803
- break;
3804
- }
3805
- } catch (e) {
3806
- console.error('WebSocket message error:', e);
3807
- }
3808
- };
3809
-
3810
- ws.onclose = () => {
3811
- setTimeout(connectWebSocket, 3000);
3812
- };
3813
- }
3814
-
3815
- function escapeHtml(str) {
3816
- if (typeof str !== 'string') return '';
3817
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3818
- }
3819
-
3820
- function formatCodeBlock(content, filename) {
3821
- const lines = content.split(/\\\\n|\\n/);
3822
- // Détecter le langage depuis le nom de fichier
3823
- let lang = 'plaintext';
3824
- if (filename) {
3825
- const ext = filename.split('.').pop().toLowerCase();
3826
- const langMap = {
3827
- 'js': 'javascript', 'ts': 'typescript', 'vue': 'html', 'jsx': 'javascript',
3828
- 'tsx': 'typescript', 'py': 'python', 'rb': 'ruby', 'java': 'java',
3829
- 'go': 'go', 'rs': 'rust', 'cpp': 'cpp', 'c': 'c', 'h': 'c',
3830
- 'css': 'css', 'scss': 'scss', 'html': 'html', 'json': 'json',
3831
- 'md': 'markdown', 'sh': 'bash', 'yml': 'yaml', 'yaml': 'yaml'
3832
- };
3833
- lang = langMap[ext] || 'plaintext';
3834
- }
3835
- // Extraire le code sans les numéros de ligne pour highlight.js
3836
- let codeLines = [];
3837
- for (const line of lines) {
3838
- const match = line.match(/^\\s*\\d+[→|](.*)$/);
3839
- if (match) {
3840
- codeLines.push(match[1]);
3841
- } else if (line.trim()) {
3842
- codeLines.push(line);
3843
- }
3844
- }
3845
- const codeContent = codeLines.join('\\n');
3846
- const lineCount = codeLines.length;
3847
- const preview = filename || (lineCount + ' lignes');
3848
-
3849
- let html = '<details class="log-code-block">';
3850
- html += '<summary class="log-code-header"><span class="code-lang">' + lang + '</span> ' + escapeHtml(preview) + '</summary>';
3851
- html += '<pre><code class="language-' + lang + '">' + escapeHtml(codeContent) + '</code></pre>';
3852
- html += '</details>';
3853
- return html;
3854
- }
3855
-
3856
- function formatLogContent(rawContent) {
3857
- if (!rawContent) return '';
3858
- // Supprimer les balises <system-reminder> et leur contenu
3859
- let cleanedContent = rawContent.replace(/<system-reminder>[\\s\\S]*?<\\/system-reminder>/gi, '');
3860
- const lines = cleanedContent.split('\\n');
3861
- const entries = [];
3862
-
3863
- for (const line of lines) {
3864
- if (!line.trim()) continue;
3865
-
3866
- const timestampMatch = line.match(/^\\[(\\d{4}-\\d{2}-\\d{2}T[^\\]]+)\\]\\s*(.*)$/);
3867
- if (timestampMatch) {
3868
- const date = new Date(timestampMatch[1]);
3869
- const timeStr = date.toLocaleTimeString();
3870
- entries.push('<div class="log-entry timestamp">' + timeStr + ' - ' + escapeHtml(timestampMatch[2]) + '</div>');
3871
- continue;
3872
- }
3873
-
3874
- // [RAW] - Parse avec regex (pas JSON.parse car les lignes sont coupées)
3875
- if (line.startsWith('[RAW] ')) {
3876
- const raw = line.slice(6);
3877
-
3878
- // Ignorer les lignes de continuation (ne commencent pas par {)
3879
- if (!raw.startsWith('{')) {
3880
- continue;
3881
- }
3882
-
3883
- // Extraire tool_result content
3884
- if (raw.includes('"type":"tool_result"')) {
3885
- const contentStart = raw.indexOf('"content":"');
3886
- if (contentStart !== -1) {
3887
- // Extraire le contenu après "content":"
3888
- let content = raw.slice(contentStart + 11);
3889
- // Enlever le reste du JSON (approximatif car tronqué)
3890
- const endQuote = content.lastIndexOf('"');
3891
- if (endQuote > 0) content = content.slice(0, endQuote);
3892
- // Décoder les échappements
3893
- const decoded = content.replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"').replace(/\\\\t/g, '\\t');
3894
- // Vérifier si c'est du code avec numéros de ligne
3895
- if (/^\\s*\\d+[→|]/.test(decoded)) {
3896
- 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>');
3897
- } else {
3898
- // Résultat texte simple (liste de fichiers, etc.)
3899
- const lines = decoded.split('\\n').slice(0, 20); // Limiter à 20 lignes
3900
- const truncated = decoded.split('\\n').length > 20 ? '<div style="color:#8b949e;font-style:italic">... (truncated)</div>' : '';
3901
- entries.push('<div class="log-message-card"><div class="log-message-header result-header">Tool Result</div><div class="log-message-body" style="font-family:monospace;font-size:12px;white-space:pre-wrap;max-height:200px;overflow-y:auto">' + escapeHtml(lines.join('\\n')) + truncated + '</div></div>');
3902
- }
3903
- continue;
3904
- }
3905
- }
3906
-
3907
- // Extraire message assistant texte
3908
- const textMatch = raw.match(/"type":"text","text":"([^"]+)"/);
3909
- if (textMatch) {
3910
- const decoded = textMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"');
3911
- entries.push('<div class="log-message-card"><div class="log-message-header assistant-header">Assistant</div><div class="log-message-body">' + escapeHtml(decoded) + '</div></div>');
3912
- continue;
3913
- }
3914
-
3915
- // Extraire tool_use
3916
- const toolMatch = raw.match(/"type":"tool_use"[^}]*"name":"([^"]+)"/);
3917
- if (toolMatch) {
3918
- entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(toolMatch[1]) + '</span></div>');
3919
- continue;
3920
- }
3921
-
3922
- // Ignorer les autres RAW
3923
- continue;
3924
- }
3925
-
3926
- if (line.startsWith('[SYSTEM]')) {
3927
- entries.push('<div class="log-entry system"><span class="log-label">System</span><div class="log-content">' + escapeHtml(line.slice(9)) + '</div></div>');
3928
- continue;
3929
- }
3930
- if (line.startsWith('[ASSISTANT]')) {
3931
- entries.push('<div class="log-message-card"><div class="log-message-header assistant-header">Assistant</div><div class="log-message-body">' + escapeHtml(line.slice(12)) + '</div></div>');
3932
- continue;
3933
- }
3934
- if (line.startsWith('[TOOL]')) {
3935
- entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(line.slice(7)) + '</span></div>');
3936
- continue;
3937
- }
3938
- if (line.startsWith('[RESULT]')) {
3939
- const content = line.slice(9);
3940
- if (/^\\s*\\d+[→|]/.test(content)) {
3941
- 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>');
3942
- } else {
3943
- 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>');
3944
- }
3945
- continue;
3946
- }
3947
- if (line.startsWith('[ERROR]')) {
3948
- entries.push('<div class="log-entry error"><span class="log-label">Error</span><div class="log-content">' + escapeHtml(line.slice(8)) + '</div></div>');
3949
- continue;
3950
- }
3951
-
3952
- entries.push('<div class="log-entry system">' + escapeHtml(line) + '</div>');
3953
- }
3954
-
3955
- return entries.join('');
3956
- }
3957
-
3958
- function startLogPolling() {
3959
- stopLogPolling();
3960
- logPollingInterval = setInterval(fetchLog, 1000);
3961
- fetchLog();
3962
- }
3963
-
3964
- function stopLogPolling() {
3965
- if (logPollingInterval) {
3966
- clearInterval(logPollingInterval);
3967
- logPollingInterval = null;
3968
- }
3969
- }
3970
-
3971
- async function fetchLog() {
3972
- try {
3973
- const res = await fetch('/api/tickets/' + TICKET_KEY + '/log');
3974
- const json = await res.json();
3975
- const log = document.getElementById('claude-log');
3976
-
3977
- if (json.success && json.data && (json.data.exists || json.data.content)) {
3978
- log.innerHTML = formatLogContent(json.data.content || '');
3979
- log.scrollTop = log.scrollHeight;
3980
- } else {
3981
- log.innerHTML = '<div class="log-entry system">' + t('ticketView.noLog') + '</div>';
3982
- }
3983
- } catch (e) {
3984
- console.error('Log fetch error:', e);
3985
- }
3986
- }
3987
-
3988
- function onClaudeStart() {
3989
- const status = document.getElementById('claude-status');
3990
- status.className = 'claude-status processing';
3991
- status.textContent = t('status.processing');
3992
- startLogPolling();
3993
- }
3994
-
3995
- function onClaudeEnd(success, duration) {
3996
- stopLogPolling();
3997
- fetchLog();
3998
- const status = document.getElementById('claude-status');
3999
- if (success) {
4000
- status.className = 'claude-status success';
4001
- status.textContent = t('status.completed') + ' (' + (duration / 1000).toFixed(1) + 's)';
4002
- } else {
4003
- status.className = 'claude-status error';
4004
- status.textContent = t('status.failed');
4005
- }
4006
- }
4007
-
4008
- // Prompt Modal
4009
- async function showPrompt(columnSlug) {
4010
- const modal = document.getElementById('prompt-modal');
4011
- const title = document.getElementById('prompt-modal-title');
4012
- const content = document.getElementById('prompt-modal-content');
4013
-
4014
- title.textContent = t('ticketView.loadingPrompt');
4015
- content.textContent = '';
4016
- modal.classList.add('visible');
4017
-
4018
- try {
4019
- const res = await fetch('/api/tickets/' + TICKET_KEY + '/prompt/' + columnSlug);
4020
- const json = await res.json();
4021
- if (json.success) {
4022
- title.textContent = 'Prompt → ' + json.data.column;
4023
- content.textContent = json.data.prompt;
4024
- } else {
4025
- title.textContent = t('ticketView.promptError');
4026
- content.textContent = json.error || 'Error loading prompt';
4027
- }
4028
- } catch (e) {
4029
- title.textContent = t('ticketView.promptError');
4030
- content.textContent = e.message;
4031
- }
4032
- }
4033
-
4034
- function closePromptModal(event) {
4035
- if (event && event.target !== event.currentTarget) return;
4036
- document.getElementById('prompt-modal').classList.remove('visible');
4037
- }
4038
-
4039
- // Log Modal (reuses prompt modal)
4040
- async function showLog(columnSlug) {
4041
- const modal = document.getElementById('prompt-modal');
4042
- const title = document.getElementById('prompt-modal-title');
4043
- const content = document.getElementById('prompt-modal-content');
4044
-
4045
- title.textContent = t('ticketView.loadingLog') || 'Loading log...';
4046
- content.textContent = '';
4047
- modal.classList.add('visible');
4048
-
4049
- try {
4050
- const res = await fetch('/api/tickets/' + TICKET_KEY + '/log/' + columnSlug);
4051
- const json = await res.json();
4052
- if (json.success) {
4053
- title.textContent = 'Terminal → ' + columnSlug;
4054
- if (json.data.content) {
4055
- content.innerHTML = formatLogContent(json.data.content);
4056
- } else {
4057
- content.textContent = t('ticketView.noLog') || 'No log available';
4058
- }
4059
- } else {
4060
- title.textContent = t('ticketView.logError') || 'Log Error';
4061
- content.textContent = json.error || 'Error loading log';
4062
- }
4063
- } catch (e) {
4064
- title.textContent = t('ticketView.logError') || 'Log Error';
4065
- content.textContent = e.message;
4066
- }
4067
- }
4068
-
4069
- // Init
4070
- updateLangUI();
4071
- renderDescription();
4072
- renderComments();
4073
- connectWebSocket();
4074
- fetchLog();
4075
- </script>
4076
- </body>
4077
- </html>`;
4078
- }
4079
- /**
4080
- * Generate the column terminal page HTML
4081
- */
4082
- export function generateColumnTerminalPage(ticketKey, columnSlug, lang) {
4083
- const config = getConfig();
4084
- const ticket = getTicket(config.root, ticketKey);
4085
- // 404 page if ticket not found
4086
- if (!ticket) {
4087
- return `<!DOCTYPE html>
4088
- <html lang="${lang}">
4089
- <head>
4090
- <meta charset="UTF-8">
4091
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
4092
- <title>Ticket Not Found - AutoCode</title>
4093
- <style>
4094
- :root {
4095
- --bg: #0a0a0f;
4096
- --text: #f1f5f9;
4097
- --accent: #6366f1;
4098
- }
4099
- * { margin: 0; padding: 0; box-sizing: border-box; }
4100
- body {
4101
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4102
- background: var(--bg);
4103
- color: var(--text);
4104
- min-height: 100vh;
4105
- display: flex;
4106
- align-items: center;
4107
- justify-content: center;
4108
- }
4109
- .not-found {
4110
- text-align: center;
4111
- padding: 48px;
4112
- }
4113
- h1 { font-size: 2rem; margin-bottom: 16px; }
4114
- p { color: #94a3b8; margin-bottom: 24px; }
4115
- a {
4116
- display: inline-block;
4117
- background: var(--accent);
4118
- color: white;
4119
- padding: 12px 24px;
4120
- border-radius: 8px;
4121
- text-decoration: none;
4122
- font-weight: 500;
4123
- }
4124
- a:hover { opacity: 0.9; }
4125
- </style>
4126
- </head>
4127
- <body>
4128
- <div class="not-found">
4129
- <h1>Ticket Not Found</h1>
4130
- <p>Ticket ${escapeHtml(ticketKey)} does not exist or has been archived.</p>
4131
- <a href="/">← Back to Dashboard</a>
4132
- </div>
4133
- </body>
4134
- </html>`;
4135
- }
4136
- return `<!DOCTYPE html>
4137
- <html lang="${lang}">
4138
- <head>
4139
- <meta charset="UTF-8">
4140
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
4141
- <title>Terminal → ${escapeHtml(columnSlug)} | ${ticketKey} - AutoCode</title>
4142
- <style>
4143
- :root {
4144
- --bg: #0a0a0f;
4145
- --bg-secondary: #12121a;
4146
- --bg-tertiary: #1a1a24;
4147
- --text: #f1f5f9;
4148
- --muted: #94a3b8;
4149
- --border: #2a2a3a;
4150
- --accent: #6366f1;
4151
- --blue: #4dabf7;
4152
- --green: #4ade80;
4153
- --yellow: #facc15;
4154
- --orange: #fb923c;
4155
- --red: #f87171;
4156
- --purple: #a78bfa;
4157
- }
4158
- * { margin: 0; padding: 0; box-sizing: border-box; }
4159
- body {
4160
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4161
- background: var(--bg);
4162
- color: var(--text);
4163
- min-height: 100vh;
4164
- display: flex;
4165
- flex-direction: column;
4166
- }
4167
- .header {
4168
- display: flex;
4169
- align-items: center;
4170
- gap: 24px;
4171
- padding: 16px 24px;
4172
- background: var(--bg-secondary);
4173
- border-bottom: 1px solid var(--border);
4174
- position: sticky;
4175
- top: 0;
4176
- z-index: 100;
4177
- }
4178
- .back-btn {
4179
- color: var(--muted);
4180
- text-decoration: none;
4181
- font-size: 14px;
4182
- display: flex;
4183
- align-items: center;
4184
- gap: 8px;
4185
- padding: 8px 16px;
4186
- background: var(--bg-tertiary);
4187
- border-radius: 6px;
4188
- transition: all 0.2s;
4189
- }
4190
- .back-btn:hover { color: var(--text); background: var(--border); }
4191
- .ticket-key {
4192
- font-family: 'SF Mono', Monaco, monospace;
4193
- font-size: 12px;
4194
- color: var(--accent);
4195
- background: rgba(99, 102, 241, 0.15);
4196
- padding: 4px 10px;
4197
- border-radius: 4px;
4198
- font-weight: 600;
4199
- }
4200
- .column-slug {
4201
- font-family: 'SF Mono', Monaco, monospace;
4202
- font-size: 14px;
4203
- color: var(--green);
4204
- background: rgba(74, 222, 128, 0.15);
4205
- padding: 6px 12px;
4206
- border-radius: 4px;
4207
- font-weight: 500;
4208
- }
4209
- .page-title {
4210
- flex: 1;
4211
- font-size: 18px;
4212
- font-weight: 600;
4213
- display: flex;
4214
- align-items: center;
4215
- gap: 12px;
4216
- }
4217
- .main {
4218
- flex: 1;
4219
- padding: 24px;
4220
- display: flex;
4221
- flex-direction: column;
4222
- }
4223
- .terminal-container {
4224
- flex: 1;
4225
- background: var(--bg-secondary);
4226
- border: 1px solid var(--border);
4227
- border-radius: 12px;
4228
- display: flex;
4229
- flex-direction: column;
4230
- overflow: hidden;
4231
- }
4232
- .terminal-header {
4233
- display: flex;
4234
- align-items: center;
4235
- gap: 12px;
4236
- padding: 12px 16px;
4237
- background: var(--bg-tertiary);
4238
- border-bottom: 1px solid var(--border);
4239
- }
4240
- .terminal-status {
4241
- font-size: 12px;
4242
- padding: 4px 12px;
4243
- border-radius: 12px;
4244
- font-weight: 500;
4245
- }
4246
- .terminal-status.idle { background: var(--bg-secondary); color: var(--muted); }
4247
- .terminal-status.processing { background: rgba(251, 146, 60, 0.15); color: var(--orange); }
4248
- .terminal-status.success { background: rgba(74, 222, 128, 0.15); color: var(--green); }
4249
- .terminal-status.error { background: rgba(248, 113, 113, 0.15); color: var(--red); }
4250
- .terminal-log {
4251
- flex: 1;
4252
- padding: 16px;
4253
- overflow-y: auto;
4254
- font-family: 'SF Mono', Monaco, Consolas, monospace;
4255
- font-size: 13px;
4256
- line-height: 1.6;
4257
- }
4258
-
4259
- /* Log entry styles */
4260
- .log-entry {
4261
- padding: 4px 0;
4262
- flex-shrink: 0;
4263
- }
4264
- .log-entry.timestamp { color: var(--muted); font-size: 11px; }
4265
- .log-entry.system { color: var(--blue); }
4266
- .log-entry.error { color: var(--red); }
4267
- .log-entry.tool-call { margin: 8px 0; }
4268
- .log-label {
4269
- display: inline-block;
4270
- padding: 2px 8px;
4271
- border-radius: 4px;
4272
- font-size: 11px;
4273
- font-weight: 600;
4274
- margin-right: 8px;
4275
- text-transform: uppercase;
4276
- }
4277
- .log-entry.system .log-label { background: rgba(77, 171, 247, 0.15); color: var(--blue); }
4278
- .log-entry.error .log-label { background: rgba(248, 113, 113, 0.15); color: var(--red); }
4279
- .log-tool-badge {
4280
- display: inline-block;
4281
- background: rgba(163, 139, 250, 0.15);
4282
- color: var(--purple);
4283
- padding: 4px 10px;
4284
- border-radius: 4px;
4285
- font-size: 12px;
4286
- font-weight: 500;
4287
- }
4288
- .log-content { display: inline; }
4289
-
4290
- /* Message cards */
4291
- .log-message-card {
4292
- background: var(--bg-tertiary);
4293
- border: 1px solid var(--border);
4294
- border-radius: 8px;
4295
- margin: 12px 0;
4296
- overflow: hidden;
4297
- flex-shrink: 0;
4298
- }
4299
- .log-message-header {
4300
- padding: 8px 12px;
4301
- font-size: 11px;
4302
- font-weight: 600;
4303
- text-transform: uppercase;
4304
- letter-spacing: 0.5px;
4305
- }
4306
- .assistant-header { background: rgba(99, 102, 241, 0.15); color: var(--accent); }
4307
- .result-header { background: rgba(74, 222, 128, 0.15); color: var(--green); }
4308
- .log-message-body {
4309
- padding: 12px;
4310
- line-height: 1.6;
4311
- white-space: pre-wrap;
4312
- word-break: break-word;
4313
- }
4314
-
4315
- /* Code blocks - collapsible */
4316
- .log-code-block {
4317
- background: #0d1117;
4318
- border: 1px solid var(--border);
4319
- border-radius: 6px;
4320
- overflow: hidden;
4321
- margin: 8px 0;
4322
- }
4323
- .log-code-block summary.log-code-header {
4324
- background: #161b22;
4325
- padding: 10px 14px;
4326
- font-size: 12px;
4327
- color: var(--muted);
4328
- cursor: pointer;
4329
- display: flex;
4330
- align-items: center;
4331
- gap: 8px;
4332
- user-select: none;
4333
- list-style: none;
4334
- }
4335
- .log-code-block summary.log-code-header::-webkit-details-marker { display: none; }
4336
- .log-code-block summary.log-code-header::before {
4337
- content: '▶';
4338
- font-size: 10px;
4339
- transition: transform 0.2s;
4340
- }
4341
- .log-code-block[open] summary.log-code-header::before {
4342
- transform: rotate(90deg);
4343
- }
4344
- .log-code-block .code-lang {
4345
- background: var(--accent);
4346
- color: white;
4347
- padding: 2px 6px;
4348
- border-radius: 4px;
4349
- font-size: 10px;
4350
- font-weight: 600;
4351
- text-transform: uppercase;
4352
- }
4353
- .log-code-block pre {
4354
- margin: 0;
4355
- padding: 0;
4356
- background: transparent;
4357
- }
4358
- .log-code-block code {
4359
- display: block;
4360
- padding: 14px;
4361
- overflow-x: auto;
4362
- font-family: 'Fira Code', 'SF Mono', Monaco, monospace;
4363
- font-size: 13px;
4364
- line-height: 1.6;
4365
- max-height: 500px;
4366
- overflow-y: auto;
4367
- }
4368
-
4369
- .no-log {
4370
- color: var(--muted);
4371
- text-align: center;
4372
- padding: 48px;
4373
- font-size: 14px;
4374
- }
4375
- </style>
4376
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
4377
- </head>
4378
- <body>
4379
- <header class="header">
4380
- <a href="/ticket/${ticketKey}" class="back-btn">← Retour</a>
4381
- <span class="ticket-key">${ticketKey}</span>
4382
- <div class="page-title">
4383
- <span>Terminal →</span>
4384
- <span class="column-slug">${escapeHtml(columnSlug)}</span>
4385
- </div>
4386
- </header>
4387
-
4388
- <main class="main">
4389
- <div class="terminal-container">
4390
- <div class="terminal-header">
4391
- <span class="terminal-status idle" id="terminal-status">En attente</span>
4392
- </div>
4393
- <div class="terminal-log" id="terminal-log">
4394
- <div class="no-log">Chargement...</div>
4395
- </div>
4396
- </div>
4397
- </main>
4398
-
4399
- <script>
4400
- const TICKET_KEY = '${ticketKey}';
4401
- const COLUMN_SLUG = '${escapeHtml(columnSlug)}';
4402
-
4403
- function escapeHtml(str) {
4404
- if (typeof str !== 'string') return '';
4405
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
4406
- }
4407
-
4408
- function formatCodeBlock(content, filename) {
4409
- const lines = content.split(/\\\\n|\\n/);
4410
- // Détecter le langage depuis le nom de fichier
4411
- let lang = 'plaintext';
4412
- if (filename) {
4413
- const ext = filename.split('.').pop().toLowerCase();
4414
- const langMap = {
4415
- 'js': 'javascript', 'ts': 'typescript', 'vue': 'html', 'jsx': 'javascript',
4416
- 'tsx': 'typescript', 'py': 'python', 'rb': 'ruby', 'java': 'java',
4417
- 'go': 'go', 'rs': 'rust', 'cpp': 'cpp', 'c': 'c', 'h': 'c',
4418
- 'css': 'css', 'scss': 'scss', 'html': 'html', 'json': 'json',
4419
- 'md': 'markdown', 'sh': 'bash', 'yml': 'yaml', 'yaml': 'yaml'
4420
- };
4421
- lang = langMap[ext] || 'plaintext';
4422
- }
4423
- // Extraire le code sans les numéros de ligne pour highlight.js
4424
- let codeLines = [];
4425
- for (const line of lines) {
4426
- const match = line.match(/^\\s*\\d+[→|](.*)$/);
4427
- if (match) {
4428
- codeLines.push(match[1]);
4429
- } else if (line.trim()) {
4430
- codeLines.push(line);
4431
- }
4432
- }
4433
- const codeContent = codeLines.join('\\n');
4434
- const lineCount = codeLines.length;
4435
- const preview = filename || (lineCount + ' lignes');
4436
-
4437
- let html = '<details class="log-code-block">';
4438
- html += '<summary class="log-code-header"><span class="code-lang">' + lang + '</span> ' + escapeHtml(preview) + '</summary>';
4439
- html += '<pre><code class="language-' + lang + '">' + escapeHtml(codeContent) + '</code></pre>';
4440
- html += '</details>';
4441
- return html;
4442
- }
4443
-
4444
- function formatLogContent(rawContent) {
4445
- if (!rawContent) return '';
4446
- // Supprimer les balises <system-reminder> et leur contenu
4447
- let cleanedContent = rawContent.replace(/<system-reminder>[\\s\\S]*?<\\/system-reminder>/gi, '');
4448
- const lines = cleanedContent.split('\\n');
4449
- const entries = [];
4450
-
4451
- for (const line of lines) {
4452
- if (!line.trim()) continue;
4453
-
4454
- const timestampMatch = line.match(/^\\[(\\d{4}-\\d{2}-\\d{2}T[^\\]]+)\\]\\s*(.*)$/);
4455
- if (timestampMatch) {
4456
- const date = new Date(timestampMatch[1]);
4457
- const timeStr = date.toLocaleTimeString();
4458
- entries.push('<div class="log-entry timestamp">' + timeStr + ' - ' + escapeHtml(timestampMatch[2]) + '</div>');
4459
- continue;
4460
- }
4461
-
4462
- if (line.startsWith('[RAW] ')) {
4463
- const raw = line.slice(6);
4464
- if (!raw.startsWith('{')) continue;
4465
-
4466
- if (raw.includes('"type":"tool_result"')) {
4467
- const contentStart = raw.indexOf('"content":"');
4468
- if (contentStart !== -1) {
4469
- let content = raw.slice(contentStart + 11);
4470
- const endQuote = content.lastIndexOf('"');
4471
- if (endQuote > 0) content = content.slice(0, endQuote);
4472
- const decoded = content.replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"').replace(/\\\\t/g, '\\t');
4473
- if (/^\\s*\\d+[→|]/.test(decoded)) {
4474
- 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>');
4475
- } else {
4476
- const lines = decoded.split('\\n').slice(0, 20);
4477
- const truncated = decoded.split('\\n').length > 20 ? '<div style="color:#8b949e;font-style:italic">... (truncated)</div>' : '';
4478
- entries.push('<div class="log-message-card"><div class="log-message-header result-header">Tool Result</div><div class="log-message-body" style="font-family:monospace;font-size:12px;white-space:pre-wrap;max-height:200px;overflow-y:auto">' + escapeHtml(lines.join('\\n')) + truncated + '</div></div>');
4479
- }
4480
- continue;
4481
- }
4482
- }
4483
-
4484
- const textMatch = raw.match(/"type":"text","text":"([^"]+)"/);
4485
- if (textMatch) {
4486
- const decoded = textMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"');
4487
- entries.push('<div class="log-message-card"><div class="log-message-header assistant-header">Assistant</div><div class="log-message-body">' + escapeHtml(decoded) + '</div></div>');
4488
- continue;
4489
- }
4490
-
4491
- const toolMatch = raw.match(/"type":"tool_use"[^}]*"name":"([^"]+)"/);
4492
- if (toolMatch) {
4493
- entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(toolMatch[1]) + '</span></div>');
4494
- continue;
4495
- }
4496
- continue;
4497
- }
4498
-
4499
- if (line.startsWith('[SYSTEM]')) {
4500
- entries.push('<div class="log-entry system"><span class="log-label">System</span><div class="log-content">' + escapeHtml(line.slice(9)) + '</div></div>');
4501
- continue;
4502
- }
4503
- if (line.startsWith('[ASSISTANT]')) {
4504
- entries.push('<div class="log-message-card"><div class="log-message-header assistant-header">Assistant</div><div class="log-message-body">' + escapeHtml(line.slice(12)) + '</div></div>');
4505
- continue;
4506
- }
4507
- if (line.startsWith('[TOOL]')) {
4508
- entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(line.slice(7)) + '</span></div>');
4509
- continue;
4510
- }
4511
- if (line.startsWith('[RESULT]')) {
4512
- const content = line.slice(9);
4513
- if (/^\\s*\\d+[→|]/.test(content)) {
4514
- 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>');
4515
- } else {
4516
- 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>');
4517
- }
4518
- continue;
4519
- }
4520
- if (line.startsWith('[ERROR]')) {
4521
- entries.push('<div class="log-entry error"><span class="log-label">Error</span><div class="log-content">' + escapeHtml(line.slice(8)) + '</div></div>');
4522
- continue;
4523
- }
4524
-
4525
- entries.push('<div class="log-entry system">' + escapeHtml(line) + '</div>');
4526
- }
4527
-
4528
- return entries.join('');
4529
- }
4530
-
4531
- async function fetchLog() {
4532
- try {
4533
- const res = await fetch('/api/tickets/' + TICKET_KEY + '/log/' + COLUMN_SLUG);
4534
- const json = await res.json();
4535
- const log = document.getElementById('terminal-log');
4536
- const status = document.getElementById('terminal-status');
4537
-
4538
- if (json.success && json.data && json.data.content) {
4539
- log.innerHTML = formatLogContent(json.data.content);
4540
- // Appliquer le syntax highlighting
4541
- if (window.hljs) {
4542
- log.querySelectorAll('pre code').forEach((block) => {
4543
- hljs.highlightElement(block);
4544
- });
4545
- }
4546
- log.scrollTop = log.scrollHeight;
4547
- status.className = 'terminal-status success';
4548
- status.textContent = 'Terminé';
4549
- } else {
4550
- log.innerHTML = '<div class="no-log">Aucun log disponible pour cette colonne.</div>';
4551
- status.className = 'terminal-status idle';
4552
- status.textContent = 'Aucun log';
4553
- }
4554
- } catch (e) {
4555
- console.error('Log fetch error:', e);
4556
- document.getElementById('terminal-log').innerHTML = '<div class="no-log">Erreur de chargement.</div>';
4557
- }
4558
- }
4559
-
4560
- // WebSocket for live updates
4561
- let ws = null;
4562
- function connectWebSocket() {
4563
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
4564
- ws = new WebSocket(protocol + '//' + location.host + '/ws');
4565
-
4566
- ws.onmessage = (event) => {
4567
- try {
4568
- const msg = JSON.parse(event.data);
4569
- if (msg.type === 'claude:start' && msg.ticketKey === TICKET_KEY) {
4570
- document.getElementById('terminal-status').className = 'terminal-status processing';
4571
- document.getElementById('terminal-status').textContent = 'En cours...';
4572
- }
4573
- if (msg.type === 'claude:end' && msg.ticketKey === TICKET_KEY) {
4574
- fetchLog();
4575
- }
4576
- if (msg.type === 'ticket:updated' && msg.ticketKey === TICKET_KEY) {
4577
- fetchLog();
4578
- }
4579
- } catch (e) {
4580
- console.error('WebSocket error:', e);
4581
- }
4582
- };
4583
-
4584
- ws.onclose = () => {
4585
- setTimeout(connectWebSocket, 3000);
4586
- };
4587
- }
4588
-
4589
- // Init
4590
- fetchLog();
4591
- connectWebSocket();
4592
- </script>
4593
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
4594
- </body>
4595
- </html>`;
4596
- }
4597
- /**
4598
- * Generate the column prompt page HTML
4599
- */
4600
- export function generateColumnPromptPage(ticketKey, columnSlug, lang) {
4601
- const config = getConfig();
4602
- const ticket = getTicket(config.root, ticketKey);
4603
- // 404 page if ticket not found
4604
- if (!ticket) {
4605
- return `<!DOCTYPE html>
4606
- <html lang="${lang}">
4607
- <head>
4608
- <meta charset="UTF-8">
4609
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
4610
- <title>Ticket Not Found - AutoCode</title>
4611
- <style>
4612
- :root {
4613
- --bg: #0a0a0f;
4614
- --text: #f1f5f9;
4615
- --accent: #6366f1;
4616
- }
4617
- * { margin: 0; padding: 0; box-sizing: border-box; }
4618
- body {
4619
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4620
- background: var(--bg);
4621
- color: var(--text);
4622
- min-height: 100vh;
4623
- display: flex;
4624
- align-items: center;
4625
- justify-content: center;
4626
- }
4627
- .not-found {
4628
- text-align: center;
4629
- padding: 48px;
4630
- }
4631
- h1 { font-size: 2rem; margin-bottom: 16px; }
4632
- p { color: #94a3b8; margin-bottom: 24px; }
4633
- a {
4634
- display: inline-block;
4635
- background: var(--accent);
4636
- color: white;
4637
- padding: 12px 24px;
4638
- border-radius: 8px;
4639
- text-decoration: none;
4640
- font-weight: 500;
4641
- }
4642
- a:hover { opacity: 0.9; }
4643
- </style>
4644
- </head>
4645
- <body>
4646
- <div class="not-found">
4647
- <h1>Ticket Not Found</h1>
4648
- <p>Ticket ${escapeHtml(ticketKey)} does not exist or has been archived.</p>
4649
- <a href="/">← Back to Dashboard</a>
4650
- </div>
4651
- </body>
4652
- </html>`;
4653
- }
4654
- return `<!DOCTYPE html>
4655
- <html lang="${lang}">
4656
- <head>
4657
- <meta charset="UTF-8">
4658
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
4659
- <title>Prompt → ${escapeHtml(columnSlug)} | ${ticketKey} - AutoCode</title>
4660
- <style>
4661
- :root {
4662
- --bg: #0a0a0f;
4663
- --bg-secondary: #12121a;
4664
- --bg-tertiary: #1a1a24;
4665
- --text: #f1f5f9;
4666
- --muted: #94a3b8;
4667
- --border: #2a2a3a;
4668
- --accent: #6366f1;
4669
- --blue: #4dabf7;
4670
- --green: #4ade80;
4671
- --yellow: #facc15;
4672
- --orange: #fb923c;
4673
- --red: #f87171;
4674
- --purple: #a78bfa;
4675
- }
4676
- * { margin: 0; padding: 0; box-sizing: border-box; }
4677
- body {
4678
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4679
- background: var(--bg);
4680
- color: var(--text);
4681
- min-height: 100vh;
4682
- display: flex;
4683
- flex-direction: column;
4684
- }
4685
- .header {
4686
- display: flex;
4687
- align-items: center;
4688
- gap: 24px;
4689
- padding: 16px 24px;
4690
- background: var(--bg-secondary);
4691
- border-bottom: 1px solid var(--border);
4692
- position: sticky;
4693
- top: 0;
4694
- z-index: 100;
4695
- }
4696
- .back-btn {
4697
- color: var(--muted);
4698
- text-decoration: none;
4699
- font-size: 14px;
4700
- display: flex;
4701
- align-items: center;
4702
- gap: 8px;
4703
- padding: 8px 16px;
4704
- background: var(--bg-tertiary);
4705
- border-radius: 6px;
4706
- transition: all 0.2s;
4707
- }
4708
- .back-btn:hover { color: var(--text); background: var(--border); }
4709
- .ticket-key {
4710
- font-family: 'SF Mono', Monaco, monospace;
4711
- font-size: 12px;
4712
- color: var(--accent);
4713
- background: rgba(99, 102, 241, 0.15);
4714
- padding: 4px 10px;
4715
- border-radius: 4px;
4716
- font-weight: 600;
4717
- }
4718
- .column-slug {
4719
- font-family: 'SF Mono', Monaco, monospace;
4720
- font-size: 14px;
4721
- color: var(--yellow);
4722
- background: rgba(250, 204, 21, 0.15);
4723
- padding: 6px 12px;
4724
- border-radius: 4px;
4725
- font-weight: 500;
4726
- }
4727
- .page-title {
4728
- flex: 1;
4729
- font-size: 18px;
4730
- font-weight: 600;
4731
- display: flex;
4732
- align-items: center;
4733
- gap: 12px;
4734
- }
4735
- .main {
4736
- flex: 1;
4737
- padding: 24px;
4738
- display: flex;
4739
- flex-direction: column;
4740
- }
4741
- .prompt-container {
4742
- flex: 1;
4743
- background: var(--bg-secondary);
4744
- border: 1px solid var(--border);
4745
- border-radius: 12px;
4746
- display: flex;
4747
- flex-direction: column;
4748
- overflow: hidden;
4749
- }
4750
- .prompt-header {
4751
- display: flex;
4752
- align-items: center;
4753
- gap: 12px;
4754
- padding: 12px 16px;
4755
- background: var(--bg-tertiary);
4756
- border-bottom: 1px solid var(--border);
4757
- }
4758
- .prompt-status {
4759
- font-size: 12px;
4760
- padding: 4px 12px;
4761
- border-radius: 12px;
4762
- font-weight: 500;
4763
- }
4764
- .prompt-status.loaded { background: rgba(74, 222, 128, 0.15); color: var(--green); }
4765
- .prompt-status.loading { background: rgba(251, 146, 60, 0.15); color: var(--orange); }
4766
- .prompt-status.error { background: rgba(248, 113, 113, 0.15); color: var(--red); }
4767
- .prompt-content {
4768
- flex: 1;
4769
- padding: 24px;
4770
- overflow-y: auto;
4771
- font-family: 'SF Mono', Monaco, Consolas, monospace;
4772
- font-size: 14px;
4773
- line-height: 1.8;
4774
- white-space: pre-wrap;
4775
- word-break: break-word;
4776
- }
4777
- .no-prompt {
4778
- color: var(--muted);
4779
- text-align: center;
4780
- padding: 48px;
4781
- font-size: 14px;
4782
- }
4783
- </style>
4784
- </head>
4785
- <body>
4786
- <header class="header">
4787
- <a href="/ticket/${ticketKey}" class="back-btn">← Retour</a>
4788
- <span class="ticket-key">${ticketKey}</span>
4789
- <div class="page-title">
4790
- <span>Prompt →</span>
4791
- <span class="column-slug">${escapeHtml(columnSlug)}</span>
4792
- </div>
4793
- </header>
4794
-
4795
- <main class="main">
4796
- <div class="prompt-container">
4797
- <div class="prompt-header">
4798
- <span class="prompt-status loading" id="prompt-status">Chargement...</span>
4799
- </div>
4800
- <div class="prompt-content" id="prompt-content">
4801
- <div class="no-prompt">Chargement du prompt...</div>
4802
- </div>
4803
- </div>
4804
- </main>
4805
-
4806
- <script>
4807
- const TICKET_KEY = '${ticketKey}';
4808
- const COLUMN_SLUG = '${escapeHtml(columnSlug)}';
4809
-
4810
- function escapeHtml(str) {
4811
- if (typeof str !== 'string') return '';
4812
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
4813
- }
4814
-
4815
- async function fetchPrompt() {
4816
- const content = document.getElementById('prompt-content');
4817
- const status = document.getElementById('prompt-status');
4818
-
4819
- try {
4820
- const res = await fetch('/api/tickets/' + TICKET_KEY + '/prompt/' + COLUMN_SLUG);
4821
- const json = await res.json();
4822
-
4823
- if (json.success && json.data && json.data.prompt) {
4824
- content.textContent = json.data.prompt;
4825
- status.className = 'prompt-status loaded';
4826
- status.textContent = 'Chargé';
4827
- } else {
4828
- content.innerHTML = '<div class="no-prompt">Aucun prompt disponible pour cette colonne.</div>';
4829
- status.className = 'prompt-status error';
4830
- status.textContent = 'Non disponible';
4831
- }
4832
- } catch (e) {
4833
- console.error('Prompt fetch error:', e);
4834
- content.innerHTML = '<div class="no-prompt">Erreur de chargement.</div>';
4835
- status.className = 'prompt-status error';
4836
- status.textContent = 'Erreur';
4837
- }
4838
- }
4839
-
4840
- // Init
4841
- fetchPrompt();
4842
- </script>
4843
- </body>
4844
- </html>`;
4845
- }
7
+ export { generateDashboard, generateColumnEditPage, generateTicketViewPage, generateColumnTerminalPage, generateColumnPromptPage } from './dashboard/pages/index.js';
4846
8
  //# sourceMappingURL=dashboard.js.map