@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.
- package/dist/cli/commands/comment.d.ts.map +1 -1
- package/dist/cli/commands/comment.js +6 -3
- package/dist/cli/commands/comment.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +23 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/core/ticket.d.ts +1 -1
- package/dist/core/ticket.d.ts.map +1 -1
- package/dist/core/ticket.js +3 -7
- package/dist/core/ticket.js.map +1 -1
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +271 -13
- package/dist/server/api.js.map +1 -1
- package/dist/server/dashboard.d.ts +4 -0
- package/dist/server/dashboard.d.ts.map +1 -1
- package/dist/server/dashboard.js +1801 -31
- package/dist/server/dashboard.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +11 -1
- package/dist/server/index.js.map +1 -1
- package/dist/services/claude.d.ts +18 -4
- package/dist/services/claude.d.ts.map +1 -1
- package/dist/services/claude.js +115 -41
- package/dist/services/claude.js.map +1 -1
- package/dist/services/ticket-io.d.ts +7 -3
- package/dist/services/ticket-io.d.ts.map +1 -1
- package/dist/services/ticket-io.js +20 -20
- package/dist/services/ticket-io.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/config.js +1 -1
- package/package.json +1 -1
package/dist/server/dashboard.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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-
|
|
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;
|
|
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') || '
|
|
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: '
|
|
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', {
|
|
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
|
-
|
|
1520
|
-
|
|
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.
|
|
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">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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').
|
|
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.
|
|
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
|
-
|
|
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-
|
|
2548
|
+
const STORAGE_KEY = 'autocode-lang';
|
|
2141
2549
|
const slug = '${slug}';
|
|
2142
|
-
let currentLang = localStorage.getItem(STORAGE_KEY) || '
|
|
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()">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|