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