@autocode-cli/autocode 0.0.39 → 0.0.41
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 +1483 -32
- 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;
|
|
@@ -960,6 +1075,9 @@ function getScript() {
|
|
|
960
1075
|
'modal.selectLabel': 'Select a label...',
|
|
961
1076
|
'modal.acceptanceCriteria': 'Acceptance criteria',
|
|
962
1077
|
'modal.addCriteria': 'Add criteria',
|
|
1078
|
+
'modal.attachments': 'Attachments',
|
|
1079
|
+
'modal.addAttachment': 'Add file',
|
|
1080
|
+
'modal.noAttachments': 'No attachments',
|
|
963
1081
|
'modal.comments': 'Comments',
|
|
964
1082
|
'modal.noComments': 'No comments',
|
|
965
1083
|
'modal.addCommentPlaceholder': 'Add a comment...',
|
|
@@ -1046,6 +1164,9 @@ function getScript() {
|
|
|
1046
1164
|
'modal.selectLabel': 'Sélectionner un label...',
|
|
1047
1165
|
'modal.acceptanceCriteria': 'Critères d\\'acceptation',
|
|
1048
1166
|
'modal.addCriteria': 'Ajouter un critère',
|
|
1167
|
+
'modal.attachments': 'Pièces jointes',
|
|
1168
|
+
'modal.addAttachment': 'Ajouter un fichier',
|
|
1169
|
+
'modal.noAttachments': 'Aucune pièce jointe',
|
|
1049
1170
|
'modal.comments': 'Commentaires',
|
|
1050
1171
|
'modal.noComments': 'Aucun commentaire',
|
|
1051
1172
|
'modal.addCommentPlaceholder': 'Ajouter un commentaire...',
|
|
@@ -1106,7 +1227,7 @@ function getScript() {
|
|
|
1106
1227
|
}
|
|
1107
1228
|
};
|
|
1108
1229
|
|
|
1109
|
-
let currentLang = localStorage.getItem('autocode-
|
|
1230
|
+
let currentLang = localStorage.getItem('autocode-lang') || 'fr';
|
|
1110
1231
|
|
|
1111
1232
|
function t(key) {
|
|
1112
1233
|
return translations[currentLang][key] || translations['en'][key] || key;
|
|
@@ -1114,8 +1235,7 @@ function getScript() {
|
|
|
1114
1235
|
|
|
1115
1236
|
function switchLanguage(lang) {
|
|
1116
1237
|
currentLang = lang;
|
|
1117
|
-
currentActionLang = lang;
|
|
1118
|
-
localStorage.setItem('autocode-ui-lang', lang);
|
|
1238
|
+
currentActionLang = lang;
|
|
1119
1239
|
localStorage.setItem('autocode-lang', lang);
|
|
1120
1240
|
document.documentElement.lang = lang;
|
|
1121
1241
|
|
|
@@ -1163,7 +1283,7 @@ function getScript() {
|
|
|
1163
1283
|
let draggedTicket = null;
|
|
1164
1284
|
let draggedFromColumn = null;
|
|
1165
1285
|
let currentActionSlug = null;
|
|
1166
|
-
let currentActionLang = localStorage.getItem('autocode-lang') || '
|
|
1286
|
+
let currentActionLang = localStorage.getItem('autocode-lang') || 'fr';
|
|
1167
1287
|
let originalActionContent = '';
|
|
1168
1288
|
let claudeProcessingTickets = new Set(); // Tickets currently being processed by Claude
|
|
1169
1289
|
|
|
@@ -1290,6 +1410,7 @@ function getScript() {
|
|
|
1290
1410
|
const nextBtn = document.getElementById('btn-next');
|
|
1291
1411
|
const archiveBtn = document.getElementById('btn-archive');
|
|
1292
1412
|
const commentsSection = document.getElementById('comments-section');
|
|
1413
|
+
const attachmentsSection = document.getElementById('attachments-section');
|
|
1293
1414
|
|
|
1294
1415
|
if (key) {
|
|
1295
1416
|
modalTitle.textContent = t('modal.editTicket') + ' ' + key;
|
|
@@ -1297,6 +1418,7 @@ function getScript() {
|
|
|
1297
1418
|
nextBtn.style.display = 'inline-block';
|
|
1298
1419
|
archiveBtn.style.display = 'inline-block';
|
|
1299
1420
|
commentsSection.style.display = 'block';
|
|
1421
|
+
attachmentsSection.style.display = 'block';
|
|
1300
1422
|
loadTicketForEdit(key);
|
|
1301
1423
|
} else {
|
|
1302
1424
|
modalTitle.textContent = t('modal.newTicket');
|
|
@@ -1304,8 +1426,10 @@ function getScript() {
|
|
|
1304
1426
|
nextBtn.style.display = 'none';
|
|
1305
1427
|
archiveBtn.style.display = 'none';
|
|
1306
1428
|
commentsSection.style.display = 'none';
|
|
1429
|
+
attachmentsSection.style.display = 'none';
|
|
1307
1430
|
resetForm();
|
|
1308
1431
|
resetComments();
|
|
1432
|
+
resetAttachments();
|
|
1309
1433
|
}
|
|
1310
1434
|
}
|
|
1311
1435
|
|
|
@@ -1353,6 +1477,9 @@ function getScript() {
|
|
|
1353
1477
|
|
|
1354
1478
|
renderComments(ticket.comments || []);
|
|
1355
1479
|
|
|
1480
|
+
// Fetch attachments
|
|
1481
|
+
loadAttachments(key);
|
|
1482
|
+
|
|
1356
1483
|
// Fetch Claude log if exists
|
|
1357
1484
|
fetchLog(key);
|
|
1358
1485
|
} catch (e) {
|
|
@@ -1381,7 +1508,7 @@ function getScript() {
|
|
|
1381
1508
|
if (editingKey) {
|
|
1382
1509
|
btn.textContent = t('btn.updating');
|
|
1383
1510
|
await fetch('/api/tickets/' + editingKey, {
|
|
1384
|
-
method: '
|
|
1511
|
+
method: 'PATCH',
|
|
1385
1512
|
headers: { 'Content-Type': 'application/json' },
|
|
1386
1513
|
body: JSON.stringify({ title, description, priority, semver, labels: selectedLabels, acceptance_criteria: criteria })
|
|
1387
1514
|
});
|
|
@@ -1411,7 +1538,11 @@ function getScript() {
|
|
|
1411
1538
|
btn.disabled = true;
|
|
1412
1539
|
btn.textContent = t('btn.moving');
|
|
1413
1540
|
try {
|
|
1414
|
-
await fetch('/api/tickets/' + editingKey + '/next', {
|
|
1541
|
+
await fetch('/api/tickets/' + editingKey + '/next', {
|
|
1542
|
+
method: 'POST',
|
|
1543
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1544
|
+
body: JSON.stringify({ lang: currentLang })
|
|
1545
|
+
});
|
|
1415
1546
|
showNotification('info', t('notify.ticketAdvanced'), editingKey);
|
|
1416
1547
|
closeModal();
|
|
1417
1548
|
loadTicketsFromAPI();
|
|
@@ -1434,7 +1565,7 @@ function getScript() {
|
|
|
1434
1565
|
await fetch('/api/tickets/' + editingKey + '/move', {
|
|
1435
1566
|
method: 'POST',
|
|
1436
1567
|
headers: { 'Content-Type': 'application/json' },
|
|
1437
|
-
body: JSON.stringify({ column: lastColumn.name, force: true })
|
|
1568
|
+
body: JSON.stringify({ column: lastColumn.name, force: true, lang: currentLang })
|
|
1438
1569
|
});
|
|
1439
1570
|
showNotification('info', t('notify.ticketArchived'), editingKey);
|
|
1440
1571
|
closeModal();
|
|
@@ -1513,16 +1644,31 @@ function getScript() {
|
|
|
1513
1644
|
}
|
|
1514
1645
|
|
|
1515
1646
|
const sorted = [...currentComments].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
1516
|
-
list.innerHTML = sorted.map(comment => {
|
|
1647
|
+
list.innerHTML = sorted.map((comment, index) => {
|
|
1517
1648
|
const date = new Date(comment.created_at);
|
|
1518
1649
|
const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
1519
|
-
|
|
1520
|
-
|
|
1650
|
+
const source = comment.source || 'user';
|
|
1651
|
+
const sourceBadge = source === 'claude'
|
|
1652
|
+
? '<span class="comment-source claude">Claude</span>'
|
|
1653
|
+
: '<span class="comment-source user">User</span>';
|
|
1654
|
+
return '<div class="comment" id="comment-' + index + '">' +
|
|
1655
|
+
'<div class="comment-meta" onclick="toggleComment(' + index + ')">' + sourceBadge + '<span class="comment-column">' + (comment.column || 'N/A') + '</span>' +
|
|
1521
1656
|
'<span class="comment-date">' + dateStr + '</span></div>' +
|
|
1522
1657
|
'<div class="comment-text">' + renderMarkdown(comment.text) + '</div></div>';
|
|
1523
1658
|
}).join('');
|
|
1524
1659
|
}
|
|
1525
1660
|
|
|
1661
|
+
function toggleComment(index) {
|
|
1662
|
+
const comments = document.querySelectorAll('.comment');
|
|
1663
|
+
comments.forEach((comment, i) => {
|
|
1664
|
+
if (i === index) {
|
|
1665
|
+
comment.classList.toggle('expanded');
|
|
1666
|
+
} else {
|
|
1667
|
+
comment.classList.remove('expanded');
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1526
1672
|
function renderMarkdown(text) {
|
|
1527
1673
|
if (!text) return '';
|
|
1528
1674
|
let html = escapeHtml(text);
|
|
@@ -1557,7 +1703,9 @@ function getScript() {
|
|
|
1557
1703
|
});
|
|
1558
1704
|
const result = await res.json();
|
|
1559
1705
|
textarea.value = '';
|
|
1560
|
-
if (result.
|
|
1706
|
+
if (result.success && result.data && result.data.comments) {
|
|
1707
|
+
renderComments(result.data.comments);
|
|
1708
|
+
}
|
|
1561
1709
|
showNotification('success', t('notify.commentAdded'), '');
|
|
1562
1710
|
} catch (e) {
|
|
1563
1711
|
showNotification('error', t('notify.error'), e.message);
|
|
@@ -1567,6 +1715,112 @@ function getScript() {
|
|
|
1567
1715
|
}
|
|
1568
1716
|
}
|
|
1569
1717
|
|
|
1718
|
+
// ========================================
|
|
1719
|
+
// ATTACHMENTS
|
|
1720
|
+
// ========================================
|
|
1721
|
+
let currentAttachments = [];
|
|
1722
|
+
|
|
1723
|
+
function resetAttachments() {
|
|
1724
|
+
currentAttachments = [];
|
|
1725
|
+
document.getElementById('attachments-list').innerHTML = '<div class="no-attachments">' + t('modal.noAttachments') + '</div>';
|
|
1726
|
+
document.getElementById('attachments-count').textContent = '0';
|
|
1727
|
+
document.getElementById('file-input').value = '';
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
async function loadAttachments(key) {
|
|
1731
|
+
try {
|
|
1732
|
+
const res = await fetch('/api/tickets/' + key + '/attachments');
|
|
1733
|
+
const json = await res.json();
|
|
1734
|
+
if (json.success) {
|
|
1735
|
+
currentAttachments = json.data || [];
|
|
1736
|
+
renderAttachments();
|
|
1737
|
+
}
|
|
1738
|
+
} catch (e) {
|
|
1739
|
+
console.error('Error loading attachments:', e);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function renderAttachments() {
|
|
1744
|
+
const list = document.getElementById('attachments-list');
|
|
1745
|
+
const count = document.getElementById('attachments-count');
|
|
1746
|
+
count.textContent = currentAttachments.length;
|
|
1747
|
+
|
|
1748
|
+
if (currentAttachments.length === 0) {
|
|
1749
|
+
list.innerHTML = '<div class="no-attachments">' + t('modal.noAttachments') + '</div>';
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
list.innerHTML = currentAttachments.map(filename => {
|
|
1754
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
1755
|
+
const icon = getFileIcon(ext);
|
|
1756
|
+
return '<div class="attachment-item">' +
|
|
1757
|
+
'<span class="attachment-icon">' + icon + '</span>' +
|
|
1758
|
+
'<span class="attachment-name" title="' + escapeHtml(filename) + '">' + escapeHtml(filename) + '</span>' +
|
|
1759
|
+
'<button class="attachment-delete" onclick="deleteAttachment(\\'' + escapeHtml(filename) + '\\')" title="Delete">×</button>' +
|
|
1760
|
+
'</div>';
|
|
1761
|
+
}).join('');
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function getFileIcon(ext) {
|
|
1765
|
+
const icons = {
|
|
1766
|
+
pdf: '📄', doc: '📝', docx: '📝', txt: '📝',
|
|
1767
|
+
png: '🖼️', jpg: '🖼️', jpeg: '🖼️', gif: '🖼️', svg: '🖼️', webp: '🖼️',
|
|
1768
|
+
mp4: '🎬', mov: '🎬', avi: '🎬',
|
|
1769
|
+
mp3: '🎵', wav: '🎵',
|
|
1770
|
+
zip: '📦', rar: '📦', tar: '📦', gz: '📦',
|
|
1771
|
+
js: '📜', ts: '📜', py: '📜', json: '📜', md: '📜',
|
|
1772
|
+
};
|
|
1773
|
+
return icons[ext] || '📎';
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
async function uploadFiles(files) {
|
|
1777
|
+
if (!editingKey || files.length === 0) return;
|
|
1778
|
+
|
|
1779
|
+
const formData = new FormData();
|
|
1780
|
+
for (const file of files) {
|
|
1781
|
+
formData.append('files', file, file.name);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
try {
|
|
1785
|
+
const res = await fetch('/api/tickets/' + editingKey + '/attachments', {
|
|
1786
|
+
method: 'POST',
|
|
1787
|
+
body: formData
|
|
1788
|
+
});
|
|
1789
|
+
const json = await res.json();
|
|
1790
|
+
if (json.success) {
|
|
1791
|
+
showNotification('success', 'Files uploaded', json.data.join(', '));
|
|
1792
|
+
loadAttachments(editingKey);
|
|
1793
|
+
} else {
|
|
1794
|
+
showNotification('error', 'Upload failed', json.error);
|
|
1795
|
+
}
|
|
1796
|
+
} catch (e) {
|
|
1797
|
+
showNotification('error', 'Upload error', e.message);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// Reset file input
|
|
1801
|
+
document.getElementById('file-input').value = '';
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
async function deleteAttachment(filename) {
|
|
1805
|
+
if (!editingKey) return;
|
|
1806
|
+
if (!confirm('Delete ' + filename + '?')) return;
|
|
1807
|
+
|
|
1808
|
+
try {
|
|
1809
|
+
const res = await fetch('/api/tickets/' + editingKey + '/attachments/' + encodeURIComponent(filename), {
|
|
1810
|
+
method: 'DELETE'
|
|
1811
|
+
});
|
|
1812
|
+
const json = await res.json();
|
|
1813
|
+
if (json.success) {
|
|
1814
|
+
showNotification('info', 'File deleted', filename);
|
|
1815
|
+
loadAttachments(editingKey);
|
|
1816
|
+
} else {
|
|
1817
|
+
showNotification('error', 'Delete failed', json.error);
|
|
1818
|
+
}
|
|
1819
|
+
} catch (e) {
|
|
1820
|
+
showNotification('error', 'Delete error', e.message);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1570
1824
|
// ========================================
|
|
1571
1825
|
// DRAG & DROP
|
|
1572
1826
|
// ========================================
|
|
@@ -1620,7 +1874,7 @@ function getScript() {
|
|
|
1620
1874
|
await fetch('/api/tickets/' + key + '/move', {
|
|
1621
1875
|
method: 'POST',
|
|
1622
1876
|
headers: { 'Content-Type': 'application/json' },
|
|
1623
|
-
body: JSON.stringify({ column: targetColumnName, force: true })
|
|
1877
|
+
body: JSON.stringify({ column: targetColumnName, force: true, lang: currentLang })
|
|
1624
1878
|
});
|
|
1625
1879
|
showNotification('info', key + ' ' + t('notify.ticketMoved'), t('notify.moveTo') + ' "' + targetColumnName + '"');
|
|
1626
1880
|
loadTicketsFromAPI();
|
|
@@ -1662,7 +1916,7 @@ function getScript() {
|
|
|
1662
1916
|
await fetch('/api/tickets/' + key + '/move', {
|
|
1663
1917
|
method: 'POST',
|
|
1664
1918
|
headers: { 'Content-Type': 'application/json' },
|
|
1665
|
-
body: JSON.stringify({ column: lastColumn.name, force: true })
|
|
1919
|
+
body: JSON.stringify({ column: lastColumn.name, force: true, lang: currentLang })
|
|
1666
1920
|
});
|
|
1667
1921
|
showNotification('info', t('notify.ticketArchived'), key);
|
|
1668
1922
|
loadTicketsFromAPI();
|
|
@@ -1947,7 +2201,7 @@ function getScript() {
|
|
|
1947
2201
|
}
|
|
1948
2202
|
|
|
1949
2203
|
function onTicketClick(key) {
|
|
1950
|
-
|
|
2204
|
+
window.location.href = '/ticket/' + key;
|
|
1951
2205
|
}
|
|
1952
2206
|
|
|
1953
2207
|
// ========================================
|
|
@@ -2119,9 +2373,9 @@ export function generateColumnEditPage(slug, lang) {
|
|
|
2119
2373
|
<header class="header">
|
|
2120
2374
|
<a href="/" class="back-btn">← Dashboard</a>
|
|
2121
2375
|
<h1 class="title">${escapeHtml(columnName)} <span>/ ACTION.md</span></h1>
|
|
2122
|
-
<div class="lang-selector">
|
|
2123
|
-
<button class="lang-btn
|
|
2124
|
-
<button class="lang-btn
|
|
2376
|
+
<div class="lang-selector" id="lang-selector">
|
|
2377
|
+
<button class="lang-btn" data-lang="en">EN</button>
|
|
2378
|
+
<button class="lang-btn" data-lang="fr">FR</button>
|
|
2125
2379
|
</div>
|
|
2126
2380
|
<div class="actions">
|
|
2127
2381
|
<button class="btn btn-save" id="saveBtn" disabled>Save</button>
|
|
@@ -2137,8 +2391,9 @@ export function generateColumnEditPage(slug, lang) {
|
|
|
2137
2391
|
<div class="notification" id="notification"></div>
|
|
2138
2392
|
|
|
2139
2393
|
<script>
|
|
2394
|
+
const STORAGE_KEY = 'autocode-lang';
|
|
2140
2395
|
const slug = '${slug}';
|
|
2141
|
-
let currentLang = '
|
|
2396
|
+
let currentLang = localStorage.getItem(STORAGE_KEY) || 'fr';
|
|
2142
2397
|
let originalContent = '';
|
|
2143
2398
|
let hasChanges = false;
|
|
2144
2399
|
|
|
@@ -2148,6 +2403,14 @@ export function generateColumnEditPage(slug, lang) {
|
|
|
2148
2403
|
const pathEl = document.getElementById('path');
|
|
2149
2404
|
const notification = document.getElementById('notification');
|
|
2150
2405
|
|
|
2406
|
+
// Update UI to reflect current language
|
|
2407
|
+
function updateLangUI() {
|
|
2408
|
+
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
2409
|
+
btn.classList.toggle('active', btn.dataset.lang === currentLang);
|
|
2410
|
+
});
|
|
2411
|
+
pathEl.textContent = slug + '/ACTION.' + currentLang + '.md';
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2151
2414
|
// Load content
|
|
2152
2415
|
async function loadContent() {
|
|
2153
2416
|
try {
|
|
@@ -2237,8 +2500,8 @@ export function generateColumnEditPage(slug, lang) {
|
|
|
2237
2500
|
return;
|
|
2238
2501
|
}
|
|
2239
2502
|
currentLang = newLang;
|
|
2240
|
-
|
|
2241
|
-
|
|
2503
|
+
localStorage.setItem(STORAGE_KEY, newLang);
|
|
2504
|
+
updateLangUI();
|
|
2242
2505
|
loadContent();
|
|
2243
2506
|
}
|
|
2244
2507
|
});
|
|
@@ -2253,9 +2516,1197 @@ export function generateColumnEditPage(slug, lang) {
|
|
|
2253
2516
|
});
|
|
2254
2517
|
|
|
2255
2518
|
// Init
|
|
2519
|
+
updateLangUI();
|
|
2256
2520
|
loadContent();
|
|
2257
2521
|
</script>
|
|
2258
2522
|
</body>
|
|
2259
2523
|
</html>`;
|
|
2260
2524
|
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Generate ticket view page
|
|
2527
|
+
*/
|
|
2528
|
+
export function generateTicketViewPage(ticketKey, lang) {
|
|
2529
|
+
const config = getConfig();
|
|
2530
|
+
const ticket = getTicket(config.root, ticketKey);
|
|
2531
|
+
const columns = getColumns();
|
|
2532
|
+
// 404 page if ticket not found
|
|
2533
|
+
if (!ticket) {
|
|
2534
|
+
return `<!DOCTYPE html>
|
|
2535
|
+
<html lang="${lang}">
|
|
2536
|
+
<head>
|
|
2537
|
+
<meta charset="UTF-8">
|
|
2538
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2539
|
+
<title>Ticket Not Found - AutoCode</title>
|
|
2540
|
+
<style>
|
|
2541
|
+
:root {
|
|
2542
|
+
--bg: #0a0a0f;
|
|
2543
|
+
--text: #f1f5f9;
|
|
2544
|
+
--accent: #6366f1;
|
|
2545
|
+
}
|
|
2546
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2547
|
+
body {
|
|
2548
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2549
|
+
background: var(--bg);
|
|
2550
|
+
color: var(--text);
|
|
2551
|
+
min-height: 100vh;
|
|
2552
|
+
display: flex;
|
|
2553
|
+
align-items: center;
|
|
2554
|
+
justify-content: center;
|
|
2555
|
+
}
|
|
2556
|
+
.not-found {
|
|
2557
|
+
text-align: center;
|
|
2558
|
+
padding: 48px;
|
|
2559
|
+
}
|
|
2560
|
+
h1 { font-size: 2rem; margin-bottom: 16px; }
|
|
2561
|
+
p { color: #94a3b8; margin-bottom: 24px; }
|
|
2562
|
+
a {
|
|
2563
|
+
display: inline-block;
|
|
2564
|
+
background: var(--accent);
|
|
2565
|
+
color: white;
|
|
2566
|
+
padding: 12px 24px;
|
|
2567
|
+
border-radius: 8px;
|
|
2568
|
+
text-decoration: none;
|
|
2569
|
+
font-weight: 500;
|
|
2570
|
+
}
|
|
2571
|
+
a:hover { opacity: 0.9; }
|
|
2572
|
+
</style>
|
|
2573
|
+
</head>
|
|
2574
|
+
<body>
|
|
2575
|
+
<div class="not-found">
|
|
2576
|
+
<h1>Ticket Not Found</h1>
|
|
2577
|
+
<p>Ticket ${escapeHtml(ticketKey)} does not exist or has been archived.</p>
|
|
2578
|
+
<a href="/">← Back to Dashboard</a>
|
|
2579
|
+
</div>
|
|
2580
|
+
</body>
|
|
2581
|
+
</html>`;
|
|
2582
|
+
}
|
|
2583
|
+
const currentColumn = columns.find(c => c.slug === ticket.column_slug);
|
|
2584
|
+
const ticketData = JSON.stringify(ticket);
|
|
2585
|
+
const columnsData = JSON.stringify(columns);
|
|
2586
|
+
return `<!DOCTYPE html>
|
|
2587
|
+
<html lang="${lang}">
|
|
2588
|
+
<head>
|
|
2589
|
+
<meta charset="UTF-8">
|
|
2590
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2591
|
+
<title>${escapeHtml(ticket.title)} - ${ticketKey} - AutoCode</title>
|
|
2592
|
+
<style>
|
|
2593
|
+
:root {
|
|
2594
|
+
--bg: #0a0a0f;
|
|
2595
|
+
--bg-secondary: #12121a;
|
|
2596
|
+
--bg-tertiary: #1a1a24;
|
|
2597
|
+
--text: #f1f5f9;
|
|
2598
|
+
--muted: #94a3b8;
|
|
2599
|
+
--border: #2a2a3a;
|
|
2600
|
+
--accent: #6366f1;
|
|
2601
|
+
--blue: #4dabf7;
|
|
2602
|
+
--green: #4ade80;
|
|
2603
|
+
--yellow: #facc15;
|
|
2604
|
+
--orange: #fb923c;
|
|
2605
|
+
--red: #f87171;
|
|
2606
|
+
--purple: #a78bfa;
|
|
2607
|
+
}
|
|
2608
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2609
|
+
body {
|
|
2610
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2611
|
+
background: var(--bg);
|
|
2612
|
+
color: var(--text);
|
|
2613
|
+
min-height: 100vh;
|
|
2614
|
+
}
|
|
2615
|
+
.header {
|
|
2616
|
+
display: flex;
|
|
2617
|
+
align-items: center;
|
|
2618
|
+
gap: 24px;
|
|
2619
|
+
padding: 16px 24px;
|
|
2620
|
+
background: var(--bg-secondary);
|
|
2621
|
+
border-bottom: 1px solid var(--border);
|
|
2622
|
+
position: sticky;
|
|
2623
|
+
top: 0;
|
|
2624
|
+
z-index: 100;
|
|
2625
|
+
}
|
|
2626
|
+
.back-btn {
|
|
2627
|
+
color: var(--muted);
|
|
2628
|
+
text-decoration: none;
|
|
2629
|
+
font-size: 14px;
|
|
2630
|
+
display: flex;
|
|
2631
|
+
align-items: center;
|
|
2632
|
+
gap: 8px;
|
|
2633
|
+
}
|
|
2634
|
+
.back-btn:hover { color: var(--text); }
|
|
2635
|
+
.ticket-header-info {
|
|
2636
|
+
flex: 1;
|
|
2637
|
+
display: flex;
|
|
2638
|
+
align-items: center;
|
|
2639
|
+
gap: 16px;
|
|
2640
|
+
}
|
|
2641
|
+
.ticket-key {
|
|
2642
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
2643
|
+
font-size: 12px;
|
|
2644
|
+
color: var(--accent);
|
|
2645
|
+
background: rgba(99, 102, 241, 0.15);
|
|
2646
|
+
padding: 4px 10px;
|
|
2647
|
+
border-radius: 4px;
|
|
2648
|
+
font-weight: 600;
|
|
2649
|
+
}
|
|
2650
|
+
.ticket-title {
|
|
2651
|
+
font-size: 18px;
|
|
2652
|
+
font-weight: 600;
|
|
2653
|
+
}
|
|
2654
|
+
.lang-selector {
|
|
2655
|
+
display: flex;
|
|
2656
|
+
gap: 4px;
|
|
2657
|
+
}
|
|
2658
|
+
.lang-btn {
|
|
2659
|
+
background: transparent;
|
|
2660
|
+
border: 1px solid var(--border);
|
|
2661
|
+
color: var(--muted);
|
|
2662
|
+
padding: 6px 12px;
|
|
2663
|
+
border-radius: 4px;
|
|
2664
|
+
cursor: pointer;
|
|
2665
|
+
font-size: 12px;
|
|
2666
|
+
font-weight: 500;
|
|
2667
|
+
}
|
|
2668
|
+
.lang-btn:hover { border-color: var(--accent); color: var(--text); }
|
|
2669
|
+
.lang-btn.active { background: var(--accent); border-color: var(--accent); color: white; }
|
|
2670
|
+
.ticket-title-input {
|
|
2671
|
+
flex: 1;
|
|
2672
|
+
background: transparent;
|
|
2673
|
+
border: 1px solid transparent;
|
|
2674
|
+
color: var(--text);
|
|
2675
|
+
font-size: 18px;
|
|
2676
|
+
font-weight: 600;
|
|
2677
|
+
padding: 4px 8px;
|
|
2678
|
+
border-radius: 4px;
|
|
2679
|
+
font-family: inherit;
|
|
2680
|
+
}
|
|
2681
|
+
.ticket-title-input:hover { border-color: var(--border); }
|
|
2682
|
+
.ticket-title-input:focus {
|
|
2683
|
+
outline: none;
|
|
2684
|
+
border-color: var(--accent);
|
|
2685
|
+
background: var(--bg-tertiary);
|
|
2686
|
+
}
|
|
2687
|
+
.section-title {
|
|
2688
|
+
display: flex;
|
|
2689
|
+
align-items: center;
|
|
2690
|
+
justify-content: space-between;
|
|
2691
|
+
}
|
|
2692
|
+
.btn-edit-toggle {
|
|
2693
|
+
background: transparent;
|
|
2694
|
+
border: none;
|
|
2695
|
+
cursor: pointer;
|
|
2696
|
+
font-size: 14px;
|
|
2697
|
+
opacity: 0.5;
|
|
2698
|
+
transition: opacity 0.2s;
|
|
2699
|
+
}
|
|
2700
|
+
.btn-edit-toggle:hover { opacity: 1; }
|
|
2701
|
+
.btn-edit-toggle.active { opacity: 1; }
|
|
2702
|
+
.description-view {
|
|
2703
|
+
line-height: 1.7;
|
|
2704
|
+
color: var(--text);
|
|
2705
|
+
}
|
|
2706
|
+
.description-view p { margin-bottom: 12px; }
|
|
2707
|
+
.description-view h1, .description-view h2, .description-view h3, .description-view h4 {
|
|
2708
|
+
margin: 16px 0 8px;
|
|
2709
|
+
font-weight: 600;
|
|
2710
|
+
}
|
|
2711
|
+
.description-view code {
|
|
2712
|
+
background: var(--bg-tertiary);
|
|
2713
|
+
padding: 2px 6px;
|
|
2714
|
+
border-radius: 4px;
|
|
2715
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
2716
|
+
font-size: 13px;
|
|
2717
|
+
}
|
|
2718
|
+
.description-view pre {
|
|
2719
|
+
background: var(--bg-tertiary);
|
|
2720
|
+
padding: 12px;
|
|
2721
|
+
border-radius: 6px;
|
|
2722
|
+
overflow-x: auto;
|
|
2723
|
+
}
|
|
2724
|
+
.description-view ul, .description-view ol {
|
|
2725
|
+
margin: 8px 0;
|
|
2726
|
+
padding-left: 24px;
|
|
2727
|
+
}
|
|
2728
|
+
.description-view a { color: var(--accent); }
|
|
2729
|
+
.description-edit {
|
|
2730
|
+
width: 100%;
|
|
2731
|
+
min-height: 200px;
|
|
2732
|
+
padding: 12px;
|
|
2733
|
+
background: var(--bg-tertiary);
|
|
2734
|
+
border: 1px solid var(--border);
|
|
2735
|
+
border-radius: 8px;
|
|
2736
|
+
color: var(--text);
|
|
2737
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
2738
|
+
font-size: 13px;
|
|
2739
|
+
line-height: 1.6;
|
|
2740
|
+
resize: vertical;
|
|
2741
|
+
}
|
|
2742
|
+
.description-edit:focus {
|
|
2743
|
+
outline: none;
|
|
2744
|
+
border-color: var(--accent);
|
|
2745
|
+
}
|
|
2746
|
+
.main-content {
|
|
2747
|
+
display: flex;
|
|
2748
|
+
flex-direction: column;
|
|
2749
|
+
gap: 24px;
|
|
2750
|
+
padding: 24px 48px;
|
|
2751
|
+
}
|
|
2752
|
+
.ticket-details { display: flex; flex-direction: column; gap: 24px; }
|
|
2753
|
+
.ticket-bottom {
|
|
2754
|
+
display: flex;
|
|
2755
|
+
flex-direction: column;
|
|
2756
|
+
gap: 24px;
|
|
2757
|
+
}
|
|
2758
|
+
.section {
|
|
2759
|
+
background: var(--bg-secondary);
|
|
2760
|
+
border: 1px solid var(--border);
|
|
2761
|
+
border-radius: 12px;
|
|
2762
|
+
padding: 20px;
|
|
2763
|
+
}
|
|
2764
|
+
.section-title {
|
|
2765
|
+
font-size: 12px;
|
|
2766
|
+
font-weight: 600;
|
|
2767
|
+
text-transform: uppercase;
|
|
2768
|
+
letter-spacing: 0.5px;
|
|
2769
|
+
color: var(--muted);
|
|
2770
|
+
margin-bottom: 16px;
|
|
2771
|
+
}
|
|
2772
|
+
.ticket-meta {
|
|
2773
|
+
display: flex;
|
|
2774
|
+
flex-wrap: wrap;
|
|
2775
|
+
gap: 12px;
|
|
2776
|
+
}
|
|
2777
|
+
.meta-badge {
|
|
2778
|
+
font-size: 11px;
|
|
2779
|
+
font-weight: 600;
|
|
2780
|
+
text-transform: uppercase;
|
|
2781
|
+
letter-spacing: 0.5px;
|
|
2782
|
+
padding: 5px 12px;
|
|
2783
|
+
border-radius: 6px;
|
|
2784
|
+
}
|
|
2785
|
+
.priority-P0 { background: rgba(248, 113, 113, 0.2); color: var(--red); }
|
|
2786
|
+
.priority-P1 { background: rgba(251, 146, 60, 0.2); color: var(--orange); }
|
|
2787
|
+
.priority-P2 { background: rgba(250, 204, 21, 0.2); color: var(--yellow); }
|
|
2788
|
+
.priority-P3 { background: rgba(148, 163, 184, 0.2); color: var(--muted); }
|
|
2789
|
+
.column-badge { background: rgba(77, 171, 247, 0.15); color: var(--blue); }
|
|
2790
|
+
.semver-badge { background: rgba(167, 139, 250, 0.15); color: var(--purple); }
|
|
2791
|
+
.labels-list {
|
|
2792
|
+
display: flex;
|
|
2793
|
+
flex-wrap: wrap;
|
|
2794
|
+
gap: 8px;
|
|
2795
|
+
}
|
|
2796
|
+
.label-tag {
|
|
2797
|
+
font-size: 11px;
|
|
2798
|
+
padding: 4px 10px;
|
|
2799
|
+
border-radius: 12px;
|
|
2800
|
+
background: var(--bg-tertiary);
|
|
2801
|
+
color: var(--text);
|
|
2802
|
+
border: 1px solid var(--border);
|
|
2803
|
+
}
|
|
2804
|
+
.description-content {
|
|
2805
|
+
line-height: 1.7;
|
|
2806
|
+
color: var(--text);
|
|
2807
|
+
}
|
|
2808
|
+
.description-content p { margin-bottom: 12px; }
|
|
2809
|
+
.description-content code {
|
|
2810
|
+
background: var(--bg-tertiary);
|
|
2811
|
+
padding: 2px 6px;
|
|
2812
|
+
border-radius: 4px;
|
|
2813
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
2814
|
+
font-size: 13px;
|
|
2815
|
+
}
|
|
2816
|
+
.criteria-list { list-style: none; }
|
|
2817
|
+
.criteria-item {
|
|
2818
|
+
padding: 12px 16px;
|
|
2819
|
+
background: var(--bg-tertiary);
|
|
2820
|
+
border-radius: 8px;
|
|
2821
|
+
margin-bottom: 8px;
|
|
2822
|
+
display: flex;
|
|
2823
|
+
align-items: flex-start;
|
|
2824
|
+
gap: 12px;
|
|
2825
|
+
}
|
|
2826
|
+
.criteria-item::before {
|
|
2827
|
+
content: '☐';
|
|
2828
|
+
color: var(--muted);
|
|
2829
|
+
}
|
|
2830
|
+
.history-list { list-style: none; }
|
|
2831
|
+
.history-item {
|
|
2832
|
+
padding: 12px 0;
|
|
2833
|
+
border-bottom: 1px solid var(--border);
|
|
2834
|
+
display: flex;
|
|
2835
|
+
align-items: center;
|
|
2836
|
+
gap: 12px;
|
|
2837
|
+
font-size: 13px;
|
|
2838
|
+
}
|
|
2839
|
+
.history-item:last-child { border-bottom: none; }
|
|
2840
|
+
.history-action {
|
|
2841
|
+
font-weight: 600;
|
|
2842
|
+
text-transform: capitalize;
|
|
2843
|
+
}
|
|
2844
|
+
.history-from, .history-to {
|
|
2845
|
+
padding: 2px 8px;
|
|
2846
|
+
background: var(--bg-tertiary);
|
|
2847
|
+
border-radius: 4px;
|
|
2848
|
+
font-size: 11px;
|
|
2849
|
+
}
|
|
2850
|
+
.history-date { color: var(--muted); margin-left: auto; font-size: 12px; }
|
|
2851
|
+
.btn-prompt {
|
|
2852
|
+
background: none;
|
|
2853
|
+
border: none;
|
|
2854
|
+
cursor: pointer;
|
|
2855
|
+
font-size: 14px;
|
|
2856
|
+
padding: 2px 6px;
|
|
2857
|
+
opacity: 0.6;
|
|
2858
|
+
transition: opacity 0.2s;
|
|
2859
|
+
}
|
|
2860
|
+
.btn-prompt:hover { opacity: 1; }
|
|
2861
|
+
.prompt-modal {
|
|
2862
|
+
display: none;
|
|
2863
|
+
position: fixed;
|
|
2864
|
+
top: 0;
|
|
2865
|
+
left: 0;
|
|
2866
|
+
right: 0;
|
|
2867
|
+
bottom: 0;
|
|
2868
|
+
background: rgba(0, 0, 0, 0.7);
|
|
2869
|
+
z-index: 1000;
|
|
2870
|
+
align-items: center;
|
|
2871
|
+
justify-content: center;
|
|
2872
|
+
}
|
|
2873
|
+
.prompt-modal.visible { display: flex; }
|
|
2874
|
+
.prompt-modal-content {
|
|
2875
|
+
background: var(--bg-primary);
|
|
2876
|
+
border-radius: 12px;
|
|
2877
|
+
max-width: 900px;
|
|
2878
|
+
max-height: 80vh;
|
|
2879
|
+
width: 90%;
|
|
2880
|
+
display: flex;
|
|
2881
|
+
flex-direction: column;
|
|
2882
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
|
2883
|
+
}
|
|
2884
|
+
.prompt-modal-header {
|
|
2885
|
+
display: flex;
|
|
2886
|
+
justify-content: space-between;
|
|
2887
|
+
align-items: center;
|
|
2888
|
+
padding: 16px 20px;
|
|
2889
|
+
border-bottom: 1px solid var(--border);
|
|
2890
|
+
}
|
|
2891
|
+
.prompt-modal-header h3 { margin: 0; }
|
|
2892
|
+
.prompt-modal-close {
|
|
2893
|
+
background: none;
|
|
2894
|
+
border: none;
|
|
2895
|
+
font-size: 24px;
|
|
2896
|
+
cursor: pointer;
|
|
2897
|
+
color: var(--muted);
|
|
2898
|
+
}
|
|
2899
|
+
.prompt-modal-close:hover { color: var(--text); }
|
|
2900
|
+
.prompt-modal-body {
|
|
2901
|
+
padding: 20px;
|
|
2902
|
+
overflow-y: auto;
|
|
2903
|
+
flex: 1;
|
|
2904
|
+
}
|
|
2905
|
+
.prompt-modal-body pre {
|
|
2906
|
+
white-space: pre-wrap;
|
|
2907
|
+
word-wrap: break-word;
|
|
2908
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
2909
|
+
font-size: 13px;
|
|
2910
|
+
line-height: 1.5;
|
|
2911
|
+
margin: 0;
|
|
2912
|
+
background: var(--bg-secondary);
|
|
2913
|
+
padding: 16px;
|
|
2914
|
+
border-radius: 8px;
|
|
2915
|
+
}
|
|
2916
|
+
.actions-bar {
|
|
2917
|
+
display: flex;
|
|
2918
|
+
gap: 12px;
|
|
2919
|
+
padding: 16px 0;
|
|
2920
|
+
border-top: 1px solid var(--border);
|
|
2921
|
+
margin-top: 8px;
|
|
2922
|
+
}
|
|
2923
|
+
.btn {
|
|
2924
|
+
padding: 12px 20px;
|
|
2925
|
+
border-radius: 8px;
|
|
2926
|
+
font-weight: 500;
|
|
2927
|
+
font-size: 14px;
|
|
2928
|
+
cursor: pointer;
|
|
2929
|
+
border: none;
|
|
2930
|
+
text-align: center;
|
|
2931
|
+
text-decoration: none;
|
|
2932
|
+
}
|
|
2933
|
+
.btn-primary { background: var(--accent); color: white; }
|
|
2934
|
+
.btn-primary:hover { opacity: 0.9; }
|
|
2935
|
+
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
2936
|
+
.btn-secondary { background: var(--bg-tertiary); color: var(--text); border: 1px solid var(--border); }
|
|
2937
|
+
.btn-secondary:hover { border-color: var(--accent); }
|
|
2938
|
+
.btn-danger { background: rgba(248, 113, 113, 0.15); color: var(--red); border: 1px solid transparent; }
|
|
2939
|
+
.btn-danger:hover { border-color: var(--red); }
|
|
2940
|
+
.comments-list {
|
|
2941
|
+
display: flex;
|
|
2942
|
+
flex-direction: column;
|
|
2943
|
+
gap: 12px;
|
|
2944
|
+
margin-bottom: 16px;
|
|
2945
|
+
}
|
|
2946
|
+
.comment {
|
|
2947
|
+
padding: 16px;
|
|
2948
|
+
background: var(--bg-tertiary);
|
|
2949
|
+
border-radius: 8px;
|
|
2950
|
+
border-left: 3px solid var(--border);
|
|
2951
|
+
transition: all 0.2s ease;
|
|
2952
|
+
}
|
|
2953
|
+
.comment:hover {
|
|
2954
|
+
border-left-color: var(--blue);
|
|
2955
|
+
}
|
|
2956
|
+
.comment-meta {
|
|
2957
|
+
display: flex;
|
|
2958
|
+
align-items: center;
|
|
2959
|
+
flex-wrap: wrap;
|
|
2960
|
+
gap: 8px;
|
|
2961
|
+
cursor: pointer;
|
|
2962
|
+
user-select: none;
|
|
2963
|
+
}
|
|
2964
|
+
.comment-meta::before {
|
|
2965
|
+
content: '▶';
|
|
2966
|
+
font-size: 10px;
|
|
2967
|
+
color: var(--muted);
|
|
2968
|
+
transition: transform 0.2s ease;
|
|
2969
|
+
}
|
|
2970
|
+
.comment.expanded .comment-meta::before {
|
|
2971
|
+
transform: rotate(90deg);
|
|
2972
|
+
}
|
|
2973
|
+
.comment-source {
|
|
2974
|
+
font-size: 10px;
|
|
2975
|
+
padding: 3px 8px;
|
|
2976
|
+
border-radius: 4px;
|
|
2977
|
+
text-transform: uppercase;
|
|
2978
|
+
font-weight: 600;
|
|
2979
|
+
letter-spacing: 0.5px;
|
|
2980
|
+
}
|
|
2981
|
+
.comment-source.user { background: #3b82f6; color: white; }
|
|
2982
|
+
.comment-source.claude { background: #8b5cf6; color: white; }
|
|
2983
|
+
.comment-column {
|
|
2984
|
+
font-size: 10px;
|
|
2985
|
+
font-weight: 600;
|
|
2986
|
+
text-transform: uppercase;
|
|
2987
|
+
letter-spacing: 0.5px;
|
|
2988
|
+
color: var(--blue);
|
|
2989
|
+
padding: 3px 8px;
|
|
2990
|
+
background: rgba(77,171,247,0.15);
|
|
2991
|
+
border-radius: 4px;
|
|
2992
|
+
}
|
|
2993
|
+
.comment-date {
|
|
2994
|
+
font-size: 11px;
|
|
2995
|
+
color: var(--muted);
|
|
2996
|
+
}
|
|
2997
|
+
.comment-text {
|
|
2998
|
+
font-size: 14px;
|
|
2999
|
+
line-height: 1.6;
|
|
3000
|
+
color: var(--text);
|
|
3001
|
+
max-height: 0;
|
|
3002
|
+
overflow: hidden;
|
|
3003
|
+
transition: max-height 0.3s ease, margin-top 0.3s ease, padding-top 0.3s ease;
|
|
3004
|
+
margin-top: 0;
|
|
3005
|
+
padding-top: 0;
|
|
3006
|
+
}
|
|
3007
|
+
.comment.expanded .comment-text {
|
|
3008
|
+
max-height: 500px;
|
|
3009
|
+
margin-top: 12px;
|
|
3010
|
+
padding-top: 12px;
|
|
3011
|
+
border-top: 1px solid var(--border);
|
|
3012
|
+
}
|
|
3013
|
+
.comment-text code {
|
|
3014
|
+
background: var(--bg);
|
|
3015
|
+
padding: 2px 6px;
|
|
3016
|
+
border-radius: 4px;
|
|
3017
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
3018
|
+
font-size: 12px;
|
|
3019
|
+
}
|
|
3020
|
+
.add-comment {
|
|
3021
|
+
display: flex;
|
|
3022
|
+
flex-direction: column;
|
|
3023
|
+
gap: 8px;
|
|
3024
|
+
}
|
|
3025
|
+
.add-comment textarea {
|
|
3026
|
+
width: 100%;
|
|
3027
|
+
min-height: 80px;
|
|
3028
|
+
padding: 12px;
|
|
3029
|
+
background: var(--bg-tertiary);
|
|
3030
|
+
border: 1px solid var(--border);
|
|
3031
|
+
border-radius: 8px;
|
|
3032
|
+
color: var(--text);
|
|
3033
|
+
font-family: inherit;
|
|
3034
|
+
font-size: 14px;
|
|
3035
|
+
resize: vertical;
|
|
3036
|
+
}
|
|
3037
|
+
.add-comment textarea:focus {
|
|
3038
|
+
outline: none;
|
|
3039
|
+
border-color: var(--accent);
|
|
3040
|
+
}
|
|
3041
|
+
.btn-comment {
|
|
3042
|
+
align-self: flex-end;
|
|
3043
|
+
padding: 8px 16px;
|
|
3044
|
+
background: var(--accent);
|
|
3045
|
+
color: white;
|
|
3046
|
+
border: none;
|
|
3047
|
+
border-radius: 6px;
|
|
3048
|
+
cursor: pointer;
|
|
3049
|
+
font-size: 13px;
|
|
3050
|
+
font-weight: 500;
|
|
3051
|
+
}
|
|
3052
|
+
.btn-comment:hover { opacity: 0.9; }
|
|
3053
|
+
.btn-comment:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
3054
|
+
.no-comments {
|
|
3055
|
+
text-align: center;
|
|
3056
|
+
color: var(--muted);
|
|
3057
|
+
padding: 24px;
|
|
3058
|
+
font-size: 14px;
|
|
3059
|
+
}
|
|
3060
|
+
.notification {
|
|
3061
|
+
position: fixed;
|
|
3062
|
+
bottom: 24px;
|
|
3063
|
+
right: 24px;
|
|
3064
|
+
padding: 12px 20px;
|
|
3065
|
+
background: var(--green);
|
|
3066
|
+
color: #000;
|
|
3067
|
+
border-radius: 8px;
|
|
3068
|
+
font-weight: 500;
|
|
3069
|
+
transform: translateY(100px);
|
|
3070
|
+
opacity: 0;
|
|
3071
|
+
transition: all 0.3s ease;
|
|
3072
|
+
z-index: 1000;
|
|
3073
|
+
}
|
|
3074
|
+
.notification.show { transform: translateY(0); opacity: 1; }
|
|
3075
|
+
.notification.error { background: var(--red); color: white; }
|
|
3076
|
+
|
|
3077
|
+
/* Claude Terminal */
|
|
3078
|
+
.claude-section .section-title {
|
|
3079
|
+
display: flex;
|
|
3080
|
+
align-items: center;
|
|
3081
|
+
justify-content: space-between;
|
|
3082
|
+
}
|
|
3083
|
+
.claude-status {
|
|
3084
|
+
font-size: 11px;
|
|
3085
|
+
padding: 3px 10px;
|
|
3086
|
+
border-radius: 12px;
|
|
3087
|
+
background: var(--bg-tertiary);
|
|
3088
|
+
color: var(--muted);
|
|
3089
|
+
}
|
|
3090
|
+
.claude-status.processing {
|
|
3091
|
+
color: var(--yellow);
|
|
3092
|
+
animation: pulse 1s infinite;
|
|
3093
|
+
}
|
|
3094
|
+
.claude-status.success { color: var(--green); }
|
|
3095
|
+
.claude-status.error { color: var(--red); }
|
|
3096
|
+
.claude-log {
|
|
3097
|
+
background: #0d1117;
|
|
3098
|
+
border: 1px solid var(--border);
|
|
3099
|
+
border-radius: 8px;
|
|
3100
|
+
padding: 16px;
|
|
3101
|
+
max-height: 400px;
|
|
3102
|
+
overflow-y: auto;
|
|
3103
|
+
font-family: 'SF Mono', Monaco, 'Consolas', monospace;
|
|
3104
|
+
font-size: 12px;
|
|
3105
|
+
line-height: 1.6;
|
|
3106
|
+
white-space: pre-wrap;
|
|
3107
|
+
word-break: break-word;
|
|
3108
|
+
color: var(--text);
|
|
3109
|
+
margin: 0;
|
|
3110
|
+
margin-top: 12px;
|
|
3111
|
+
}
|
|
3112
|
+
@keyframes pulse {
|
|
3113
|
+
0%, 100% { opacity: 1; }
|
|
3114
|
+
50% { opacity: 0.5; }
|
|
3115
|
+
}
|
|
3116
|
+
</style>
|
|
3117
|
+
</head>
|
|
3118
|
+
<body>
|
|
3119
|
+
<header class="header">
|
|
3120
|
+
<a href="/" class="back-btn">← Dashboard</a>
|
|
3121
|
+
<div class="ticket-header-info">
|
|
3122
|
+
<span class="ticket-key">${escapeHtml(ticketKey)}</span>
|
|
3123
|
+
<input type="text" class="ticket-title-input" id="ticket-title" value="${escapeHtml(ticket.title)}" />
|
|
3124
|
+
</div>
|
|
3125
|
+
<div class="lang-selector" id="lang-selector">
|
|
3126
|
+
<button class="lang-btn" data-lang="en">EN</button>
|
|
3127
|
+
<button class="lang-btn" data-lang="fr">FR</button>
|
|
3128
|
+
</div>
|
|
3129
|
+
</header>
|
|
3130
|
+
|
|
3131
|
+
<main class="main-content">
|
|
3132
|
+
<div class="ticket-details">
|
|
3133
|
+
<!-- Meta info -->
|
|
3134
|
+
<div class="section">
|
|
3135
|
+
<div class="section-title" data-i18n="ticketView.meta">Meta</div>
|
|
3136
|
+
<div class="ticket-meta">
|
|
3137
|
+
<span class="meta-badge priority-${ticket.priority}">${ticket.priority}</span>
|
|
3138
|
+
<span class="meta-badge column-badge">${escapeHtml(currentColumn?.name || ticket.column_slug)}</span>
|
|
3139
|
+
<span class="meta-badge semver-badge">${ticket.semver}</span>
|
|
3140
|
+
</div>
|
|
3141
|
+
</div>
|
|
3142
|
+
|
|
3143
|
+
<!-- Labels -->
|
|
3144
|
+
${ticket.labels && ticket.labels.length > 0 ? `
|
|
3145
|
+
<div class="section">
|
|
3146
|
+
<div class="section-title" data-i18n="ticketView.labels">Labels</div>
|
|
3147
|
+
<div class="labels-list">
|
|
3148
|
+
${ticket.labels.map(label => `<span class="label-tag">${escapeHtml(label)}</span>`).join('')}
|
|
3149
|
+
</div>
|
|
3150
|
+
</div>
|
|
3151
|
+
` : ''}
|
|
3152
|
+
|
|
3153
|
+
<!-- Description -->
|
|
3154
|
+
<div class="section">
|
|
3155
|
+
<div class="section-title">
|
|
3156
|
+
<span data-i18n="ticketView.description">Description</span>
|
|
3157
|
+
<button class="btn-edit-toggle" id="btn-edit-description" onclick="toggleDescriptionEdit()">✏️</button>
|
|
3158
|
+
</div>
|
|
3159
|
+
<div class="description-view" id="description-view"></div>
|
|
3160
|
+
<textarea class="description-edit" id="description-edit" style="display:none" placeholder="Description (Markdown)">${escapeHtml(ticket.description || '')}</textarea>
|
|
3161
|
+
</div>
|
|
3162
|
+
|
|
3163
|
+
<!-- Acceptance Criteria -->
|
|
3164
|
+
${ticket.acceptance_criteria && ticket.acceptance_criteria.length > 0 ? `
|
|
3165
|
+
<div class="section">
|
|
3166
|
+
<div class="section-title" data-i18n="ticketView.criteria">Acceptance Criteria</div>
|
|
3167
|
+
<ul class="criteria-list">
|
|
3168
|
+
${ticket.acceptance_criteria.map(c => `<li class="criteria-item">${escapeHtml(c)}</li>`).join('')}
|
|
3169
|
+
</ul>
|
|
3170
|
+
</div>
|
|
3171
|
+
` : ''}
|
|
3172
|
+
|
|
3173
|
+
<!-- History -->
|
|
3174
|
+
${ticket.history && ticket.history.length > 0 ? `
|
|
3175
|
+
<div class="section">
|
|
3176
|
+
<div class="section-title" data-i18n="ticketView.history">History</div>
|
|
3177
|
+
<ul class="history-list">
|
|
3178
|
+
${ticket.history.map(h => {
|
|
3179
|
+
const date = new Date(h.at);
|
|
3180
|
+
const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
3181
|
+
const showPromptBtn = h.to && h.action !== 'created' ? `<button class="btn-prompt" onclick="showPrompt('${escapeHtml(h.to)}')" title="View prompt">📜</button>` : '';
|
|
3182
|
+
const showLogBtn = h.to && h.action !== 'created' ? `<button class="btn-prompt" onclick="showLog('${escapeHtml(h.to)}')" title="View terminal log">🖥️</button>` : '';
|
|
3183
|
+
return `<li class="history-item">
|
|
3184
|
+
<span class="history-action">${h.action}</span>
|
|
3185
|
+
${h.from ? `<span class="history-from">${escapeHtml(h.from)}</span> →` : ''}
|
|
3186
|
+
<span class="history-to">${escapeHtml(h.to)}</span>
|
|
3187
|
+
${showPromptBtn}
|
|
3188
|
+
${showLogBtn}
|
|
3189
|
+
<span class="history-date">${dateStr}</span>
|
|
3190
|
+
</li>`;
|
|
3191
|
+
}).join('')}
|
|
3192
|
+
</ul>
|
|
3193
|
+
</div>
|
|
3194
|
+
` : ''}
|
|
3195
|
+
|
|
3196
|
+
<!-- Actions bar -->
|
|
3197
|
+
<div class="actions-bar">
|
|
3198
|
+
<button class="btn btn-primary" id="btn-save" onclick="saveTicket()" data-i18n="ticketView.save">Save</button>
|
|
3199
|
+
<button class="btn btn-secondary" onclick="advanceTicket()" data-i18n="ticketView.moveNext">Move to next column</button>
|
|
3200
|
+
<button class="btn btn-danger" onclick="archiveTicket()" data-i18n="ticketView.archive">Archive</button>
|
|
3201
|
+
</div>
|
|
3202
|
+
</div>
|
|
3203
|
+
|
|
3204
|
+
<!-- Bottom section: Comments & Claude Terminal -->
|
|
3205
|
+
<div class="ticket-bottom">
|
|
3206
|
+
<!-- Comments -->
|
|
3207
|
+
<div class="section comments-section">
|
|
3208
|
+
<div class="section-title"><span data-i18n="ticketView.comments">Comments</span> (<span id="comments-count">${ticket.comments?.length || 0}</span>)</div>
|
|
3209
|
+
<div class="comments-list" id="comments-list"></div>
|
|
3210
|
+
<div class="add-comment">
|
|
3211
|
+
<textarea id="new-comment" placeholder="Add a comment..." data-i18n-placeholder="ticketView.addComment"></textarea>
|
|
3212
|
+
<button class="btn-comment" onclick="addComment()" data-i18n="btn.add">Add</button>
|
|
3213
|
+
</div>
|
|
3214
|
+
</div>
|
|
3215
|
+
|
|
3216
|
+
<!-- Claude Terminal -->
|
|
3217
|
+
<div class="section claude-section" id="claude-section">
|
|
3218
|
+
<div class="section-title">
|
|
3219
|
+
<span data-i18n="ticketView.claudeTerminal">Claude Terminal</span>
|
|
3220
|
+
<span class="claude-status" id="claude-status" data-i18n="status.waiting">Waiting</span>
|
|
3221
|
+
</div>
|
|
3222
|
+
<pre class="claude-log" id="claude-log"></pre>
|
|
3223
|
+
</div>
|
|
3224
|
+
</div>
|
|
3225
|
+
</main>
|
|
3226
|
+
|
|
3227
|
+
<div class="notification" id="notification"></div>
|
|
3228
|
+
|
|
3229
|
+
<!-- Prompt Modal -->
|
|
3230
|
+
<div class="prompt-modal" id="prompt-modal" onclick="closePromptModal(event)">
|
|
3231
|
+
<div class="prompt-modal-content" onclick="event.stopPropagation()">
|
|
3232
|
+
<div class="prompt-modal-header">
|
|
3233
|
+
<h3 id="prompt-modal-title">Prompt</h3>
|
|
3234
|
+
<button class="prompt-modal-close" onclick="closePromptModal()">×</button>
|
|
3235
|
+
</div>
|
|
3236
|
+
<div class="prompt-modal-body">
|
|
3237
|
+
<pre id="prompt-modal-content"></pre>
|
|
3238
|
+
</div>
|
|
3239
|
+
</div>
|
|
3240
|
+
</div>
|
|
3241
|
+
|
|
3242
|
+
<script>
|
|
3243
|
+
const TICKET_KEY = '${ticketKey}';
|
|
3244
|
+
const TICKET = ${ticketData};
|
|
3245
|
+
const COLUMNS = ${columnsData};
|
|
3246
|
+
const STORAGE_KEY = 'autocode-lang';
|
|
3247
|
+
|
|
3248
|
+
let currentLang = localStorage.getItem(STORAGE_KEY) || 'fr';
|
|
3249
|
+
let currentComments = TICKET.comments || [];
|
|
3250
|
+
|
|
3251
|
+
const translations = {
|
|
3252
|
+
en: {
|
|
3253
|
+
'ticketView.meta': 'Meta',
|
|
3254
|
+
'ticketView.labels': 'Labels',
|
|
3255
|
+
'ticketView.description': 'Description',
|
|
3256
|
+
'ticketView.criteria': 'Acceptance Criteria',
|
|
3257
|
+
'ticketView.history': 'History',
|
|
3258
|
+
'ticketView.actions': 'Actions',
|
|
3259
|
+
'ticketView.save': 'Save',
|
|
3260
|
+
'ticketView.moveNext': 'Move to next column',
|
|
3261
|
+
'ticketView.archive': 'Archive',
|
|
3262
|
+
'ticketView.confirmMove': 'Move this ticket to the next column?',
|
|
3263
|
+
'ticketView.confirmArchive': 'Archive this ticket?',
|
|
3264
|
+
'ticketView.comments': 'Comments',
|
|
3265
|
+
'ticketView.addComment': 'Add a comment...',
|
|
3266
|
+
'ticketView.noComments': 'No comments yet',
|
|
3267
|
+
'ticketView.claudeTerminal': 'Claude Terminal',
|
|
3268
|
+
'ticketView.noDescription': 'No description',
|
|
3269
|
+
'ticketView.noLog': 'No log yet. Waiting for Claude processing...',
|
|
3270
|
+
'ticketView.loadingPrompt': 'Loading prompt...',
|
|
3271
|
+
'ticketView.promptError': 'Error',
|
|
3272
|
+
'btn.add': 'Add',
|
|
3273
|
+
'btn.sending': 'Sending...',
|
|
3274
|
+
'btn.saving': 'Saving...',
|
|
3275
|
+
'status.waiting': 'Waiting',
|
|
3276
|
+
'status.processing': 'Processing...',
|
|
3277
|
+
'status.completed': 'Completed',
|
|
3278
|
+
'status.failed': 'Failed',
|
|
3279
|
+
'notify.commentAdded': 'Comment added',
|
|
3280
|
+
'notify.ticketAdvanced': 'Ticket advanced',
|
|
3281
|
+
'notify.ticketArchived': 'Ticket archived',
|
|
3282
|
+
'notify.ticketSaved': 'Ticket saved',
|
|
3283
|
+
'notify.error': 'Error'
|
|
3284
|
+
},
|
|
3285
|
+
fr: {
|
|
3286
|
+
'ticketView.meta': 'Méta',
|
|
3287
|
+
'ticketView.labels': 'Labels',
|
|
3288
|
+
'ticketView.description': 'Description',
|
|
3289
|
+
'ticketView.criteria': 'Critères d\\'acceptation',
|
|
3290
|
+
'ticketView.history': 'Historique',
|
|
3291
|
+
'ticketView.actions': 'Actions',
|
|
3292
|
+
'ticketView.save': 'Sauvegarder',
|
|
3293
|
+
'ticketView.moveNext': 'Déplacer vers la colonne suivante',
|
|
3294
|
+
'ticketView.archive': 'Archiver',
|
|
3295
|
+
'ticketView.confirmMove': 'Déplacer ce ticket vers la colonne suivante ?',
|
|
3296
|
+
'ticketView.confirmArchive': 'Archiver ce ticket ?',
|
|
3297
|
+
'ticketView.comments': 'Commentaires',
|
|
3298
|
+
'ticketView.addComment': 'Ajouter un commentaire...',
|
|
3299
|
+
'ticketView.noComments': 'Aucun commentaire',
|
|
3300
|
+
'ticketView.claudeTerminal': 'Terminal Claude',
|
|
3301
|
+
'ticketView.noDescription': 'Aucune description',
|
|
3302
|
+
'ticketView.noLog': 'Aucun log. En attente du traitement Claude...',
|
|
3303
|
+
'ticketView.loadingPrompt': 'Chargement du prompt...',
|
|
3304
|
+
'ticketView.promptError': 'Erreur',
|
|
3305
|
+
'btn.add': 'Ajouter',
|
|
3306
|
+
'btn.sending': 'Envoi...',
|
|
3307
|
+
'btn.saving': 'Sauvegarde...',
|
|
3308
|
+
'status.waiting': 'En attente',
|
|
3309
|
+
'status.processing': 'En cours...',
|
|
3310
|
+
'status.completed': 'Terminé',
|
|
3311
|
+
'status.failed': 'Échec',
|
|
3312
|
+
'notify.commentAdded': 'Commentaire ajouté',
|
|
3313
|
+
'notify.ticketAdvanced': 'Ticket avancé',
|
|
3314
|
+
'notify.ticketArchived': 'Ticket archivé',
|
|
3315
|
+
'notify.ticketSaved': 'Ticket sauvegardé',
|
|
3316
|
+
'notify.error': 'Erreur'
|
|
3317
|
+
}
|
|
3318
|
+
};
|
|
3319
|
+
|
|
3320
|
+
function t(key) {
|
|
3321
|
+
return translations[currentLang]?.[key] || translations['en'][key] || key;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
function escapeHtml(text) {
|
|
3325
|
+
if (!text) return '';
|
|
3326
|
+
const div = document.createElement('div');
|
|
3327
|
+
div.textContent = text;
|
|
3328
|
+
return div.innerHTML;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
function renderMarkdown(text) {
|
|
3332
|
+
if (!text) return '';
|
|
3333
|
+
let html = escapeHtml(text);
|
|
3334
|
+
html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
|
|
3335
|
+
html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
|
|
3336
|
+
html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>');
|
|
3337
|
+
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
|
3338
|
+
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
|
3339
|
+
html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
|
3340
|
+
html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
3341
|
+
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
|
|
3342
|
+
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
|
|
3343
|
+
html = html.replace(/\\n/g, '<br>');
|
|
3344
|
+
return html;
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
function updateLangUI() {
|
|
3348
|
+
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
3349
|
+
btn.classList.toggle('active', btn.dataset.lang === currentLang);
|
|
3350
|
+
});
|
|
3351
|
+
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
3352
|
+
el.textContent = t(el.dataset.i18n);
|
|
3353
|
+
});
|
|
3354
|
+
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
|
3355
|
+
el.placeholder = t(el.dataset.i18nPlaceholder);
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
function renderComments() {
|
|
3360
|
+
const list = document.getElementById('comments-list');
|
|
3361
|
+
const count = document.getElementById('comments-count');
|
|
3362
|
+
count.textContent = currentComments.length;
|
|
3363
|
+
|
|
3364
|
+
if (currentComments.length === 0) {
|
|
3365
|
+
list.innerHTML = '<div class="no-comments">' + t('ticketView.noComments') + '</div>';
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
const sorted = [...currentComments].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
3370
|
+
list.innerHTML = sorted.map((comment, index) => {
|
|
3371
|
+
const date = new Date(comment.created_at);
|
|
3372
|
+
const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
3373
|
+
const source = comment.source || 'user';
|
|
3374
|
+
const sourceBadge = source === 'claude'
|
|
3375
|
+
? '<span class="comment-source claude">Claude</span>'
|
|
3376
|
+
: '<span class="comment-source user">User</span>';
|
|
3377
|
+
return '<div class="comment" id="comment-' + index + '">' +
|
|
3378
|
+
'<div class="comment-meta" onclick="toggleComment(' + index + ')">' + sourceBadge + '<span class="comment-column">' + (comment.column || 'N/A') + '</span>' +
|
|
3379
|
+
'<span class="comment-date">' + dateStr + '</span></div>' +
|
|
3380
|
+
'<div class="comment-text">' + renderMarkdown(comment.text) + '</div></div>';
|
|
3381
|
+
}).join('');
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
function toggleComment(index) {
|
|
3385
|
+
const comments = document.querySelectorAll('.comment');
|
|
3386
|
+
comments.forEach((comment, i) => {
|
|
3387
|
+
if (i === index) {
|
|
3388
|
+
comment.classList.toggle('expanded');
|
|
3389
|
+
} else {
|
|
3390
|
+
comment.classList.remove('expanded');
|
|
3391
|
+
}
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
async function addComment() {
|
|
3396
|
+
const textarea = document.getElementById('new-comment');
|
|
3397
|
+
const text = textarea.value.trim();
|
|
3398
|
+
if (!text) return;
|
|
3399
|
+
|
|
3400
|
+
const btn = document.querySelector('.btn-comment');
|
|
3401
|
+
btn.disabled = true;
|
|
3402
|
+
btn.textContent = t('btn.sending');
|
|
3403
|
+
|
|
3404
|
+
try {
|
|
3405
|
+
const res = await fetch('/api/tickets/' + TICKET_KEY + '/comments', {
|
|
3406
|
+
method: 'POST',
|
|
3407
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3408
|
+
body: JSON.stringify({ text, source: 'user' })
|
|
3409
|
+
});
|
|
3410
|
+
const result = await res.json();
|
|
3411
|
+
textarea.value = '';
|
|
3412
|
+
if (result.success && result.data && result.data.comments) {
|
|
3413
|
+
currentComments = result.data.comments;
|
|
3414
|
+
renderComments();
|
|
3415
|
+
}
|
|
3416
|
+
showNotification(t('notify.commentAdded'));
|
|
3417
|
+
} catch (e) {
|
|
3418
|
+
showNotification(t('notify.error') + ': ' + e.message, true);
|
|
3419
|
+
} finally {
|
|
3420
|
+
btn.disabled = false;
|
|
3421
|
+
btn.textContent = t('btn.add');
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
let isEditingDescription = false;
|
|
3426
|
+
|
|
3427
|
+
function renderDescription() {
|
|
3428
|
+
const view = document.getElementById('description-view');
|
|
3429
|
+
const edit = document.getElementById('description-edit');
|
|
3430
|
+
const description = edit.value || '';
|
|
3431
|
+
if (description) {
|
|
3432
|
+
view.innerHTML = renderMarkdown(description);
|
|
3433
|
+
} else {
|
|
3434
|
+
view.innerHTML = '<span style="color: var(--muted)">' + t('ticketView.noDescription') + '</span>';
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
function toggleDescriptionEdit() {
|
|
3439
|
+
isEditingDescription = !isEditingDescription;
|
|
3440
|
+
const view = document.getElementById('description-view');
|
|
3441
|
+
const edit = document.getElementById('description-edit');
|
|
3442
|
+
const btn = document.getElementById('btn-edit-description');
|
|
3443
|
+
|
|
3444
|
+
if (isEditingDescription) {
|
|
3445
|
+
view.style.display = 'none';
|
|
3446
|
+
edit.style.display = 'block';
|
|
3447
|
+
btn.classList.add('active');
|
|
3448
|
+
btn.textContent = '✓';
|
|
3449
|
+
edit.focus();
|
|
3450
|
+
} else {
|
|
3451
|
+
view.style.display = 'block';
|
|
3452
|
+
edit.style.display = 'none';
|
|
3453
|
+
btn.classList.remove('active');
|
|
3454
|
+
btn.textContent = '✏️';
|
|
3455
|
+
renderDescription();
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
async function saveTicket() {
|
|
3460
|
+
const btn = document.getElementById('btn-save');
|
|
3461
|
+
btn.disabled = true;
|
|
3462
|
+
btn.textContent = t('btn.saving');
|
|
3463
|
+
|
|
3464
|
+
try {
|
|
3465
|
+
const res = await fetch('/api/tickets/' + TICKET_KEY, {
|
|
3466
|
+
method: 'PATCH',
|
|
3467
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3468
|
+
body: JSON.stringify({
|
|
3469
|
+
title: document.getElementById('ticket-title').value,
|
|
3470
|
+
description: document.getElementById('description-edit').value
|
|
3471
|
+
})
|
|
3472
|
+
});
|
|
3473
|
+
const result = await res.json();
|
|
3474
|
+
if (result.success) {
|
|
3475
|
+
showNotification(t('notify.ticketSaved'));
|
|
3476
|
+
if (isEditingDescription) {
|
|
3477
|
+
toggleDescriptionEdit();
|
|
3478
|
+
}
|
|
3479
|
+
} else {
|
|
3480
|
+
showNotification(t('notify.error') + ': ' + result.error, true);
|
|
3481
|
+
}
|
|
3482
|
+
} catch (e) {
|
|
3483
|
+
showNotification(t('notify.error') + ': ' + e.message, true);
|
|
3484
|
+
} finally {
|
|
3485
|
+
btn.disabled = false;
|
|
3486
|
+
btn.textContent = t('ticketView.save');
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
async function advanceTicket() {
|
|
3491
|
+
if (!confirm(t('ticketView.confirmMove'))) return;
|
|
3492
|
+
try {
|
|
3493
|
+
const res = await fetch('/api/tickets/' + TICKET_KEY + '/next', {
|
|
3494
|
+
method: 'POST',
|
|
3495
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3496
|
+
body: JSON.stringify({ lang: currentLang })
|
|
3497
|
+
});
|
|
3498
|
+
const result = await res.json();
|
|
3499
|
+
if (result.success) {
|
|
3500
|
+
showNotification(t('notify.ticketAdvanced'));
|
|
3501
|
+
setTimeout(() => location.reload(), 1000);
|
|
3502
|
+
} else {
|
|
3503
|
+
showNotification(t('notify.error') + ': ' + result.error, true);
|
|
3504
|
+
}
|
|
3505
|
+
} catch (e) {
|
|
3506
|
+
showNotification(t('notify.error') + ': ' + e.message, true);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
async function archiveTicket() {
|
|
3511
|
+
if (!confirm(t('ticketView.confirmArchive'))) return;
|
|
3512
|
+
try {
|
|
3513
|
+
const res = await fetch('/api/tickets/' + TICKET_KEY + '/archive', {
|
|
3514
|
+
method: 'POST',
|
|
3515
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3516
|
+
body: JSON.stringify({ lang: currentLang })
|
|
3517
|
+
});
|
|
3518
|
+
const result = await res.json();
|
|
3519
|
+
if (result.success) {
|
|
3520
|
+
showNotification(t('notify.ticketArchived'));
|
|
3521
|
+
setTimeout(() => location.href = '/', 1000);
|
|
3522
|
+
} else {
|
|
3523
|
+
showNotification(t('notify.error') + ': ' + result.error, true);
|
|
3524
|
+
}
|
|
3525
|
+
} catch (e) {
|
|
3526
|
+
showNotification(t('notify.error') + ': ' + e.message, true);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
function showNotification(msg, isError) {
|
|
3531
|
+
const notification = document.getElementById('notification');
|
|
3532
|
+
notification.textContent = msg;
|
|
3533
|
+
notification.className = 'notification show' + (isError ? ' error' : '');
|
|
3534
|
+
setTimeout(() => notification.className = 'notification', 3000);
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
// Language switcher
|
|
3538
|
+
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
3539
|
+
btn.addEventListener('click', () => {
|
|
3540
|
+
const newLang = btn.dataset.lang;
|
|
3541
|
+
if (newLang !== currentLang) {
|
|
3542
|
+
currentLang = newLang;
|
|
3543
|
+
localStorage.setItem(STORAGE_KEY, newLang);
|
|
3544
|
+
updateLangUI();
|
|
3545
|
+
renderComments();
|
|
3546
|
+
}
|
|
3547
|
+
});
|
|
3548
|
+
});
|
|
3549
|
+
|
|
3550
|
+
// ========================================
|
|
3551
|
+
// WEBSOCKET & CLAUDE LOG
|
|
3552
|
+
// ========================================
|
|
3553
|
+
let ws;
|
|
3554
|
+
let logPollingInterval = null;
|
|
3555
|
+
|
|
3556
|
+
function connectWebSocket() {
|
|
3557
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
3558
|
+
ws = new WebSocket(protocol + '//' + location.host + '/ws');
|
|
3559
|
+
|
|
3560
|
+
ws.onmessage = (event) => {
|
|
3561
|
+
try {
|
|
3562
|
+
const data = JSON.parse(event.data);
|
|
3563
|
+
switch (data.type) {
|
|
3564
|
+
case 'ticket_updated':
|
|
3565
|
+
if (data.key === TICKET_KEY) {
|
|
3566
|
+
location.reload();
|
|
3567
|
+
}
|
|
3568
|
+
break;
|
|
3569
|
+
case 'claude_start':
|
|
3570
|
+
if (data.ticket === TICKET_KEY) {
|
|
3571
|
+
onClaudeStart();
|
|
3572
|
+
}
|
|
3573
|
+
break;
|
|
3574
|
+
case 'claude_stream':
|
|
3575
|
+
if (data.ticket === TICKET_KEY) {
|
|
3576
|
+
fetchLog();
|
|
3577
|
+
}
|
|
3578
|
+
break;
|
|
3579
|
+
case 'claude_end':
|
|
3580
|
+
if (data.ticket === TICKET_KEY) {
|
|
3581
|
+
onClaudeEnd(data.success, data.duration);
|
|
3582
|
+
}
|
|
3583
|
+
break;
|
|
3584
|
+
}
|
|
3585
|
+
} catch (e) {
|
|
3586
|
+
console.error('WebSocket message error:', e);
|
|
3587
|
+
}
|
|
3588
|
+
};
|
|
3589
|
+
|
|
3590
|
+
ws.onclose = () => {
|
|
3591
|
+
setTimeout(connectWebSocket, 3000);
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
function startLogPolling() {
|
|
3596
|
+
stopLogPolling();
|
|
3597
|
+
logPollingInterval = setInterval(fetchLog, 1000);
|
|
3598
|
+
fetchLog();
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
function stopLogPolling() {
|
|
3602
|
+
if (logPollingInterval) {
|
|
3603
|
+
clearInterval(logPollingInterval);
|
|
3604
|
+
logPollingInterval = null;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
async function fetchLog() {
|
|
3609
|
+
try {
|
|
3610
|
+
const res = await fetch('/api/tickets/' + TICKET_KEY + '/log');
|
|
3611
|
+
const json = await res.json();
|
|
3612
|
+
const log = document.getElementById('claude-log');
|
|
3613
|
+
|
|
3614
|
+
if (json.success && json.data && (json.data.exists || json.data.content)) {
|
|
3615
|
+
log.textContent = json.data.content || '';
|
|
3616
|
+
log.scrollTop = log.scrollHeight;
|
|
3617
|
+
} else {
|
|
3618
|
+
log.textContent = t('ticketView.noLog');
|
|
3619
|
+
}
|
|
3620
|
+
} catch (e) {
|
|
3621
|
+
console.error('Log fetch error:', e);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
function onClaudeStart() {
|
|
3626
|
+
const status = document.getElementById('claude-status');
|
|
3627
|
+
status.className = 'claude-status processing';
|
|
3628
|
+
status.textContent = t('status.processing');
|
|
3629
|
+
startLogPolling();
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
function onClaudeEnd(success, duration) {
|
|
3633
|
+
stopLogPolling();
|
|
3634
|
+
fetchLog();
|
|
3635
|
+
const status = document.getElementById('claude-status');
|
|
3636
|
+
if (success) {
|
|
3637
|
+
status.className = 'claude-status success';
|
|
3638
|
+
status.textContent = t('status.completed') + ' (' + (duration / 1000).toFixed(1) + 's)';
|
|
3639
|
+
} else {
|
|
3640
|
+
status.className = 'claude-status error';
|
|
3641
|
+
status.textContent = t('status.failed');
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
// Prompt Modal
|
|
3646
|
+
async function showPrompt(columnSlug) {
|
|
3647
|
+
const modal = document.getElementById('prompt-modal');
|
|
3648
|
+
const title = document.getElementById('prompt-modal-title');
|
|
3649
|
+
const content = document.getElementById('prompt-modal-content');
|
|
3650
|
+
|
|
3651
|
+
title.textContent = t('ticketView.loadingPrompt');
|
|
3652
|
+
content.textContent = '';
|
|
3653
|
+
modal.classList.add('visible');
|
|
3654
|
+
|
|
3655
|
+
try {
|
|
3656
|
+
const res = await fetch('/api/tickets/' + TICKET_KEY + '/prompt/' + columnSlug);
|
|
3657
|
+
const json = await res.json();
|
|
3658
|
+
if (json.success) {
|
|
3659
|
+
title.textContent = 'Prompt → ' + json.data.column;
|
|
3660
|
+
content.textContent = json.data.prompt;
|
|
3661
|
+
} else {
|
|
3662
|
+
title.textContent = t('ticketView.promptError');
|
|
3663
|
+
content.textContent = json.error || 'Error loading prompt';
|
|
3664
|
+
}
|
|
3665
|
+
} catch (e) {
|
|
3666
|
+
title.textContent = t('ticketView.promptError');
|
|
3667
|
+
content.textContent = e.message;
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
function closePromptModal(event) {
|
|
3672
|
+
if (event && event.target !== event.currentTarget) return;
|
|
3673
|
+
document.getElementById('prompt-modal').classList.remove('visible');
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
// Log Modal (reuses prompt modal)
|
|
3677
|
+
async function showLog(columnSlug) {
|
|
3678
|
+
const modal = document.getElementById('prompt-modal');
|
|
3679
|
+
const title = document.getElementById('prompt-modal-title');
|
|
3680
|
+
const content = document.getElementById('prompt-modal-content');
|
|
3681
|
+
|
|
3682
|
+
title.textContent = t('ticketView.loadingLog') || 'Loading log...';
|
|
3683
|
+
content.textContent = '';
|
|
3684
|
+
modal.classList.add('visible');
|
|
3685
|
+
|
|
3686
|
+
try {
|
|
3687
|
+
const res = await fetch('/api/tickets/' + TICKET_KEY + '/log/' + columnSlug);
|
|
3688
|
+
const json = await res.json();
|
|
3689
|
+
if (json.success) {
|
|
3690
|
+
title.textContent = 'Terminal → ' + columnSlug;
|
|
3691
|
+
content.textContent = json.data.content || t('ticketView.noLog') || 'No log available';
|
|
3692
|
+
} else {
|
|
3693
|
+
title.textContent = t('ticketView.logError') || 'Log Error';
|
|
3694
|
+
content.textContent = json.error || 'Error loading log';
|
|
3695
|
+
}
|
|
3696
|
+
} catch (e) {
|
|
3697
|
+
title.textContent = t('ticketView.logError') || 'Log Error';
|
|
3698
|
+
content.textContent = e.message;
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
// Init
|
|
3703
|
+
updateLangUI();
|
|
3704
|
+
renderDescription();
|
|
3705
|
+
renderComments();
|
|
3706
|
+
connectWebSocket();
|
|
3707
|
+
fetchLog();
|
|
3708
|
+
</script>
|
|
3709
|
+
</body>
|
|
3710
|
+
</html>`;
|
|
3711
|
+
}
|
|
2261
3712
|
//# sourceMappingURL=dashboard.js.map
|