@autocode-cli/autocode 0.0.40 → 0.0.42

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.
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Dashboard HTML generation - Full feature parity with legacy autocode.sh
3
3
  */
4
+ import { createRequire } from 'module';
4
5
  import { getColumns } from '../core/column.js';
5
- import { listTickets } from '../core/ticket.js';
6
+ import { listTickets, getTicket } from '../core/ticket.js';
6
7
  import { getWorkflowSummary } from '../core/workflow.js';
7
8
  import { getConfig } from '../utils/config.js';
9
+ const require = createRequire(import.meta.url);
10
+ const pkg = require('../../package.json');
8
11
  /**
9
12
  * Generate the full dashboard HTML
10
13
  */
@@ -115,6 +118,19 @@ export function generateDashboard() {
115
118
  <span>+</span> <span data-i18n="modal.addCriteria">Add criteria</span>
116
119
  </button>
117
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>
118
134
  <div class="comments-section" id="comments-section" style="display:none">
119
135
  <div class="comments-header">
120
136
  <h3 data-i18n="modal.comments">Comments</h3>
@@ -175,7 +191,7 @@ export function generateDashboard() {
175
191
  </div>
176
192
 
177
193
  <footer>
178
- <span>AutoCode v2.0 | <span id="time">${timestamp}</span></span>
194
+ <span>AutoCode v${pkg.version} | <span id="time">${timestamp}</span></span>
179
195
  </footer>
180
196
 
181
197
  <script>
@@ -627,12 +643,7 @@ function getStyles() {
627
643
  display: flex;
628
644
  flex-direction: column;
629
645
  gap: 10px;
630
- max-height: 300px;
631
- overflow-y: auto;
632
- padding-right: 4px;
633
646
  }
634
- .comments-list::-webkit-scrollbar { width: 6px; }
635
- .comments-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
636
647
  .no-comments {
637
648
  color: var(--muted);
638
649
  font-size: 13px;
@@ -651,15 +662,37 @@ function getStyles() {
651
662
  }
652
663
  .comment:hover {
653
664
  border-left-color: var(--blue);
654
- transform: translateX(2px);
655
665
  }
656
666
  .comment-meta {
657
667
  display: flex;
658
668
  align-items: center;
659
- justify-content: space-between;
660
- margin-bottom: 8px;
661
- padding-bottom: 8px;
662
- border-bottom: 1px solid var(--border);
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);
663
696
  }
664
697
  .comment-column {
665
698
  font-size: 10px;
@@ -671,6 +704,22 @@ function getStyles() {
671
704
  background: rgba(77,171,247,0.15);
672
705
  border-radius: 4px;
673
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
+ }
674
723
  .comment-date {
675
724
  font-size: 11px;
676
725
  color: var(--muted);
@@ -746,6 +795,72 @@ function getStyles() {
746
795
  font-size: 13px;
747
796
  }
748
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
+
749
864
  .modal-actions {
750
865
  display: flex;
751
866
  gap: 12px;
@@ -894,6 +1009,46 @@ function getStyles() {
894
1009
  color: var(--text);
895
1010
  margin: 0;
896
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; }
897
1052
  @keyframes pulse {
898
1053
  0%, 100% { opacity: 1; }
899
1054
  50% { opacity: 0.5; }
@@ -960,6 +1115,9 @@ function getScript() {
960
1115
  'modal.selectLabel': 'Select a label...',
961
1116
  'modal.acceptanceCriteria': 'Acceptance criteria',
962
1117
  'modal.addCriteria': 'Add criteria',
1118
+ 'modal.attachments': 'Attachments',
1119
+ 'modal.addAttachment': 'Add file',
1120
+ 'modal.noAttachments': 'No attachments',
963
1121
  'modal.comments': 'Comments',
964
1122
  'modal.noComments': 'No comments',
965
1123
  'modal.addCommentPlaceholder': 'Add a comment...',
@@ -1046,6 +1204,9 @@ function getScript() {
1046
1204
  'modal.selectLabel': 'Sélectionner un label...',
1047
1205
  'modal.acceptanceCriteria': 'Critères d\\'acceptation',
1048
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',
1049
1210
  'modal.comments': 'Commentaires',
1050
1211
  'modal.noComments': 'Aucun commentaire',
1051
1212
  'modal.addCommentPlaceholder': 'Ajouter un commentaire...',
@@ -1106,7 +1267,7 @@ function getScript() {
1106
1267
  }
1107
1268
  };
1108
1269
 
1109
- let currentLang = localStorage.getItem('autocode-ui-lang') || 'en';
1270
+ let currentLang = localStorage.getItem('autocode-lang') || 'fr';
1110
1271
 
1111
1272
  function t(key) {
1112
1273
  return translations[currentLang][key] || translations['en'][key] || key;
@@ -1114,8 +1275,7 @@ function getScript() {
1114
1275
 
1115
1276
  function switchLanguage(lang) {
1116
1277
  currentLang = lang;
1117
- currentActionLang = lang; // Sync ACTION file language
1118
- localStorage.setItem('autocode-ui-lang', lang);
1278
+ currentActionLang = lang;
1119
1279
  localStorage.setItem('autocode-lang', lang);
1120
1280
  document.documentElement.lang = lang;
1121
1281
 
@@ -1163,7 +1323,7 @@ function getScript() {
1163
1323
  let draggedTicket = null;
1164
1324
  let draggedFromColumn = null;
1165
1325
  let currentActionSlug = null;
1166
- let currentActionLang = localStorage.getItem('autocode-lang') || 'en';
1326
+ let currentActionLang = localStorage.getItem('autocode-lang') || 'fr';
1167
1327
  let originalActionContent = '';
1168
1328
  let claudeProcessingTickets = new Set(); // Tickets currently being processed by Claude
1169
1329
 
@@ -1290,6 +1450,7 @@ function getScript() {
1290
1450
  const nextBtn = document.getElementById('btn-next');
1291
1451
  const archiveBtn = document.getElementById('btn-archive');
1292
1452
  const commentsSection = document.getElementById('comments-section');
1453
+ const attachmentsSection = document.getElementById('attachments-section');
1293
1454
 
1294
1455
  if (key) {
1295
1456
  modalTitle.textContent = t('modal.editTicket') + ' ' + key;
@@ -1297,6 +1458,7 @@ function getScript() {
1297
1458
  nextBtn.style.display = 'inline-block';
1298
1459
  archiveBtn.style.display = 'inline-block';
1299
1460
  commentsSection.style.display = 'block';
1461
+ attachmentsSection.style.display = 'block';
1300
1462
  loadTicketForEdit(key);
1301
1463
  } else {
1302
1464
  modalTitle.textContent = t('modal.newTicket');
@@ -1304,8 +1466,10 @@ function getScript() {
1304
1466
  nextBtn.style.display = 'none';
1305
1467
  archiveBtn.style.display = 'none';
1306
1468
  commentsSection.style.display = 'none';
1469
+ attachmentsSection.style.display = 'none';
1307
1470
  resetForm();
1308
1471
  resetComments();
1472
+ resetAttachments();
1309
1473
  }
1310
1474
  }
1311
1475
 
@@ -1353,6 +1517,9 @@ function getScript() {
1353
1517
 
1354
1518
  renderComments(ticket.comments || []);
1355
1519
 
1520
+ // Fetch attachments
1521
+ loadAttachments(key);
1522
+
1356
1523
  // Fetch Claude log if exists
1357
1524
  fetchLog(key);
1358
1525
  } catch (e) {
@@ -1381,7 +1548,7 @@ function getScript() {
1381
1548
  if (editingKey) {
1382
1549
  btn.textContent = t('btn.updating');
1383
1550
  await fetch('/api/tickets/' + editingKey, {
1384
- method: 'POST',
1551
+ method: 'PATCH',
1385
1552
  headers: { 'Content-Type': 'application/json' },
1386
1553
  body: JSON.stringify({ title, description, priority, semver, labels: selectedLabels, acceptance_criteria: criteria })
1387
1554
  });
@@ -1411,7 +1578,11 @@ function getScript() {
1411
1578
  btn.disabled = true;
1412
1579
  btn.textContent = t('btn.moving');
1413
1580
  try {
1414
- await fetch('/api/tickets/' + editingKey + '/next', { method: 'POST' });
1581
+ await fetch('/api/tickets/' + editingKey + '/next', {
1582
+ method: 'POST',
1583
+ headers: { 'Content-Type': 'application/json' },
1584
+ body: JSON.stringify({ lang: currentLang })
1585
+ });
1415
1586
  showNotification('info', t('notify.ticketAdvanced'), editingKey);
1416
1587
  closeModal();
1417
1588
  loadTicketsFromAPI();
@@ -1434,7 +1605,7 @@ function getScript() {
1434
1605
  await fetch('/api/tickets/' + editingKey + '/move', {
1435
1606
  method: 'POST',
1436
1607
  headers: { 'Content-Type': 'application/json' },
1437
- body: JSON.stringify({ column: lastColumn.name, force: true })
1608
+ body: JSON.stringify({ column: lastColumn.name, force: true, lang: currentLang })
1438
1609
  });
1439
1610
  showNotification('info', t('notify.ticketArchived'), editingKey);
1440
1611
  closeModal();
@@ -1513,16 +1684,31 @@ function getScript() {
1513
1684
  }
1514
1685
 
1515
1686
  const sorted = [...currentComments].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
1516
- list.innerHTML = sorted.map(comment => {
1687
+ list.innerHTML = sorted.map((comment, index) => {
1517
1688
  const date = new Date(comment.created_at);
1518
1689
  const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
1519
- return '<div class="comment">' +
1520
- '<div class="comment-meta"><span class="comment-column">' + (comment.column || 'N/A') + '</span>' +
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>' +
1521
1696
  '<span class="comment-date">' + dateStr + '</span></div>' +
1522
1697
  '<div class="comment-text">' + renderMarkdown(comment.text) + '</div></div>';
1523
1698
  }).join('');
1524
1699
  }
1525
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
+
1526
1712
  function renderMarkdown(text) {
1527
1713
  if (!text) return '';
1528
1714
  let html = escapeHtml(text);
@@ -1557,7 +1743,9 @@ function getScript() {
1557
1743
  });
1558
1744
  const result = await res.json();
1559
1745
  textarea.value = '';
1560
- if (result.comments) renderComments(result.comments);
1746
+ if (result.success && result.data && result.data.comments) {
1747
+ renderComments(result.data.comments);
1748
+ }
1561
1749
  showNotification('success', t('notify.commentAdded'), '');
1562
1750
  } catch (e) {
1563
1751
  showNotification('error', t('notify.error'), e.message);
@@ -1567,6 +1755,112 @@ function getScript() {
1567
1755
  }
1568
1756
  }
1569
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
+
1570
1864
  // ========================================
1571
1865
  // DRAG & DROP
1572
1866
  // ========================================
@@ -1620,7 +1914,7 @@ function getScript() {
1620
1914
  await fetch('/api/tickets/' + key + '/move', {
1621
1915
  method: 'POST',
1622
1916
  headers: { 'Content-Type': 'application/json' },
1623
- body: JSON.stringify({ column: targetColumnName, force: true })
1917
+ body: JSON.stringify({ column: targetColumnName, force: true, lang: currentLang })
1624
1918
  });
1625
1919
  showNotification('info', key + ' ' + t('notify.ticketMoved'), t('notify.moveTo') + ' "' + targetColumnName + '"');
1626
1920
  loadTicketsFromAPI();
@@ -1662,7 +1956,7 @@ function getScript() {
1662
1956
  await fetch('/api/tickets/' + key + '/move', {
1663
1957
  method: 'POST',
1664
1958
  headers: { 'Content-Type': 'application/json' },
1665
- body: JSON.stringify({ column: lastColumn.name, force: true })
1959
+ body: JSON.stringify({ column: lastColumn.name, force: true, lang: currentLang })
1666
1960
  });
1667
1961
  showNotification('info', t('notify.ticketArchived'), key);
1668
1962
  loadTicketsFromAPI();
@@ -1849,10 +2143,125 @@ function getScript() {
1849
2143
  }
1850
2144
  }
1851
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
+ let html = '<div class="log-code-block">';
2154
+ if (filename) {
2155
+ html += '<div class="log-code-header">' + escapeHtml(filename) + '</div>';
2156
+ }
2157
+ html += '<div class="log-code-content">';
2158
+ for (const line of lines) {
2159
+ const match = line.match(/^\\s*(\\d+)[→|](.*)$/);
2160
+ if (match) {
2161
+ html += '<div class="log-code-line"><span class="log-line-number">' + match[1] + '</span><span class="log-line-content">' + escapeHtml(match[2]) + '</span></div>';
2162
+ } else if (line.trim()) {
2163
+ html += '<div class="log-code-line"><span class="log-line-content">' + escapeHtml(line) + '</span></div>';
2164
+ }
2165
+ }
2166
+ html += '</div></div>';
2167
+ return html;
2168
+ }
2169
+
2170
+ function formatLogContent(rawContent) {
2171
+ if (!rawContent) return '';
2172
+ const lines = rawContent.split('\\n');
2173
+ const entries = [];
2174
+
2175
+ for (const line of lines) {
2176
+ if (!line.trim()) continue;
2177
+
2178
+ // Timestamp line
2179
+ const timestampMatch = line.match(/^\\[(\\d{4}-\\d{2}-\\d{2}T[^\\]]+)\\]\\s*(.*)$/);
2180
+ if (timestampMatch) {
2181
+ const date = new Date(timestampMatch[1]);
2182
+ const timeStr = date.toLocaleTimeString();
2183
+ entries.push('<div class="log-entry timestamp">' + timeStr + ' - ' + escapeHtml(timestampMatch[2]) + '</div>');
2184
+ continue;
2185
+ }
2186
+
2187
+ // [RAW] - Parse avec regex (pas JSON.parse car les lignes sont coupées)
2188
+ if (line.startsWith('[RAW] ')) {
2189
+ const raw = line.slice(6);
2190
+
2191
+ // Ignorer les lignes de continuation (ne commencent pas par {)
2192
+ if (!raw.startsWith('{')) {
2193
+ continue;
2194
+ }
2195
+
2196
+ // Extraire code source avec numéros de ligne (tool_result)
2197
+ const codeMatch = raw.match(/"content":"(\\s*\\d+[→|][^"]*)/);
2198
+ if (codeMatch) {
2199
+ // Extraire tout le contenu entre "content":" et la fin
2200
+ const contentMatch = raw.match(/"content":"([^"]+)/);
2201
+ if (contentMatch) {
2202
+ const decoded = contentMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"');
2203
+ 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>');
2204
+ continue;
2205
+ }
2206
+ }
2207
+
2208
+ // Extraire message assistant texte
2209
+ const textMatch = raw.match(/"type":"text","text":"([^"]+)"/);
2210
+ if (textMatch) {
2211
+ const decoded = textMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"');
2212
+ 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>');
2213
+ continue;
2214
+ }
2215
+
2216
+ // Extraire tool_use
2217
+ const toolMatch = raw.match(/"type":"tool_use"[^}]*"name":"([^"]+)"/);
2218
+ if (toolMatch) {
2219
+ entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(toolMatch[1]) + '</span></div>');
2220
+ continue;
2221
+ }
2222
+
2223
+ // Ignorer les autres RAW (métadonnées, etc.)
2224
+ continue;
2225
+ }
2226
+
2227
+ // Other prefixed messages
2228
+ if (line.startsWith('[SYSTEM]')) {
2229
+ entries.push('<div class="log-entry system"><span class="log-label">System</span><div class="log-content">' + escapeHtml(line.slice(9)) + '</div></div>');
2230
+ continue;
2231
+ }
2232
+ if (line.startsWith('[ASSISTANT]')) {
2233
+ 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>');
2234
+ continue;
2235
+ }
2236
+ if (line.startsWith('[TOOL]')) {
2237
+ entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(line.slice(7)) + '</span></div>');
2238
+ continue;
2239
+ }
2240
+ if (line.startsWith('[RESULT]')) {
2241
+ const content = line.slice(9);
2242
+ if (/^\\s*\\d+[→|]/.test(content)) {
2243
+ 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>');
2244
+ } else {
2245
+ 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>');
2246
+ }
2247
+ continue;
2248
+ }
2249
+ if (line.startsWith('[ERROR]')) {
2250
+ entries.push('<div class="log-entry error"><span class="log-label">Error</span><div class="log-content">' + escapeHtml(line.slice(8)) + '</div></div>');
2251
+ continue;
2252
+ }
2253
+
2254
+ // Default
2255
+ entries.push('<div class="log-entry system">' + escapeHtml(line) + '</div>');
2256
+ }
2257
+
2258
+ return entries.join('');
2259
+ }
2260
+
1852
2261
  function resetClaudeLog() {
1853
2262
  stopLogPolling();
1854
2263
  document.getElementById('claude-log-section').style.display = 'none';
1855
- document.getElementById('claude-log').textContent = '';
2264
+ document.getElementById('claude-log').innerHTML = '';
1856
2265
  document.getElementById('claude-log-status').className = 'claude-log-status';
1857
2266
  document.getElementById('claude-log-status').textContent = t('status.waiting');
1858
2267
  }
@@ -1864,11 +2273,10 @@ function getScript() {
1864
2273
  if (json.success && json.data) {
1865
2274
  const section = document.getElementById('claude-log-section');
1866
2275
  const log = document.getElementById('claude-log');
1867
- const status = document.getElementById('claude-log-status');
1868
2276
 
1869
2277
  if (json.data.exists || json.data.content) {
1870
2278
  section.style.display = 'block';
1871
- log.textContent = json.data.content || '';
2279
+ log.innerHTML = formatLogContent(json.data.content || '');
1872
2280
  // Auto-scroll
1873
2281
  log.scrollTop = log.scrollHeight;
1874
2282
  }
@@ -1947,7 +2355,7 @@ function getScript() {
1947
2355
  }
1948
2356
 
1949
2357
  function onTicketClick(key) {
1950
- openModal(key);
2358
+ window.location.href = '/ticket/' + key;
1951
2359
  }
1952
2360
 
1953
2361
  // ========================================
@@ -2137,9 +2545,9 @@ export function generateColumnEditPage(slug, lang) {
2137
2545
  <div class="notification" id="notification"></div>
2138
2546
 
2139
2547
  <script>
2140
- const STORAGE_KEY = 'autocode-ui-lang';
2548
+ const STORAGE_KEY = 'autocode-lang';
2141
2549
  const slug = '${slug}';
2142
- let currentLang = localStorage.getItem(STORAGE_KEY) || 'en';
2550
+ let currentLang = localStorage.getItem(STORAGE_KEY) || 'fr';
2143
2551
  let originalContent = '';
2144
2552
  let hasChanges = false;
2145
2553
 
@@ -2268,4 +2676,1366 @@ export function generateColumnEditPage(slug, lang) {
2268
2676
  </body>
2269
2677
  </html>`;
2270
2678
  }
2679
+ /**
2680
+ * Generate ticket view page
2681
+ */
2682
+ export function generateTicketViewPage(ticketKey, lang) {
2683
+ const config = getConfig();
2684
+ const ticket = getTicket(config.root, ticketKey);
2685
+ const columns = getColumns();
2686
+ // 404 page if ticket not found
2687
+ if (!ticket) {
2688
+ return `<!DOCTYPE html>
2689
+ <html lang="${lang}">
2690
+ <head>
2691
+ <meta charset="UTF-8">
2692
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2693
+ <title>Ticket Not Found - AutoCode</title>
2694
+ <style>
2695
+ :root {
2696
+ --bg: #0a0a0f;
2697
+ --text: #f1f5f9;
2698
+ --accent: #6366f1;
2699
+ }
2700
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2701
+ body {
2702
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2703
+ background: var(--bg);
2704
+ color: var(--text);
2705
+ min-height: 100vh;
2706
+ display: flex;
2707
+ align-items: center;
2708
+ justify-content: center;
2709
+ }
2710
+ .not-found {
2711
+ text-align: center;
2712
+ padding: 48px;
2713
+ }
2714
+ h1 { font-size: 2rem; margin-bottom: 16px; }
2715
+ p { color: #94a3b8; margin-bottom: 24px; }
2716
+ a {
2717
+ display: inline-block;
2718
+ background: var(--accent);
2719
+ color: white;
2720
+ padding: 12px 24px;
2721
+ border-radius: 8px;
2722
+ text-decoration: none;
2723
+ font-weight: 500;
2724
+ }
2725
+ a:hover { opacity: 0.9; }
2726
+ </style>
2727
+ </head>
2728
+ <body>
2729
+ <div class="not-found">
2730
+ <h1>Ticket Not Found</h1>
2731
+ <p>Ticket ${escapeHtml(ticketKey)} does not exist or has been archived.</p>
2732
+ <a href="/">← Back to Dashboard</a>
2733
+ </div>
2734
+ </body>
2735
+ </html>`;
2736
+ }
2737
+ const currentColumn = columns.find(c => c.slug === ticket.column_slug);
2738
+ const ticketData = JSON.stringify(ticket);
2739
+ const columnsData = JSON.stringify(columns);
2740
+ return `<!DOCTYPE html>
2741
+ <html lang="${lang}">
2742
+ <head>
2743
+ <meta charset="UTF-8">
2744
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2745
+ <title>${escapeHtml(ticket.title)} - ${ticketKey} - AutoCode</title>
2746
+ <style>
2747
+ :root {
2748
+ --bg: #0a0a0f;
2749
+ --bg-secondary: #12121a;
2750
+ --bg-tertiary: #1a1a24;
2751
+ --text: #f1f5f9;
2752
+ --muted: #94a3b8;
2753
+ --border: #2a2a3a;
2754
+ --accent: #6366f1;
2755
+ --blue: #4dabf7;
2756
+ --green: #4ade80;
2757
+ --yellow: #facc15;
2758
+ --orange: #fb923c;
2759
+ --red: #f87171;
2760
+ --purple: #a78bfa;
2761
+ }
2762
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2763
+ body {
2764
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2765
+ background: var(--bg);
2766
+ color: var(--text);
2767
+ min-height: 100vh;
2768
+ }
2769
+ .header {
2770
+ display: flex;
2771
+ align-items: center;
2772
+ gap: 24px;
2773
+ padding: 16px 24px;
2774
+ background: var(--bg-secondary);
2775
+ border-bottom: 1px solid var(--border);
2776
+ position: sticky;
2777
+ top: 0;
2778
+ z-index: 100;
2779
+ }
2780
+ .back-btn {
2781
+ color: var(--muted);
2782
+ text-decoration: none;
2783
+ font-size: 14px;
2784
+ display: flex;
2785
+ align-items: center;
2786
+ gap: 8px;
2787
+ }
2788
+ .back-btn:hover { color: var(--text); }
2789
+ .ticket-header-info {
2790
+ flex: 1;
2791
+ display: flex;
2792
+ align-items: center;
2793
+ gap: 16px;
2794
+ }
2795
+ .ticket-key {
2796
+ font-family: 'SF Mono', Monaco, monospace;
2797
+ font-size: 12px;
2798
+ color: var(--accent);
2799
+ background: rgba(99, 102, 241, 0.15);
2800
+ padding: 4px 10px;
2801
+ border-radius: 4px;
2802
+ font-weight: 600;
2803
+ }
2804
+ .ticket-title {
2805
+ font-size: 18px;
2806
+ font-weight: 600;
2807
+ }
2808
+ .lang-selector {
2809
+ display: flex;
2810
+ gap: 4px;
2811
+ }
2812
+ .lang-btn {
2813
+ background: transparent;
2814
+ border: 1px solid var(--border);
2815
+ color: var(--muted);
2816
+ padding: 6px 12px;
2817
+ border-radius: 4px;
2818
+ cursor: pointer;
2819
+ font-size: 12px;
2820
+ font-weight: 500;
2821
+ }
2822
+ .lang-btn:hover { border-color: var(--accent); color: var(--text); }
2823
+ .lang-btn.active { background: var(--accent); border-color: var(--accent); color: white; }
2824
+ .ticket-title-input {
2825
+ flex: 1;
2826
+ background: transparent;
2827
+ border: 1px solid transparent;
2828
+ color: var(--text);
2829
+ font-size: 18px;
2830
+ font-weight: 600;
2831
+ padding: 4px 8px;
2832
+ border-radius: 4px;
2833
+ font-family: inherit;
2834
+ }
2835
+ .ticket-title-input:hover { border-color: var(--border); }
2836
+ .ticket-title-input:focus {
2837
+ outline: none;
2838
+ border-color: var(--accent);
2839
+ background: var(--bg-tertiary);
2840
+ }
2841
+ .section-title {
2842
+ display: flex;
2843
+ align-items: center;
2844
+ justify-content: space-between;
2845
+ }
2846
+ .btn-edit-toggle {
2847
+ background: transparent;
2848
+ border: none;
2849
+ cursor: pointer;
2850
+ font-size: 14px;
2851
+ opacity: 0.5;
2852
+ transition: opacity 0.2s;
2853
+ }
2854
+ .btn-edit-toggle:hover { opacity: 1; }
2855
+ .btn-edit-toggle.active { opacity: 1; }
2856
+ .description-view {
2857
+ line-height: 1.7;
2858
+ color: var(--text);
2859
+ }
2860
+ .description-view p { margin-bottom: 12px; }
2861
+ .description-view h1, .description-view h2, .description-view h3, .description-view h4 {
2862
+ margin: 16px 0 8px;
2863
+ font-weight: 600;
2864
+ }
2865
+ .description-view code {
2866
+ background: var(--bg-tertiary);
2867
+ padding: 2px 6px;
2868
+ border-radius: 4px;
2869
+ font-family: 'SF Mono', Monaco, monospace;
2870
+ font-size: 13px;
2871
+ }
2872
+ .description-view pre {
2873
+ background: var(--bg-tertiary);
2874
+ padding: 12px;
2875
+ border-radius: 6px;
2876
+ overflow-x: auto;
2877
+ }
2878
+ .description-view ul, .description-view ol {
2879
+ margin: 8px 0;
2880
+ padding-left: 24px;
2881
+ }
2882
+ .description-view a { color: var(--accent); }
2883
+ .description-edit {
2884
+ width: 100%;
2885
+ min-height: 200px;
2886
+ padding: 12px;
2887
+ background: var(--bg-tertiary);
2888
+ border: 1px solid var(--border);
2889
+ border-radius: 8px;
2890
+ color: var(--text);
2891
+ font-family: 'SF Mono', Monaco, monospace;
2892
+ font-size: 13px;
2893
+ line-height: 1.6;
2894
+ resize: vertical;
2895
+ }
2896
+ .description-edit:focus {
2897
+ outline: none;
2898
+ border-color: var(--accent);
2899
+ }
2900
+ .main-content {
2901
+ display: flex;
2902
+ flex-direction: column;
2903
+ gap: 24px;
2904
+ padding: 24px 48px;
2905
+ }
2906
+ .ticket-details { display: flex; flex-direction: column; gap: 24px; }
2907
+ .ticket-bottom {
2908
+ display: flex;
2909
+ flex-direction: column;
2910
+ gap: 24px;
2911
+ }
2912
+ .section {
2913
+ background: var(--bg-secondary);
2914
+ border: 1px solid var(--border);
2915
+ border-radius: 12px;
2916
+ padding: 20px;
2917
+ }
2918
+ .section-title {
2919
+ font-size: 12px;
2920
+ font-weight: 600;
2921
+ text-transform: uppercase;
2922
+ letter-spacing: 0.5px;
2923
+ color: var(--muted);
2924
+ margin-bottom: 16px;
2925
+ }
2926
+ .ticket-meta {
2927
+ display: flex;
2928
+ flex-wrap: wrap;
2929
+ gap: 12px;
2930
+ }
2931
+ .meta-badge {
2932
+ font-size: 11px;
2933
+ font-weight: 600;
2934
+ text-transform: uppercase;
2935
+ letter-spacing: 0.5px;
2936
+ padding: 5px 12px;
2937
+ border-radius: 6px;
2938
+ }
2939
+ .priority-P0 { background: rgba(248, 113, 113, 0.2); color: var(--red); }
2940
+ .priority-P1 { background: rgba(251, 146, 60, 0.2); color: var(--orange); }
2941
+ .priority-P2 { background: rgba(250, 204, 21, 0.2); color: var(--yellow); }
2942
+ .priority-P3 { background: rgba(148, 163, 184, 0.2); color: var(--muted); }
2943
+ .column-badge { background: rgba(77, 171, 247, 0.15); color: var(--blue); }
2944
+ .semver-badge { background: rgba(167, 139, 250, 0.15); color: var(--purple); }
2945
+ .labels-list {
2946
+ display: flex;
2947
+ flex-wrap: wrap;
2948
+ gap: 8px;
2949
+ }
2950
+ .label-tag {
2951
+ font-size: 11px;
2952
+ padding: 4px 10px;
2953
+ border-radius: 12px;
2954
+ background: var(--bg-tertiary);
2955
+ color: var(--text);
2956
+ border: 1px solid var(--border);
2957
+ }
2958
+ .description-content {
2959
+ line-height: 1.7;
2960
+ color: var(--text);
2961
+ }
2962
+ .description-content p { margin-bottom: 12px; }
2963
+ .description-content code {
2964
+ background: var(--bg-tertiary);
2965
+ padding: 2px 6px;
2966
+ border-radius: 4px;
2967
+ font-family: 'SF Mono', Monaco, monospace;
2968
+ font-size: 13px;
2969
+ }
2970
+ .criteria-list { list-style: none; }
2971
+ .criteria-item {
2972
+ padding: 12px 16px;
2973
+ background: var(--bg-tertiary);
2974
+ border-radius: 8px;
2975
+ margin-bottom: 8px;
2976
+ display: flex;
2977
+ align-items: flex-start;
2978
+ gap: 12px;
2979
+ }
2980
+ .criteria-item::before {
2981
+ content: '☐';
2982
+ color: var(--muted);
2983
+ }
2984
+ .history-list { list-style: none; }
2985
+ .history-item {
2986
+ padding: 12px 0;
2987
+ border-bottom: 1px solid var(--border);
2988
+ display: flex;
2989
+ align-items: center;
2990
+ gap: 12px;
2991
+ font-size: 13px;
2992
+ }
2993
+ .history-item:last-child { border-bottom: none; }
2994
+ .history-action {
2995
+ font-weight: 600;
2996
+ text-transform: capitalize;
2997
+ }
2998
+ .history-from, .history-to {
2999
+ padding: 2px 8px;
3000
+ background: var(--bg-tertiary);
3001
+ border-radius: 4px;
3002
+ font-size: 11px;
3003
+ }
3004
+ .history-date { color: var(--muted); margin-left: auto; font-size: 12px; }
3005
+ .btn-prompt {
3006
+ background: none;
3007
+ border: none;
3008
+ cursor: pointer;
3009
+ font-size: 14px;
3010
+ padding: 2px 6px;
3011
+ opacity: 0.6;
3012
+ transition: opacity 0.2s;
3013
+ }
3014
+ .btn-prompt:hover { opacity: 1; }
3015
+ .prompt-modal {
3016
+ display: none;
3017
+ position: fixed;
3018
+ top: 0;
3019
+ left: 0;
3020
+ right: 0;
3021
+ bottom: 0;
3022
+ background: rgba(0, 0, 0, 0.7);
3023
+ z-index: 1000;
3024
+ align-items: center;
3025
+ justify-content: center;
3026
+ }
3027
+ .prompt-modal.visible { display: flex; }
3028
+ .prompt-modal-content {
3029
+ background: var(--bg-primary);
3030
+ border-radius: 12px;
3031
+ max-width: 900px;
3032
+ max-height: 80vh;
3033
+ width: 90%;
3034
+ display: flex;
3035
+ flex-direction: column;
3036
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
3037
+ }
3038
+ .prompt-modal-header {
3039
+ display: flex;
3040
+ justify-content: space-between;
3041
+ align-items: center;
3042
+ padding: 16px 20px;
3043
+ border-bottom: 1px solid var(--border);
3044
+ }
3045
+ .prompt-modal-header h3 { margin: 0; }
3046
+ .prompt-modal-close {
3047
+ background: none;
3048
+ border: none;
3049
+ font-size: 24px;
3050
+ cursor: pointer;
3051
+ color: var(--muted);
3052
+ }
3053
+ .prompt-modal-close:hover { color: var(--text); }
3054
+ .prompt-modal-body {
3055
+ padding: 20px;
3056
+ overflow-y: auto;
3057
+ flex: 1;
3058
+ }
3059
+ .prompt-modal-body pre {
3060
+ white-space: pre-wrap;
3061
+ word-wrap: break-word;
3062
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
3063
+ font-size: 13px;
3064
+ line-height: 1.5;
3065
+ margin: 0;
3066
+ background: var(--bg-secondary);
3067
+ padding: 16px;
3068
+ border-radius: 8px;
3069
+ }
3070
+ .log-container {
3071
+ max-height: 70vh;
3072
+ overflow-y: auto;
3073
+ display: flex;
3074
+ flex-direction: column;
3075
+ gap: 8px;
3076
+ }
3077
+ .actions-bar {
3078
+ display: flex;
3079
+ gap: 12px;
3080
+ padding: 16px 0;
3081
+ border-top: 1px solid var(--border);
3082
+ margin-top: 8px;
3083
+ }
3084
+ .btn {
3085
+ padding: 12px 20px;
3086
+ border-radius: 8px;
3087
+ font-weight: 500;
3088
+ font-size: 14px;
3089
+ cursor: pointer;
3090
+ border: none;
3091
+ text-align: center;
3092
+ text-decoration: none;
3093
+ }
3094
+ .btn-primary { background: var(--accent); color: white; }
3095
+ .btn-primary:hover { opacity: 0.9; }
3096
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
3097
+ .btn-secondary { background: var(--bg-tertiary); color: var(--text); border: 1px solid var(--border); }
3098
+ .btn-secondary:hover { border-color: var(--accent); }
3099
+ .btn-danger { background: rgba(248, 113, 113, 0.15); color: var(--red); border: 1px solid transparent; }
3100
+ .btn-danger:hover { border-color: var(--red); }
3101
+ .comments-list {
3102
+ display: flex;
3103
+ flex-direction: column;
3104
+ gap: 12px;
3105
+ margin-bottom: 16px;
3106
+ }
3107
+ .comment {
3108
+ padding: 16px;
3109
+ background: var(--bg-tertiary);
3110
+ border-radius: 8px;
3111
+ border-left: 3px solid var(--border);
3112
+ transition: all 0.2s ease;
3113
+ }
3114
+ .comment:hover {
3115
+ border-left-color: var(--blue);
3116
+ }
3117
+ .comment-meta {
3118
+ display: flex;
3119
+ align-items: center;
3120
+ flex-wrap: wrap;
3121
+ gap: 8px;
3122
+ cursor: pointer;
3123
+ user-select: none;
3124
+ }
3125
+ .comment-meta::before {
3126
+ content: '▶';
3127
+ font-size: 10px;
3128
+ color: var(--muted);
3129
+ transition: transform 0.2s ease;
3130
+ }
3131
+ .comment.expanded .comment-meta::before {
3132
+ transform: rotate(90deg);
3133
+ }
3134
+ .comment-source {
3135
+ font-size: 10px;
3136
+ padding: 3px 8px;
3137
+ border-radius: 4px;
3138
+ text-transform: uppercase;
3139
+ font-weight: 600;
3140
+ letter-spacing: 0.5px;
3141
+ }
3142
+ .comment-source.user { background: #3b82f6; color: white; }
3143
+ .comment-source.claude { background: #8b5cf6; color: white; }
3144
+ .comment-column {
3145
+ font-size: 10px;
3146
+ font-weight: 600;
3147
+ text-transform: uppercase;
3148
+ letter-spacing: 0.5px;
3149
+ color: var(--blue);
3150
+ padding: 3px 8px;
3151
+ background: rgba(77,171,247,0.15);
3152
+ border-radius: 4px;
3153
+ }
3154
+ .comment-date {
3155
+ font-size: 11px;
3156
+ color: var(--muted);
3157
+ }
3158
+ .comment-text {
3159
+ font-size: 14px;
3160
+ line-height: 1.6;
3161
+ color: var(--text);
3162
+ max-height: 0;
3163
+ overflow: hidden;
3164
+ transition: max-height 0.3s ease, margin-top 0.3s ease, padding-top 0.3s ease;
3165
+ margin-top: 0;
3166
+ padding-top: 0;
3167
+ }
3168
+ .comment.expanded .comment-text {
3169
+ max-height: 500px;
3170
+ margin-top: 12px;
3171
+ padding-top: 12px;
3172
+ border-top: 1px solid var(--border);
3173
+ }
3174
+ .comment-text code {
3175
+ background: var(--bg);
3176
+ padding: 2px 6px;
3177
+ border-radius: 4px;
3178
+ font-family: 'SF Mono', Monaco, monospace;
3179
+ font-size: 12px;
3180
+ }
3181
+ .add-comment {
3182
+ display: flex;
3183
+ flex-direction: column;
3184
+ gap: 8px;
3185
+ }
3186
+ .add-comment textarea {
3187
+ width: 100%;
3188
+ min-height: 80px;
3189
+ padding: 12px;
3190
+ background: var(--bg-tertiary);
3191
+ border: 1px solid var(--border);
3192
+ border-radius: 8px;
3193
+ color: var(--text);
3194
+ font-family: inherit;
3195
+ font-size: 14px;
3196
+ resize: vertical;
3197
+ }
3198
+ .add-comment textarea:focus {
3199
+ outline: none;
3200
+ border-color: var(--accent);
3201
+ }
3202
+ .btn-comment {
3203
+ align-self: flex-end;
3204
+ padding: 8px 16px;
3205
+ background: var(--accent);
3206
+ color: white;
3207
+ border: none;
3208
+ border-radius: 6px;
3209
+ cursor: pointer;
3210
+ font-size: 13px;
3211
+ font-weight: 500;
3212
+ }
3213
+ .btn-comment:hover { opacity: 0.9; }
3214
+ .btn-comment:disabled { opacity: 0.5; cursor: not-allowed; }
3215
+ .no-comments {
3216
+ text-align: center;
3217
+ color: var(--muted);
3218
+ padding: 24px;
3219
+ font-size: 14px;
3220
+ }
3221
+ .notification {
3222
+ position: fixed;
3223
+ bottom: 24px;
3224
+ right: 24px;
3225
+ padding: 12px 20px;
3226
+ background: var(--green);
3227
+ color: #000;
3228
+ border-radius: 8px;
3229
+ font-weight: 500;
3230
+ transform: translateY(100px);
3231
+ opacity: 0;
3232
+ transition: all 0.3s ease;
3233
+ z-index: 1000;
3234
+ }
3235
+ .notification.show { transform: translateY(0); opacity: 1; }
3236
+ .notification.error { background: var(--red); color: white; }
3237
+
3238
+ /* Claude Terminal */
3239
+ .claude-section .section-title {
3240
+ display: flex;
3241
+ align-items: center;
3242
+ justify-content: space-between;
3243
+ }
3244
+ .claude-status {
3245
+ font-size: 11px;
3246
+ padding: 3px 10px;
3247
+ border-radius: 12px;
3248
+ background: var(--bg-tertiary);
3249
+ color: var(--muted);
3250
+ }
3251
+ .claude-status.processing {
3252
+ color: var(--yellow);
3253
+ animation: pulse 1s infinite;
3254
+ }
3255
+ .claude-status.success { color: var(--green); }
3256
+ .claude-status.error { color: var(--red); }
3257
+ .claude-log {
3258
+ background: #0d1117;
3259
+ border: 1px solid var(--border);
3260
+ border-radius: 8px;
3261
+ padding: 16px;
3262
+ max-height: 400px;
3263
+ overflow-y: auto;
3264
+ font-family: 'SF Mono', Monaco, 'Consolas', monospace;
3265
+ font-size: 12px;
3266
+ line-height: 1.6;
3267
+ white-space: pre-wrap;
3268
+ word-break: break-word;
3269
+ color: var(--text);
3270
+ margin: 0;
3271
+ margin-top: 12px;
3272
+ }
3273
+ /* Formatted log entries */
3274
+ .log-entry { margin-bottom: 8px; padding: 8px 12px; border-radius: 4px; border-left: 3px solid transparent; flex-shrink: 0; }
3275
+ .log-entry.timestamp { color: #8b949e; font-size: 11px; border-left-color: #484f58; background: transparent; padding: 4px 12px; }
3276
+ .log-entry.system { color: #8b949e; border-left-color: #484f58; background: rgba(139,148,158,0.1); }
3277
+ .log-entry.user { color: #58a6ff; border-left-color: #58a6ff; background: rgba(88,166,255,0.1); }
3278
+ .log-entry.assistant { color: #7ee787; border-left-color: #7ee787; background: rgba(126,231,135,0.1); }
3279
+ .log-entry.tool-call { color: #d2a8ff; border-left-color: #d2a8ff; background: rgba(210,168,255,0.1); padding: 12px; }
3280
+ .log-entry.tool-result { color: #ffa657; border-left-color: #ffa657; background: rgba(255,166,87,0.1); }
3281
+ .log-entry.error { color: #f85149; border-left-color: #f85149; background: rgba(248,81,73,0.1); }
3282
+ .log-entry.success { color: #7ee787; border-left-color: #7ee787; background: rgba(126,231,135,0.1); }
3283
+ .log-label { font-weight: 600; font-size: 11px; text-transform: uppercase; margin-bottom: 4px; display: block; opacity: 0.8; }
3284
+ .log-content { white-space: pre-wrap; word-break: break-word; }
3285
+
3286
+ /* Code blocks with line numbers */
3287
+ .log-code-block { background: #161b22; border-radius: 6px; overflow: hidden; margin: 8px 0; border: 1px solid #30363d; }
3288
+ .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; }
3289
+ .log-code-header::before { content: ''; display: inline-block; width: 12px; height: 12px; background: #ffa657; border-radius: 50%; }
3290
+ .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; }
3291
+ .log-code-line { display: flex; min-height: 20px; }
3292
+ .log-line-number { color: #484f58; text-align: right; padding-right: 16px; user-select: none; min-width: 40px; flex-shrink: 0; }
3293
+ .log-line-content { color: #c9d1d9; white-space: pre; }
3294
+
3295
+ /* Message cards */
3296
+ .log-message-card { background: #161b22; border-radius: 8px; margin: 12px 0; overflow: hidden; border: 1px solid #30363d; flex-shrink: 0; }
3297
+ .log-message-header { padding: 10px 14px; display: flex; align-items: center; gap: 8px; font-weight: 500; font-size: 13px; }
3298
+ .log-message-header.assistant-header { background: linear-gradient(135deg, #238636 0%, #2ea043 100%); color: white; }
3299
+ .log-message-header.user-header { background: linear-gradient(135deg, #1f6feb 0%, #388bfd 100%); color: white; }
3300
+ .log-message-header.tool-header { background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%); color: white; }
3301
+ .log-message-header.result-header { background: linear-gradient(135deg, #f97316 0%, #fb923c 100%); color: white; }
3302
+ .log-message-body { padding: 14px; border-top: 1px solid #30363d; color: #c9d1d9; white-space: pre-wrap; word-break: break-word; }
3303
+
3304
+ /* Tool badges */
3305
+ .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; }
3306
+ .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; }
3307
+
3308
+ /* Collapsible raw JSON */
3309
+ .log-raw-details { margin: 8px 0; }
3310
+ .log-raw-summary { cursor: pointer; color: #8b949e; font-size: 12px; padding: 8px; background: rgba(139,148,158,0.1); border-radius: 4px; }
3311
+ .log-raw-summary:hover { background: rgba(139,148,158,0.2); }
3312
+ .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; }
3313
+ @keyframes pulse {
3314
+ 0%, 100% { opacity: 1; }
3315
+ 50% { opacity: 0.5; }
3316
+ }
3317
+ </style>
3318
+ </head>
3319
+ <body>
3320
+ <header class="header">
3321
+ <a href="/" class="back-btn">← Dashboard</a>
3322
+ <div class="ticket-header-info">
3323
+ <span class="ticket-key">${escapeHtml(ticketKey)}</span>
3324
+ <input type="text" class="ticket-title-input" id="ticket-title" value="${escapeHtml(ticket.title)}" />
3325
+ </div>
3326
+ <div class="lang-selector" id="lang-selector">
3327
+ <button class="lang-btn" data-lang="en">EN</button>
3328
+ <button class="lang-btn" data-lang="fr">FR</button>
3329
+ </div>
3330
+ </header>
3331
+
3332
+ <main class="main-content">
3333
+ <div class="ticket-details">
3334
+ <!-- Meta info -->
3335
+ <div class="section">
3336
+ <div class="section-title" data-i18n="ticketView.meta">Meta</div>
3337
+ <div class="ticket-meta">
3338
+ <span class="meta-badge priority-${ticket.priority}">${ticket.priority}</span>
3339
+ <span class="meta-badge column-badge">${escapeHtml(currentColumn?.name || ticket.column_slug)}</span>
3340
+ <span class="meta-badge semver-badge">${ticket.semver}</span>
3341
+ </div>
3342
+ </div>
3343
+
3344
+ <!-- Labels -->
3345
+ ${ticket.labels && ticket.labels.length > 0 ? `
3346
+ <div class="section">
3347
+ <div class="section-title" data-i18n="ticketView.labels">Labels</div>
3348
+ <div class="labels-list">
3349
+ ${ticket.labels.map(label => `<span class="label-tag">${escapeHtml(label)}</span>`).join('')}
3350
+ </div>
3351
+ </div>
3352
+ ` : ''}
3353
+
3354
+ <!-- Description -->
3355
+ <div class="section">
3356
+ <div class="section-title">
3357
+ <span data-i18n="ticketView.description">Description</span>
3358
+ <button class="btn-edit-toggle" id="btn-edit-description" onclick="toggleDescriptionEdit()">✏️</button>
3359
+ </div>
3360
+ <div class="description-view" id="description-view"></div>
3361
+ <textarea class="description-edit" id="description-edit" style="display:none" placeholder="Description (Markdown)">${escapeHtml(ticket.description || '')}</textarea>
3362
+ </div>
3363
+
3364
+ <!-- Acceptance Criteria -->
3365
+ ${ticket.acceptance_criteria && ticket.acceptance_criteria.length > 0 ? `
3366
+ <div class="section">
3367
+ <div class="section-title" data-i18n="ticketView.criteria">Acceptance Criteria</div>
3368
+ <ul class="criteria-list">
3369
+ ${ticket.acceptance_criteria.map(c => `<li class="criteria-item">${escapeHtml(c)}</li>`).join('')}
3370
+ </ul>
3371
+ </div>
3372
+ ` : ''}
3373
+
3374
+ <!-- History -->
3375
+ ${ticket.history && ticket.history.length > 0 ? `
3376
+ <div class="section">
3377
+ <div class="section-title" data-i18n="ticketView.history">History</div>
3378
+ <ul class="history-list">
3379
+ ${ticket.history.map(h => {
3380
+ const date = new Date(h.at);
3381
+ const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
3382
+ const showPromptBtn = h.to && h.action !== 'created' ? `<button class="btn-prompt" onclick="showPrompt('${escapeHtml(h.to)}')" title="View prompt">📜</button>` : '';
3383
+ const showLogBtn = h.to && h.action !== 'created' ? `<button class="btn-prompt" onclick="showLog('${escapeHtml(h.to)}')" title="View terminal log">🖥️</button>` : '';
3384
+ return `<li class="history-item">
3385
+ <span class="history-action">${h.action}</span>
3386
+ ${h.from ? `<span class="history-from">${escapeHtml(h.from)}</span> →` : ''}
3387
+ <span class="history-to">${escapeHtml(h.to)}</span>
3388
+ ${showPromptBtn}
3389
+ ${showLogBtn}
3390
+ <span class="history-date">${dateStr}</span>
3391
+ </li>`;
3392
+ }).join('')}
3393
+ </ul>
3394
+ </div>
3395
+ ` : ''}
3396
+
3397
+ <!-- Actions bar -->
3398
+ <div class="actions-bar">
3399
+ <button class="btn btn-primary" id="btn-save" onclick="saveTicket()" data-i18n="ticketView.save">Save</button>
3400
+ <button class="btn btn-secondary" onclick="advanceTicket()" data-i18n="ticketView.moveNext">Move to next column</button>
3401
+ <button class="btn btn-danger" onclick="archiveTicket()" data-i18n="ticketView.archive">Archive</button>
3402
+ </div>
3403
+ </div>
3404
+
3405
+ <!-- Bottom section: Comments & Claude Terminal -->
3406
+ <div class="ticket-bottom">
3407
+ <!-- Comments -->
3408
+ <div class="section comments-section">
3409
+ <div class="section-title"><span data-i18n="ticketView.comments">Comments</span> (<span id="comments-count">${ticket.comments?.length || 0}</span>)</div>
3410
+ <div class="comments-list" id="comments-list"></div>
3411
+ <div class="add-comment">
3412
+ <textarea id="new-comment" placeholder="Add a comment..." data-i18n-placeholder="ticketView.addComment"></textarea>
3413
+ <button class="btn-comment" onclick="addComment()" data-i18n="btn.add">Add</button>
3414
+ </div>
3415
+ </div>
3416
+
3417
+ <!-- Claude Terminal -->
3418
+ <div class="section claude-section" id="claude-section">
3419
+ <div class="section-title">
3420
+ <span data-i18n="ticketView.claudeTerminal">Claude Terminal</span>
3421
+ <span class="claude-status" id="claude-status" data-i18n="status.waiting">Waiting</span>
3422
+ </div>
3423
+ <pre class="claude-log" id="claude-log"></pre>
3424
+ </div>
3425
+ </div>
3426
+ </main>
3427
+
3428
+ <div class="notification" id="notification"></div>
3429
+
3430
+ <!-- Prompt Modal -->
3431
+ <div class="prompt-modal" id="prompt-modal" onclick="closePromptModal(event)">
3432
+ <div class="prompt-modal-content" onclick="event.stopPropagation()">
3433
+ <div class="prompt-modal-header">
3434
+ <h3 id="prompt-modal-title">Prompt</h3>
3435
+ <button class="prompt-modal-close" onclick="closePromptModal()">&times;</button>
3436
+ </div>
3437
+ <div class="prompt-modal-body">
3438
+ <div id="prompt-modal-content" class="log-container"></div>
3439
+ </div>
3440
+ </div>
3441
+ </div>
3442
+
3443
+ <script>
3444
+ const TICKET_KEY = '${ticketKey}';
3445
+ const TICKET = ${ticketData};
3446
+ const COLUMNS = ${columnsData};
3447
+ const STORAGE_KEY = 'autocode-lang';
3448
+
3449
+ let currentLang = localStorage.getItem(STORAGE_KEY) || 'fr';
3450
+ let currentComments = TICKET.comments || [];
3451
+
3452
+ const translations = {
3453
+ en: {
3454
+ 'ticketView.meta': 'Meta',
3455
+ 'ticketView.labels': 'Labels',
3456
+ 'ticketView.description': 'Description',
3457
+ 'ticketView.criteria': 'Acceptance Criteria',
3458
+ 'ticketView.history': 'History',
3459
+ 'ticketView.actions': 'Actions',
3460
+ 'ticketView.save': 'Save',
3461
+ 'ticketView.moveNext': 'Move to next column',
3462
+ 'ticketView.archive': 'Archive',
3463
+ 'ticketView.confirmMove': 'Move this ticket to the next column?',
3464
+ 'ticketView.confirmArchive': 'Archive this ticket?',
3465
+ 'ticketView.comments': 'Comments',
3466
+ 'ticketView.addComment': 'Add a comment...',
3467
+ 'ticketView.noComments': 'No comments yet',
3468
+ 'ticketView.claudeTerminal': 'Claude Terminal',
3469
+ 'ticketView.noDescription': 'No description',
3470
+ 'ticketView.noLog': 'No log yet. Waiting for Claude processing...',
3471
+ 'ticketView.loadingPrompt': 'Loading prompt...',
3472
+ 'ticketView.promptError': 'Error',
3473
+ 'btn.add': 'Add',
3474
+ 'btn.sending': 'Sending...',
3475
+ 'btn.saving': 'Saving...',
3476
+ 'status.waiting': 'Waiting',
3477
+ 'status.processing': 'Processing...',
3478
+ 'status.completed': 'Completed',
3479
+ 'status.failed': 'Failed',
3480
+ 'notify.commentAdded': 'Comment added',
3481
+ 'notify.ticketAdvanced': 'Ticket advanced',
3482
+ 'notify.ticketArchived': 'Ticket archived',
3483
+ 'notify.ticketSaved': 'Ticket saved',
3484
+ 'notify.error': 'Error'
3485
+ },
3486
+ fr: {
3487
+ 'ticketView.meta': 'Méta',
3488
+ 'ticketView.labels': 'Labels',
3489
+ 'ticketView.description': 'Description',
3490
+ 'ticketView.criteria': 'Critères d\\'acceptation',
3491
+ 'ticketView.history': 'Historique',
3492
+ 'ticketView.actions': 'Actions',
3493
+ 'ticketView.save': 'Sauvegarder',
3494
+ 'ticketView.moveNext': 'Déplacer vers la colonne suivante',
3495
+ 'ticketView.archive': 'Archiver',
3496
+ 'ticketView.confirmMove': 'Déplacer ce ticket vers la colonne suivante ?',
3497
+ 'ticketView.confirmArchive': 'Archiver ce ticket ?',
3498
+ 'ticketView.comments': 'Commentaires',
3499
+ 'ticketView.addComment': 'Ajouter un commentaire...',
3500
+ 'ticketView.noComments': 'Aucun commentaire',
3501
+ 'ticketView.claudeTerminal': 'Terminal Claude',
3502
+ 'ticketView.noDescription': 'Aucune description',
3503
+ 'ticketView.noLog': 'Aucun log. En attente du traitement Claude...',
3504
+ 'ticketView.loadingPrompt': 'Chargement du prompt...',
3505
+ 'ticketView.promptError': 'Erreur',
3506
+ 'btn.add': 'Ajouter',
3507
+ 'btn.sending': 'Envoi...',
3508
+ 'btn.saving': 'Sauvegarde...',
3509
+ 'status.waiting': 'En attente',
3510
+ 'status.processing': 'En cours...',
3511
+ 'status.completed': 'Terminé',
3512
+ 'status.failed': 'Échec',
3513
+ 'notify.commentAdded': 'Commentaire ajouté',
3514
+ 'notify.ticketAdvanced': 'Ticket avancé',
3515
+ 'notify.ticketArchived': 'Ticket archivé',
3516
+ 'notify.ticketSaved': 'Ticket sauvegardé',
3517
+ 'notify.error': 'Erreur'
3518
+ }
3519
+ };
3520
+
3521
+ function t(key) {
3522
+ return translations[currentLang]?.[key] || translations['en'][key] || key;
3523
+ }
3524
+
3525
+ function escapeHtml(text) {
3526
+ if (!text) return '';
3527
+ const div = document.createElement('div');
3528
+ div.textContent = text;
3529
+ return div.innerHTML;
3530
+ }
3531
+
3532
+ function renderMarkdown(text) {
3533
+ if (!text) return '';
3534
+ let html = escapeHtml(text);
3535
+ html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
3536
+ html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
3537
+ html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>');
3538
+ html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
3539
+ html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
3540
+ html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
3541
+ html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
3542
+ html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
3543
+ html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
3544
+ html = html.replace(/\\n/g, '<br>');
3545
+ return html;
3546
+ }
3547
+
3548
+ function updateLangUI() {
3549
+ document.querySelectorAll('.lang-btn').forEach(btn => {
3550
+ btn.classList.toggle('active', btn.dataset.lang === currentLang);
3551
+ });
3552
+ document.querySelectorAll('[data-i18n]').forEach(el => {
3553
+ el.textContent = t(el.dataset.i18n);
3554
+ });
3555
+ document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
3556
+ el.placeholder = t(el.dataset.i18nPlaceholder);
3557
+ });
3558
+ }
3559
+
3560
+ function renderComments() {
3561
+ const list = document.getElementById('comments-list');
3562
+ const count = document.getElementById('comments-count');
3563
+ count.textContent = currentComments.length;
3564
+
3565
+ if (currentComments.length === 0) {
3566
+ list.innerHTML = '<div class="no-comments">' + t('ticketView.noComments') + '</div>';
3567
+ return;
3568
+ }
3569
+
3570
+ const sorted = [...currentComments].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
3571
+ list.innerHTML = sorted.map((comment, index) => {
3572
+ const date = new Date(comment.created_at);
3573
+ const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
3574
+ const source = comment.source || 'user';
3575
+ const sourceBadge = source === 'claude'
3576
+ ? '<span class="comment-source claude">Claude</span>'
3577
+ : '<span class="comment-source user">User</span>';
3578
+ return '<div class="comment" id="comment-' + index + '">' +
3579
+ '<div class="comment-meta" onclick="toggleComment(' + index + ')">' + sourceBadge + '<span class="comment-column">' + (comment.column || 'N/A') + '</span>' +
3580
+ '<span class="comment-date">' + dateStr + '</span></div>' +
3581
+ '<div class="comment-text">' + renderMarkdown(comment.text) + '</div></div>';
3582
+ }).join('');
3583
+ }
3584
+
3585
+ function toggleComment(index) {
3586
+ const comments = document.querySelectorAll('.comment');
3587
+ comments.forEach((comment, i) => {
3588
+ if (i === index) {
3589
+ comment.classList.toggle('expanded');
3590
+ } else {
3591
+ comment.classList.remove('expanded');
3592
+ }
3593
+ });
3594
+ }
3595
+
3596
+ async function addComment() {
3597
+ const textarea = document.getElementById('new-comment');
3598
+ const text = textarea.value.trim();
3599
+ if (!text) return;
3600
+
3601
+ const btn = document.querySelector('.btn-comment');
3602
+ btn.disabled = true;
3603
+ btn.textContent = t('btn.sending');
3604
+
3605
+ try {
3606
+ const res = await fetch('/api/tickets/' + TICKET_KEY + '/comments', {
3607
+ method: 'POST',
3608
+ headers: { 'Content-Type': 'application/json' },
3609
+ body: JSON.stringify({ text, source: 'user' })
3610
+ });
3611
+ const result = await res.json();
3612
+ textarea.value = '';
3613
+ if (result.success && result.data && result.data.comments) {
3614
+ currentComments = result.data.comments;
3615
+ renderComments();
3616
+ }
3617
+ showNotification(t('notify.commentAdded'));
3618
+ } catch (e) {
3619
+ showNotification(t('notify.error') + ': ' + e.message, true);
3620
+ } finally {
3621
+ btn.disabled = false;
3622
+ btn.textContent = t('btn.add');
3623
+ }
3624
+ }
3625
+
3626
+ let isEditingDescription = false;
3627
+
3628
+ function renderDescription() {
3629
+ const view = document.getElementById('description-view');
3630
+ const edit = document.getElementById('description-edit');
3631
+ const description = edit.value || '';
3632
+ if (description) {
3633
+ view.innerHTML = renderMarkdown(description);
3634
+ } else {
3635
+ view.innerHTML = '<span style="color: var(--muted)">' + t('ticketView.noDescription') + '</span>';
3636
+ }
3637
+ }
3638
+
3639
+ function toggleDescriptionEdit() {
3640
+ isEditingDescription = !isEditingDescription;
3641
+ const view = document.getElementById('description-view');
3642
+ const edit = document.getElementById('description-edit');
3643
+ const btn = document.getElementById('btn-edit-description');
3644
+
3645
+ if (isEditingDescription) {
3646
+ view.style.display = 'none';
3647
+ edit.style.display = 'block';
3648
+ btn.classList.add('active');
3649
+ btn.textContent = '✓';
3650
+ edit.focus();
3651
+ } else {
3652
+ view.style.display = 'block';
3653
+ edit.style.display = 'none';
3654
+ btn.classList.remove('active');
3655
+ btn.textContent = '✏️';
3656
+ renderDescription();
3657
+ }
3658
+ }
3659
+
3660
+ async function saveTicket() {
3661
+ const btn = document.getElementById('btn-save');
3662
+ btn.disabled = true;
3663
+ btn.textContent = t('btn.saving');
3664
+
3665
+ try {
3666
+ const res = await fetch('/api/tickets/' + TICKET_KEY, {
3667
+ method: 'PATCH',
3668
+ headers: { 'Content-Type': 'application/json' },
3669
+ body: JSON.stringify({
3670
+ title: document.getElementById('ticket-title').value,
3671
+ description: document.getElementById('description-edit').value
3672
+ })
3673
+ });
3674
+ const result = await res.json();
3675
+ if (result.success) {
3676
+ showNotification(t('notify.ticketSaved'));
3677
+ if (isEditingDescription) {
3678
+ toggleDescriptionEdit();
3679
+ }
3680
+ } else {
3681
+ showNotification(t('notify.error') + ': ' + result.error, true);
3682
+ }
3683
+ } catch (e) {
3684
+ showNotification(t('notify.error') + ': ' + e.message, true);
3685
+ } finally {
3686
+ btn.disabled = false;
3687
+ btn.textContent = t('ticketView.save');
3688
+ }
3689
+ }
3690
+
3691
+ async function advanceTicket() {
3692
+ if (!confirm(t('ticketView.confirmMove'))) return;
3693
+ try {
3694
+ const res = await fetch('/api/tickets/' + TICKET_KEY + '/next', {
3695
+ method: 'POST',
3696
+ headers: { 'Content-Type': 'application/json' },
3697
+ body: JSON.stringify({ lang: currentLang })
3698
+ });
3699
+ const result = await res.json();
3700
+ if (result.success) {
3701
+ showNotification(t('notify.ticketAdvanced'));
3702
+ setTimeout(() => location.reload(), 1000);
3703
+ } else {
3704
+ showNotification(t('notify.error') + ': ' + result.error, true);
3705
+ }
3706
+ } catch (e) {
3707
+ showNotification(t('notify.error') + ': ' + e.message, true);
3708
+ }
3709
+ }
3710
+
3711
+ async function archiveTicket() {
3712
+ if (!confirm(t('ticketView.confirmArchive'))) return;
3713
+ try {
3714
+ const res = await fetch('/api/tickets/' + TICKET_KEY + '/archive', {
3715
+ method: 'POST',
3716
+ headers: { 'Content-Type': 'application/json' },
3717
+ body: JSON.stringify({ lang: currentLang })
3718
+ });
3719
+ const result = await res.json();
3720
+ if (result.success) {
3721
+ showNotification(t('notify.ticketArchived'));
3722
+ setTimeout(() => location.href = '/', 1000);
3723
+ } else {
3724
+ showNotification(t('notify.error') + ': ' + result.error, true);
3725
+ }
3726
+ } catch (e) {
3727
+ showNotification(t('notify.error') + ': ' + e.message, true);
3728
+ }
3729
+ }
3730
+
3731
+ function showNotification(msg, isError) {
3732
+ const notification = document.getElementById('notification');
3733
+ notification.textContent = msg;
3734
+ notification.className = 'notification show' + (isError ? ' error' : '');
3735
+ setTimeout(() => notification.className = 'notification', 3000);
3736
+ }
3737
+
3738
+ // Language switcher
3739
+ document.querySelectorAll('.lang-btn').forEach(btn => {
3740
+ btn.addEventListener('click', () => {
3741
+ const newLang = btn.dataset.lang;
3742
+ if (newLang !== currentLang) {
3743
+ currentLang = newLang;
3744
+ localStorage.setItem(STORAGE_KEY, newLang);
3745
+ updateLangUI();
3746
+ renderComments();
3747
+ }
3748
+ });
3749
+ });
3750
+
3751
+ // ========================================
3752
+ // WEBSOCKET & CLAUDE LOG
3753
+ // ========================================
3754
+ let ws;
3755
+ let logPollingInterval = null;
3756
+
3757
+ function connectWebSocket() {
3758
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
3759
+ ws = new WebSocket(protocol + '//' + location.host + '/ws');
3760
+
3761
+ ws.onmessage = (event) => {
3762
+ try {
3763
+ const data = JSON.parse(event.data);
3764
+ switch (data.type) {
3765
+ case 'ticket_updated':
3766
+ if (data.key === TICKET_KEY) {
3767
+ location.reload();
3768
+ }
3769
+ break;
3770
+ case 'claude_start':
3771
+ if (data.ticket === TICKET_KEY) {
3772
+ onClaudeStart();
3773
+ }
3774
+ break;
3775
+ case 'claude_stream':
3776
+ if (data.ticket === TICKET_KEY) {
3777
+ fetchLog();
3778
+ }
3779
+ break;
3780
+ case 'claude_end':
3781
+ if (data.ticket === TICKET_KEY) {
3782
+ onClaudeEnd(data.success, data.duration);
3783
+ }
3784
+ break;
3785
+ }
3786
+ } catch (e) {
3787
+ console.error('WebSocket message error:', e);
3788
+ }
3789
+ };
3790
+
3791
+ ws.onclose = () => {
3792
+ setTimeout(connectWebSocket, 3000);
3793
+ };
3794
+ }
3795
+
3796
+ function escapeHtml(str) {
3797
+ if (typeof str !== 'string') return '';
3798
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3799
+ }
3800
+
3801
+ function formatCodeBlock(content, filename) {
3802
+ const lines = content.split(/\\\\n|\\n/);
3803
+ let html = '<div class="log-code-block">';
3804
+ if (filename) {
3805
+ html += '<div class="log-code-header">' + escapeHtml(filename) + '</div>';
3806
+ }
3807
+ html += '<div class="log-code-content">';
3808
+ for (const line of lines) {
3809
+ const match = line.match(/^\\s*(\\d+)[→|](.*)$/);
3810
+ if (match) {
3811
+ html += '<div class="log-code-line"><span class="log-line-number">' + match[1] + '</span><span class="log-line-content">' + escapeHtml(match[2]) + '</span></div>';
3812
+ } else if (line.trim()) {
3813
+ html += '<div class="log-code-line"><span class="log-line-content">' + escapeHtml(line) + '</span></div>';
3814
+ }
3815
+ }
3816
+ html += '</div></div>';
3817
+ return html;
3818
+ }
3819
+
3820
+ function formatLogContent(rawContent) {
3821
+ if (!rawContent) return '';
3822
+ const lines = rawContent.split('\\n');
3823
+ const entries = [];
3824
+
3825
+ for (const line of lines) {
3826
+ if (!line.trim()) continue;
3827
+
3828
+ const timestampMatch = line.match(/^\\[(\\d{4}-\\d{2}-\\d{2}T[^\\]]+)\\]\\s*(.*)$/);
3829
+ if (timestampMatch) {
3830
+ const date = new Date(timestampMatch[1]);
3831
+ const timeStr = date.toLocaleTimeString();
3832
+ entries.push('<div class="log-entry timestamp">' + timeStr + ' - ' + escapeHtml(timestampMatch[2]) + '</div>');
3833
+ continue;
3834
+ }
3835
+
3836
+ // [RAW] - Parse avec regex (pas JSON.parse car les lignes sont coupées)
3837
+ if (line.startsWith('[RAW] ')) {
3838
+ const raw = line.slice(6);
3839
+
3840
+ // Ignorer les lignes de continuation (ne commencent pas par {)
3841
+ if (!raw.startsWith('{')) {
3842
+ continue;
3843
+ }
3844
+
3845
+ // Extraire tool_result content
3846
+ if (raw.includes('"type":"tool_result"')) {
3847
+ const contentStart = raw.indexOf('"content":"');
3848
+ if (contentStart !== -1) {
3849
+ // Extraire le contenu après "content":"
3850
+ let content = raw.slice(contentStart + 11);
3851
+ // Enlever le reste du JSON (approximatif car tronqué)
3852
+ const endQuote = content.lastIndexOf('"');
3853
+ if (endQuote > 0) content = content.slice(0, endQuote);
3854
+ // Décoder les échappements
3855
+ const decoded = content.replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"').replace(/\\\\t/g, '\\t');
3856
+ // Vérifier si c'est du code avec numéros de ligne
3857
+ if (/^\\s*\\d+[→|]/.test(decoded)) {
3858
+ 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>');
3859
+ } else {
3860
+ // Résultat texte simple (liste de fichiers, etc.)
3861
+ const lines = decoded.split('\\n').slice(0, 20); // Limiter à 20 lignes
3862
+ const truncated = decoded.split('\\n').length > 20 ? '<div style="color:#8b949e;font-style:italic">... (truncated)</div>' : '';
3863
+ 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>');
3864
+ }
3865
+ continue;
3866
+ }
3867
+ }
3868
+
3869
+ // Extraire message assistant texte
3870
+ const textMatch = raw.match(/"type":"text","text":"([^"]+)"/);
3871
+ if (textMatch) {
3872
+ const decoded = textMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\"/g, '"');
3873
+ 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>');
3874
+ continue;
3875
+ }
3876
+
3877
+ // Extraire tool_use
3878
+ const toolMatch = raw.match(/"type":"tool_use"[^}]*"name":"([^"]+)"/);
3879
+ if (toolMatch) {
3880
+ entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(toolMatch[1]) + '</span></div>');
3881
+ continue;
3882
+ }
3883
+
3884
+ // Ignorer les autres RAW
3885
+ continue;
3886
+ }
3887
+
3888
+ if (line.startsWith('[SYSTEM]')) {
3889
+ entries.push('<div class="log-entry system"><span class="log-label">System</span><div class="log-content">' + escapeHtml(line.slice(9)) + '</div></div>');
3890
+ continue;
3891
+ }
3892
+ if (line.startsWith('[ASSISTANT]')) {
3893
+ 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>');
3894
+ continue;
3895
+ }
3896
+ if (line.startsWith('[TOOL]')) {
3897
+ entries.push('<div class="log-entry tool-call"><span class="log-tool-badge">' + escapeHtml(line.slice(7)) + '</span></div>');
3898
+ continue;
3899
+ }
3900
+ if (line.startsWith('[RESULT]')) {
3901
+ const content = line.slice(9);
3902
+ if (/^\\s*\\d+[→|]/.test(content)) {
3903
+ 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>');
3904
+ } else {
3905
+ 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>');
3906
+ }
3907
+ continue;
3908
+ }
3909
+ if (line.startsWith('[ERROR]')) {
3910
+ entries.push('<div class="log-entry error"><span class="log-label">Error</span><div class="log-content">' + escapeHtml(line.slice(8)) + '</div></div>');
3911
+ continue;
3912
+ }
3913
+
3914
+ entries.push('<div class="log-entry system">' + escapeHtml(line) + '</div>');
3915
+ }
3916
+
3917
+ return entries.join('');
3918
+ }
3919
+
3920
+ function startLogPolling() {
3921
+ stopLogPolling();
3922
+ logPollingInterval = setInterval(fetchLog, 1000);
3923
+ fetchLog();
3924
+ }
3925
+
3926
+ function stopLogPolling() {
3927
+ if (logPollingInterval) {
3928
+ clearInterval(logPollingInterval);
3929
+ logPollingInterval = null;
3930
+ }
3931
+ }
3932
+
3933
+ async function fetchLog() {
3934
+ try {
3935
+ const res = await fetch('/api/tickets/' + TICKET_KEY + '/log');
3936
+ const json = await res.json();
3937
+ const log = document.getElementById('claude-log');
3938
+
3939
+ if (json.success && json.data && (json.data.exists || json.data.content)) {
3940
+ log.innerHTML = formatLogContent(json.data.content || '');
3941
+ log.scrollTop = log.scrollHeight;
3942
+ } else {
3943
+ log.innerHTML = '<div class="log-entry system">' + t('ticketView.noLog') + '</div>';
3944
+ }
3945
+ } catch (e) {
3946
+ console.error('Log fetch error:', e);
3947
+ }
3948
+ }
3949
+
3950
+ function onClaudeStart() {
3951
+ const status = document.getElementById('claude-status');
3952
+ status.className = 'claude-status processing';
3953
+ status.textContent = t('status.processing');
3954
+ startLogPolling();
3955
+ }
3956
+
3957
+ function onClaudeEnd(success, duration) {
3958
+ stopLogPolling();
3959
+ fetchLog();
3960
+ const status = document.getElementById('claude-status');
3961
+ if (success) {
3962
+ status.className = 'claude-status success';
3963
+ status.textContent = t('status.completed') + ' (' + (duration / 1000).toFixed(1) + 's)';
3964
+ } else {
3965
+ status.className = 'claude-status error';
3966
+ status.textContent = t('status.failed');
3967
+ }
3968
+ }
3969
+
3970
+ // Prompt Modal
3971
+ async function showPrompt(columnSlug) {
3972
+ const modal = document.getElementById('prompt-modal');
3973
+ const title = document.getElementById('prompt-modal-title');
3974
+ const content = document.getElementById('prompt-modal-content');
3975
+
3976
+ title.textContent = t('ticketView.loadingPrompt');
3977
+ content.textContent = '';
3978
+ modal.classList.add('visible');
3979
+
3980
+ try {
3981
+ const res = await fetch('/api/tickets/' + TICKET_KEY + '/prompt/' + columnSlug);
3982
+ const json = await res.json();
3983
+ if (json.success) {
3984
+ title.textContent = 'Prompt → ' + json.data.column;
3985
+ content.textContent = json.data.prompt;
3986
+ } else {
3987
+ title.textContent = t('ticketView.promptError');
3988
+ content.textContent = json.error || 'Error loading prompt';
3989
+ }
3990
+ } catch (e) {
3991
+ title.textContent = t('ticketView.promptError');
3992
+ content.textContent = e.message;
3993
+ }
3994
+ }
3995
+
3996
+ function closePromptModal(event) {
3997
+ if (event && event.target !== event.currentTarget) return;
3998
+ document.getElementById('prompt-modal').classList.remove('visible');
3999
+ }
4000
+
4001
+ // Log Modal (reuses prompt modal)
4002
+ async function showLog(columnSlug) {
4003
+ const modal = document.getElementById('prompt-modal');
4004
+ const title = document.getElementById('prompt-modal-title');
4005
+ const content = document.getElementById('prompt-modal-content');
4006
+
4007
+ title.textContent = t('ticketView.loadingLog') || 'Loading log...';
4008
+ content.textContent = '';
4009
+ modal.classList.add('visible');
4010
+
4011
+ try {
4012
+ const res = await fetch('/api/tickets/' + TICKET_KEY + '/log/' + columnSlug);
4013
+ const json = await res.json();
4014
+ if (json.success) {
4015
+ title.textContent = 'Terminal → ' + columnSlug;
4016
+ if (json.data.content) {
4017
+ content.innerHTML = formatLogContent(json.data.content);
4018
+ } else {
4019
+ content.textContent = t('ticketView.noLog') || 'No log available';
4020
+ }
4021
+ } else {
4022
+ title.textContent = t('ticketView.logError') || 'Log Error';
4023
+ content.textContent = json.error || 'Error loading log';
4024
+ }
4025
+ } catch (e) {
4026
+ title.textContent = t('ticketView.logError') || 'Log Error';
4027
+ content.textContent = e.message;
4028
+ }
4029
+ }
4030
+
4031
+ // Init
4032
+ updateLangUI();
4033
+ renderDescription();
4034
+ renderComments();
4035
+ connectWebSocket();
4036
+ fetchLog();
4037
+ </script>
4038
+ </body>
4039
+ </html>`;
4040
+ }
2271
4041
  //# sourceMappingURL=dashboard.js.map