@autocode-cli/autocode 0.0.22
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/README.md +172 -0
- package/bin/autocode +2 -0
- package/dist/cli/commands/comment.d.ts +9 -0
- package/dist/cli/commands/comment.d.ts.map +1 -0
- package/dist/cli/commands/comment.js +37 -0
- package/dist/cli/commands/comment.js.map +1 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +41 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +9 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +78 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/move.d.ts +9 -0
- package/dist/cli/commands/move.d.ts.map +1 -0
- package/dist/cli/commands/move.js +59 -0
- package/dist/cli/commands/move.js.map +1 -0
- package/dist/cli/commands/new.d.ts +9 -0
- package/dist/cli/commands/new.d.ts.map +1 -0
- package/dist/cli/commands/new.js +74 -0
- package/dist/cli/commands/new.js.map +1 -0
- package/dist/cli/commands/next.d.ts +9 -0
- package/dist/cli/commands/next.d.ts.map +1 -0
- package/dist/cli/commands/next.js +46 -0
- package/dist/cli/commands/next.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +9 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +55 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/show.d.ts +9 -0
- package/dist/cli/commands/show.d.ts.map +1 -0
- package/dist/cli/commands/show.js +91 -0
- package/dist/cli/commands/show.js.map +1 -0
- package/dist/cli/parser.d.ts +13 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +54 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/core/column.d.ts +53 -0
- package/dist/core/column.d.ts.map +1 -0
- package/dist/core/column.js +128 -0
- package/dist/core/column.js.map +1 -0
- package/dist/core/ticket.d.ts +50 -0
- package/dist/core/ticket.d.ts.map +1 -0
- package/dist/core/ticket.js +228 -0
- package/dist/core/ticket.js.map +1 -0
- package/dist/core/workflow.d.ts +66 -0
- package/dist/core/workflow.d.ts.map +1 -0
- package/dist/core/workflow.js +176 -0
- package/dist/core/workflow.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/server/api.d.ts +9 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +313 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/dashboard.d.ts +8 -0
- package/dist/server/dashboard.d.ts.map +1 -0
- package/dist/server/dashboard.js +1865 -0
- package/dist/server/dashboard.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +94 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/watcher.d.ts +13 -0
- package/dist/server/watcher.d.ts.map +1 -0
- package/dist/server/watcher.js +62 -0
- package/dist/server/watcher.js.map +1 -0
- package/dist/server/websocket.d.ts +26 -0
- package/dist/server/websocket.d.ts.map +1 -0
- package/dist/server/websocket.js +165 -0
- package/dist/server/websocket.js.map +1 -0
- package/dist/services/claude.d.ts +52 -0
- package/dist/services/claude.d.ts.map +1 -0
- package/dist/services/claude.js +304 -0
- package/dist/services/claude.js.map +1 -0
- package/dist/services/ticket-io.d.ts +76 -0
- package/dist/services/ticket-io.d.ts.map +1 -0
- package/dist/services/ticket-io.js +248 -0
- package/dist/services/ticket-io.js.map +1 -0
- package/dist/types/index.d.ts +79 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/config.d.ts +93 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +96 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/fs.d.ts +60 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +129 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/logger.d.ts +23 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +56 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +57 -0
- package/templates/columns/00_backlog.en.md +31 -0
- package/templates/columns/00_backlog.fr.md +31 -0
- package/templates/columns/01_ready.en.md +30 -0
- package/templates/columns/01_ready.fr.md +30 -0
- package/templates/columns/02_in-progress.en.md +30 -0
- package/templates/columns/02_in-progress.fr.md +30 -0
- package/templates/columns/03_review-best-practices.en.md +31 -0
- package/templates/columns/03_review-best-practices.fr.md +31 -0
- package/templates/columns/04_review-no-duplication.en.md +30 -0
- package/templates/columns/04_review-no-duplication.fr.md +30 -0
- package/templates/columns/05_review-consistency.en.md +31 -0
- package/templates/columns/05_review-consistency.fr.md +31 -0
- package/templates/columns/06_review-security.en.md +32 -0
- package/templates/columns/06_review-security.fr.md +32 -0
- package/templates/columns/07_testing-playwright.en.md +30 -0
- package/templates/columns/07_testing-playwright.fr.md +30 -0
- package/templates/columns/08_testing-cypress.en.md +31 -0
- package/templates/columns/08_testing-cypress.fr.md +31 -0
- package/templates/columns/09_update-docs.en.md +29 -0
- package/templates/columns/09_update-docs.fr.md +29 -0
- package/templates/columns/10_deploy-staging.en.md +29 -0
- package/templates/columns/10_deploy-staging.fr.md +29 -0
- package/templates/columns/11_validate-staging.en.md +32 -0
- package/templates/columns/11_validate-staging.fr.md +32 -0
- package/templates/columns/12_done.en.md +23 -0
- package/templates/columns/12_done.fr.md +23 -0
|
@@ -0,0 +1,1865 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard HTML generation - Full feature parity with legacy autocode.sh
|
|
3
|
+
*/
|
|
4
|
+
import { getColumns } from '../core/column.js';
|
|
5
|
+
import { listTickets } from '../core/ticket.js';
|
|
6
|
+
import { getWorkflowSummary } from '../core/workflow.js';
|
|
7
|
+
import { getConfig } from '../utils/config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generate the full dashboard HTML
|
|
10
|
+
*/
|
|
11
|
+
export function generateDashboard() {
|
|
12
|
+
const config = getConfig();
|
|
13
|
+
const tickets = listTickets(config.root);
|
|
14
|
+
const columns = getColumns();
|
|
15
|
+
const summary = getWorkflowSummary(tickets);
|
|
16
|
+
// Prepare data for client-side rendering
|
|
17
|
+
const columnsJson = JSON.stringify(columns);
|
|
18
|
+
const ticketsJson = JSON.stringify(tickets);
|
|
19
|
+
const timestamp = new Date().toLocaleString('en-US');
|
|
20
|
+
return `<!DOCTYPE html>
|
|
21
|
+
<html lang="en">
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="UTF-8">
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
25
|
+
<title>AutoCode Dashboard</title>
|
|
26
|
+
<style>
|
|
27
|
+
${getStyles()}
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<div id="notifications" class="notifications"></div>
|
|
32
|
+
|
|
33
|
+
<header>
|
|
34
|
+
<div class="header-logo">
|
|
35
|
+
<a href="#" class="logo-link"><span>⚡</span> AutoCode</a>
|
|
36
|
+
<span class="header-title">Dashboard</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="stats" id="stats"></div>
|
|
39
|
+
<div class="filters">
|
|
40
|
+
<select id="filter-priority">
|
|
41
|
+
<option value="" data-i18n="filter.allPriorities">All priorities</option>
|
|
42
|
+
<option value="P0">P0</option>
|
|
43
|
+
<option value="P1">P1</option>
|
|
44
|
+
<option value="P2">P2</option>
|
|
45
|
+
<option value="P3">P3</option>
|
|
46
|
+
</select>
|
|
47
|
+
<input type="text" id="filter-search" placeholder="Search..." data-i18n-placeholder="filter.search">
|
|
48
|
+
<select id="ui-lang" onchange="switchLanguage(this.value)" title="Interface language">
|
|
49
|
+
<option value="en">EN</option>
|
|
50
|
+
<option value="fr">FR</option>
|
|
51
|
+
</select>
|
|
52
|
+
<button class="btn btn-primary" onclick="openModal()" data-i18n="btn.newTicket">+ New ticket</button>
|
|
53
|
+
</div>
|
|
54
|
+
</header>
|
|
55
|
+
|
|
56
|
+
<main class="board" id="board"></main>
|
|
57
|
+
|
|
58
|
+
<!-- Modal Ticket (create/edit) -->
|
|
59
|
+
<div class="modal-overlay" id="modal-overlay">
|
|
60
|
+
<div class="modal">
|
|
61
|
+
<div class="modal-header">
|
|
62
|
+
<h2 id="modal-title" data-i18n="modal.newTicket">New ticket</h2>
|
|
63
|
+
<button class="modal-close" onclick="closeModal()">×</button>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="modal-body">
|
|
66
|
+
<div class="form-group">
|
|
67
|
+
<label for="ticket-title" data-i18n="modal.title">Title *</label>
|
|
68
|
+
<input type="text" id="ticket-title" placeholder="E.g.: Fix the login bug" data-i18n-placeholder="modal.titlePlaceholder">
|
|
69
|
+
</div>
|
|
70
|
+
<div class="form-group">
|
|
71
|
+
<label for="ticket-description" data-i18n="modal.description">Description</label>
|
|
72
|
+
<textarea id="ticket-description" placeholder="Describe the context and details..." data-i18n-placeholder="modal.descriptionPlaceholder"></textarea>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="form-row">
|
|
75
|
+
<div class="form-group">
|
|
76
|
+
<label for="ticket-priority" data-i18n="modal.priority">Priority</label>
|
|
77
|
+
<select id="ticket-priority">
|
|
78
|
+
<option value="P2" data-i18n="priority.p2">P2 - Normal</option>
|
|
79
|
+
<option value="P0" data-i18n="priority.p0">P0 - Critical</option>
|
|
80
|
+
<option value="P1" data-i18n="priority.p1">P1 - High</option>
|
|
81
|
+
<option value="P3" data-i18n="priority.p3">P3 - Low</option>
|
|
82
|
+
</select>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="form-group">
|
|
85
|
+
<label for="ticket-semver" data-i18n="modal.releaseType">Release type</label>
|
|
86
|
+
<select id="ticket-semver">
|
|
87
|
+
<option value="patch" data-i18n="semver.patch">Patch (x.x.X) - Bug fix</option>
|
|
88
|
+
<option value="minor" data-i18n="semver.minor">Minor (x.X.0) - Feature</option>
|
|
89
|
+
<option value="major" data-i18n="semver.major">Major (X.0.0) - Breaking</option>
|
|
90
|
+
<option value="none" data-i18n="semver.none">No deployment</option>
|
|
91
|
+
</select>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="form-group labels-select">
|
|
95
|
+
<label for="ticket-labels" data-i18n="modal.labels">Labels</label>
|
|
96
|
+
<select id="ticket-labels" onchange="addLabel(this)">
|
|
97
|
+
<option value="" data-i18n="modal.selectLabel">Select a label...</option>
|
|
98
|
+
<option value="bug">bug</option>
|
|
99
|
+
<option value="feature">feature</option>
|
|
100
|
+
<option value="enhancement">enhancement</option>
|
|
101
|
+
<option value="documentation">documentation</option>
|
|
102
|
+
<option value="urgent">urgent</option>
|
|
103
|
+
<option value="refactoring">refactoring</option>
|
|
104
|
+
<option value="security">security</option>
|
|
105
|
+
<option value="performance">performance</option>
|
|
106
|
+
<option value="ui">ui</option>
|
|
107
|
+
<option value="api">api</option>
|
|
108
|
+
</select>
|
|
109
|
+
<div class="selected-labels" id="selected-labels"></div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="form-group">
|
|
112
|
+
<label data-i18n="modal.acceptanceCriteria">Acceptance criteria</label>
|
|
113
|
+
<div class="criteria-list" id="criteria-list"></div>
|
|
114
|
+
<button type="button" class="btn-add" onclick="addCriteria()">
|
|
115
|
+
<span>+</span> <span data-i18n="modal.addCriteria">Add criteria</span>
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="comments-section" id="comments-section" style="display:none">
|
|
119
|
+
<div class="comments-header">
|
|
120
|
+
<h3 data-i18n="modal.comments">Comments</h3>
|
|
121
|
+
<span class="comments-count" id="comments-count">0</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="comments-list" id="comments-list">
|
|
124
|
+
<div class="no-comments" data-i18n="modal.noComments">No comments</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="add-comment">
|
|
127
|
+
<textarea id="new-comment" placeholder="Add a comment..." data-i18n-placeholder="modal.addCommentPlaceholder"></textarea>
|
|
128
|
+
<button class="btn-comment" onclick="addComment()" data-i18n="btn.add">Add</button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="claude-log-section" id="claude-log-section" style="display:none">
|
|
132
|
+
<div class="claude-log-header">
|
|
133
|
+
<h3 data-i18n="modal.claudeTerminal">Claude Terminal</h3>
|
|
134
|
+
<span class="claude-log-status" id="claude-log-status" data-i18n="status.waiting">Waiting</span>
|
|
135
|
+
</div>
|
|
136
|
+
<pre class="claude-log" id="claude-log"></pre>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="modal-actions">
|
|
139
|
+
<button class="btn btn-secondary" onclick="closeModal()" data-i18n="btn.cancel">Cancel</button>
|
|
140
|
+
<button class="btn btn-danger" id="btn-archive" style="display:none" onclick="archiveTicket()" data-i18n="btn.archive">Archive</button>
|
|
141
|
+
<button class="btn btn-next" id="btn-next" style="display:none" onclick="advanceTicket()" data-i18n="btn.next">Next</button>
|
|
142
|
+
<button class="btn btn-primary" id="btn-save" onclick="saveTicket()" data-i18n="btn.createTicket">Create ticket</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- Modal ACTION.md -->
|
|
149
|
+
<div class="modal-overlay" id="action-modal">
|
|
150
|
+
<div class="modal action-modal">
|
|
151
|
+
<div class="modal-header">
|
|
152
|
+
<div>
|
|
153
|
+
<span class="pill">ACTION.md</span>
|
|
154
|
+
<h2 id="action-modal-title" data-i18n="action.instructions">Instructions</h2>
|
|
155
|
+
</div>
|
|
156
|
+
<button class="modal-close" onclick="closeActionModal()">×</button>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="modal-body">
|
|
159
|
+
<div class="action-toolbar">
|
|
160
|
+
<button class="btn btn-secondary" onclick="reloadActionContent()" data-i18n="btn.reload">Reload</button>
|
|
161
|
+
<button class="btn btn-secondary" id="action-edit-btn" onclick="enterActionEdit()" data-i18n="btn.edit">Edit</button>
|
|
162
|
+
<button class="btn btn-primary" id="action-save-btn" onclick="saveActionContent()" disabled data-i18n="btn.save">Save</button>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="action-meta" id="action-meta"></div>
|
|
165
|
+
<div class="action-empty" id="action-empty" style="display:none"></div>
|
|
166
|
+
<textarea id="action-content" readonly placeholder="No instructions available" data-i18n-placeholder="action.noInstructions"></textarea>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<!-- Context menu -->
|
|
172
|
+
<div class="context-menu" id="context-menu">
|
|
173
|
+
<div class="context-menu-item" onclick="editFromContext()" data-i18n="btn.edit">Edit</div>
|
|
174
|
+
<div class="context-menu-item danger" onclick="archiveFromContext()" data-i18n="btn.archive">Archive</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<footer>
|
|
178
|
+
<span>AutoCode v2.0 | <span id="time">${timestamp}</span></span>
|
|
179
|
+
</footer>
|
|
180
|
+
|
|
181
|
+
<script>
|
|
182
|
+
const COLUMNS = ${columnsJson};
|
|
183
|
+
const TICKETS = ${ticketsJson};
|
|
184
|
+
${getScript()}
|
|
185
|
+
</script>
|
|
186
|
+
</body>
|
|
187
|
+
</html>`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Escape HTML entities
|
|
191
|
+
*/
|
|
192
|
+
function escapeHtml(text) {
|
|
193
|
+
return text
|
|
194
|
+
.replace(/&/g, '&')
|
|
195
|
+
.replace(/</g, '<')
|
|
196
|
+
.replace(/>/g, '>')
|
|
197
|
+
.replace(/"/g, '"');
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get CSS styles
|
|
201
|
+
*/
|
|
202
|
+
function getStyles() {
|
|
203
|
+
return `
|
|
204
|
+
:root {
|
|
205
|
+
--bg-primary: #0a0a0f;
|
|
206
|
+
--bg-secondary: #12121a;
|
|
207
|
+
--bg-tertiary: #1a1a25;
|
|
208
|
+
--bg: #0a0a0f;
|
|
209
|
+
--card: #12121a;
|
|
210
|
+
--border: #2a2a3a;
|
|
211
|
+
--text: #f1f5f9;
|
|
212
|
+
--text-primary: #f1f5f9;
|
|
213
|
+
--text-secondary: #94a3b8;
|
|
214
|
+
--muted: #64748b;
|
|
215
|
+
--accent: #6366f1;
|
|
216
|
+
--accent-light: #818cf8;
|
|
217
|
+
--accent-hover: #818cf8;
|
|
218
|
+
--accent-glow: rgba(99, 102, 241, 0.3);
|
|
219
|
+
--blue: #3b82f6;
|
|
220
|
+
--green: #22c55e;
|
|
221
|
+
--yellow: #f59e0b;
|
|
222
|
+
--red: #ef4444;
|
|
223
|
+
--gradient-1: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
227
|
+
|
|
228
|
+
body {
|
|
229
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
230
|
+
background: var(--bg);
|
|
231
|
+
color: var(--text);
|
|
232
|
+
min-height: 100vh;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Header */
|
|
236
|
+
header {
|
|
237
|
+
background: var(--bg-secondary);
|
|
238
|
+
padding: 12px 24px;
|
|
239
|
+
border-bottom: 1px solid var(--border);
|
|
240
|
+
display: flex;
|
|
241
|
+
justify-content: space-between;
|
|
242
|
+
align-items: center;
|
|
243
|
+
flex-wrap: wrap;
|
|
244
|
+
gap: 12px;
|
|
245
|
+
height: 60px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.header-logo {
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
gap: 12px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.logo-link {
|
|
255
|
+
font-size: 1.25rem;
|
|
256
|
+
font-weight: 700;
|
|
257
|
+
background: var(--gradient-1);
|
|
258
|
+
-webkit-background-clip: text;
|
|
259
|
+
-webkit-text-fill-color: transparent;
|
|
260
|
+
background-clip: text;
|
|
261
|
+
text-decoration: none;
|
|
262
|
+
display: flex;
|
|
263
|
+
align-items: center;
|
|
264
|
+
gap: 8px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.header-title {
|
|
268
|
+
color: var(--text-secondary);
|
|
269
|
+
font-size: 0.9rem;
|
|
270
|
+
padding-left: 12px;
|
|
271
|
+
border-left: 1px solid var(--border);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
h1 { font-size: 20px; font-weight: 600; color: var(--accent); }
|
|
275
|
+
|
|
276
|
+
.stats { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
277
|
+
.stat {
|
|
278
|
+
background: var(--bg-tertiary);
|
|
279
|
+
padding: 6px 10px;
|
|
280
|
+
border-radius: 6px;
|
|
281
|
+
font-size: 13px;
|
|
282
|
+
}
|
|
283
|
+
.stat.connected { color: var(--green); }
|
|
284
|
+
.stat.P0 { color: var(--red); }
|
|
285
|
+
.stat.P1 { color: var(--yellow); }
|
|
286
|
+
.stat.P2 { color: var(--blue); }
|
|
287
|
+
|
|
288
|
+
.filters { display: flex; gap: 8px; align-items: center; }
|
|
289
|
+
|
|
290
|
+
select, input, textarea {
|
|
291
|
+
background: var(--bg-primary);
|
|
292
|
+
border: 1px solid var(--border);
|
|
293
|
+
color: var(--text);
|
|
294
|
+
padding: 6px 10px;
|
|
295
|
+
border-radius: 8px;
|
|
296
|
+
font-size: 13px;
|
|
297
|
+
transition: border-color 0.3s;
|
|
298
|
+
}
|
|
299
|
+
select:focus, input:focus, textarea:focus {
|
|
300
|
+
outline: none;
|
|
301
|
+
border-color: var(--accent);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.btn {
|
|
305
|
+
padding: 10px 24px;
|
|
306
|
+
border: none;
|
|
307
|
+
border-radius: 8px;
|
|
308
|
+
font-size: 0.9rem;
|
|
309
|
+
font-weight: 600;
|
|
310
|
+
cursor: pointer;
|
|
311
|
+
transition: all 0.3s;
|
|
312
|
+
}
|
|
313
|
+
.btn-primary { background: var(--accent); color: #fff; }
|
|
314
|
+
.btn-primary:hover { background: var(--accent-light); transform: translateY(-2px); }
|
|
315
|
+
.btn-secondary { background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border); }
|
|
316
|
+
.btn-secondary:hover { border-color: var(--accent); }
|
|
317
|
+
.btn-danger { background: var(--red); color: #fff; }
|
|
318
|
+
.btn-next { background: var(--blue); color: #fff; }
|
|
319
|
+
|
|
320
|
+
/* Board */
|
|
321
|
+
.board {
|
|
322
|
+
display: flex;
|
|
323
|
+
gap: 16px;
|
|
324
|
+
padding: 20px;
|
|
325
|
+
overflow-x: auto;
|
|
326
|
+
height: calc(100vh - 60px - 50px);
|
|
327
|
+
padding-bottom: 10px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.column {
|
|
331
|
+
min-width: 300px;
|
|
332
|
+
max-width: 350px;
|
|
333
|
+
flex: 1;
|
|
334
|
+
background: var(--bg-secondary);
|
|
335
|
+
border: 1px solid var(--border);
|
|
336
|
+
border-radius: 12px;
|
|
337
|
+
display: flex;
|
|
338
|
+
flex-direction: column;
|
|
339
|
+
transition: border-color 0.2s;
|
|
340
|
+
}
|
|
341
|
+
.column.drag-over {
|
|
342
|
+
border-color: var(--accent);
|
|
343
|
+
background: rgba(99, 102, 241, 0.1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.column-header {
|
|
347
|
+
padding: 16px;
|
|
348
|
+
display: flex;
|
|
349
|
+
justify-content: space-between;
|
|
350
|
+
align-items: center;
|
|
351
|
+
border-bottom: 1px solid var(--border);
|
|
352
|
+
}
|
|
353
|
+
.column-title { font-size: 14px; font-weight: 600; }
|
|
354
|
+
.column-header-actions { display: flex; gap: 10px; align-items: center; }
|
|
355
|
+
.column-count {
|
|
356
|
+
background: var(--bg-tertiary);
|
|
357
|
+
color: var(--text-secondary);
|
|
358
|
+
padding: 2px 10px;
|
|
359
|
+
border-radius: 10px;
|
|
360
|
+
font-size: 0.85rem;
|
|
361
|
+
font-weight: 600;
|
|
362
|
+
}
|
|
363
|
+
.btn-action {
|
|
364
|
+
background: none;
|
|
365
|
+
border: none;
|
|
366
|
+
cursor: pointer;
|
|
367
|
+
font-size: 16px;
|
|
368
|
+
padding: 4px;
|
|
369
|
+
opacity: 0.6;
|
|
370
|
+
transition: opacity 0.3s;
|
|
371
|
+
}
|
|
372
|
+
.btn-action:hover { opacity: 1; }
|
|
373
|
+
|
|
374
|
+
.column-body {
|
|
375
|
+
padding: 12px;
|
|
376
|
+
flex: 1;
|
|
377
|
+
overflow-y: auto;
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-direction: column;
|
|
380
|
+
gap: 10px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.empty {
|
|
384
|
+
text-align: center;
|
|
385
|
+
color: var(--muted);
|
|
386
|
+
padding: 24px;
|
|
387
|
+
font-size: 13px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Tickets */
|
|
391
|
+
.ticket {
|
|
392
|
+
background: var(--bg-primary);
|
|
393
|
+
border: 1px solid var(--border);
|
|
394
|
+
border-radius: 8px;
|
|
395
|
+
padding: 14px;
|
|
396
|
+
cursor: pointer;
|
|
397
|
+
transition: all 0.3s ease;
|
|
398
|
+
}
|
|
399
|
+
.ticket:hover {
|
|
400
|
+
border-color: var(--accent);
|
|
401
|
+
transform: translateY(-2px);
|
|
402
|
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
|
403
|
+
}
|
|
404
|
+
.ticket.dragging {
|
|
405
|
+
opacity: 0.5;
|
|
406
|
+
transform: rotate(3deg) scale(1.05);
|
|
407
|
+
box-shadow: 0 15px 40px var(--accent-glow);
|
|
408
|
+
z-index: 100;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.ticket-key {
|
|
412
|
+
font-family: 'JetBrains Mono', monospace;
|
|
413
|
+
font-size: 0.8rem;
|
|
414
|
+
color: var(--accent-light);
|
|
415
|
+
font-weight: 600;
|
|
416
|
+
margin-bottom: 4px;
|
|
417
|
+
}
|
|
418
|
+
.ticket-title {
|
|
419
|
+
font-size: 0.95rem;
|
|
420
|
+
font-weight: 500;
|
|
421
|
+
line-height: 1.4;
|
|
422
|
+
margin-bottom: 10px;
|
|
423
|
+
}
|
|
424
|
+
.ticket-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
425
|
+
|
|
426
|
+
.priority {
|
|
427
|
+
font-size: 0.7rem;
|
|
428
|
+
font-weight: 700;
|
|
429
|
+
padding: 3px 8px;
|
|
430
|
+
border-radius: 4px;
|
|
431
|
+
}
|
|
432
|
+
.priority.P0 { background: #ef444420; color: #ef4444; }
|
|
433
|
+
.priority.P1 { background: #f59e0b20; color: #f59e0b; }
|
|
434
|
+
.priority.P2 { background: #3b82f620; color: #3b82f6; }
|
|
435
|
+
.priority.P3 { background: #64748b20; color: #64748b; }
|
|
436
|
+
|
|
437
|
+
/* Claude Processing Indicator */
|
|
438
|
+
.ticket.claude-processing {
|
|
439
|
+
border-color: #a855f7;
|
|
440
|
+
animation: claudePulse 2s ease-in-out infinite;
|
|
441
|
+
box-shadow: 0 0 20px rgba(168, 85, 247, 0.3);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
@keyframes claudePulse {
|
|
445
|
+
0%, 100% {
|
|
446
|
+
box-shadow: 0 0 10px rgba(168, 85, 247, 0.2);
|
|
447
|
+
border-color: #a855f7;
|
|
448
|
+
}
|
|
449
|
+
50% {
|
|
450
|
+
box-shadow: 0 0 25px rgba(168, 85, 247, 0.5);
|
|
451
|
+
border-color: #c084fc;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.ticket-claude-indicator {
|
|
456
|
+
margin-top: 10px;
|
|
457
|
+
padding-top: 10px;
|
|
458
|
+
border-top: 1px solid var(--border);
|
|
459
|
+
display: flex;
|
|
460
|
+
align-items: center;
|
|
461
|
+
gap: 8px;
|
|
462
|
+
font-size: 0.8rem;
|
|
463
|
+
color: #a855f7;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.claude-dot {
|
|
467
|
+
width: 8px;
|
|
468
|
+
height: 8px;
|
|
469
|
+
background: #a855f7;
|
|
470
|
+
border-radius: 50%;
|
|
471
|
+
animation: claudeDotPulse 1.5s ease infinite;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
@keyframes claudeDotPulse {
|
|
475
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
476
|
+
50% { opacity: 0.5; transform: scale(1.3); }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* Modal */
|
|
480
|
+
.modal-overlay {
|
|
481
|
+
position: fixed;
|
|
482
|
+
inset: 0;
|
|
483
|
+
background: rgba(0,0,0,0.8);
|
|
484
|
+
backdrop-filter: blur(5px);
|
|
485
|
+
z-index: 1000;
|
|
486
|
+
display: none;
|
|
487
|
+
overflow-y: auto;
|
|
488
|
+
}
|
|
489
|
+
.modal-overlay.active { display: block; }
|
|
490
|
+
|
|
491
|
+
.modal {
|
|
492
|
+
width: 100%;
|
|
493
|
+
min-height: 100vh;
|
|
494
|
+
padding: 24px 48px;
|
|
495
|
+
max-width: 100%;
|
|
496
|
+
margin: 0;
|
|
497
|
+
background: var(--bg-secondary);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.modal-header {
|
|
501
|
+
display: flex;
|
|
502
|
+
justify-content: space-between;
|
|
503
|
+
align-items: center;
|
|
504
|
+
margin-bottom: 24px;
|
|
505
|
+
padding-bottom: 16px;
|
|
506
|
+
border-bottom: 1px solid var(--border);
|
|
507
|
+
}
|
|
508
|
+
.modal-header h2 { font-size: 1.1rem; font-weight: 600; }
|
|
509
|
+
.modal-close {
|
|
510
|
+
background: none;
|
|
511
|
+
border: none;
|
|
512
|
+
color: var(--text-secondary);
|
|
513
|
+
font-size: 1.5rem;
|
|
514
|
+
cursor: pointer;
|
|
515
|
+
line-height: 1;
|
|
516
|
+
padding: 0;
|
|
517
|
+
}
|
|
518
|
+
.modal-close:hover { color: var(--text-primary); }
|
|
519
|
+
|
|
520
|
+
.modal-body { display: flex; flex-direction: column; gap: 16px; }
|
|
521
|
+
|
|
522
|
+
.form-group { display: flex; flex-direction: column; gap: 6px; }
|
|
523
|
+
.form-group label { font-size: 13px; color: var(--muted); font-weight: 500; }
|
|
524
|
+
.form-group input, .form-group textarea, .form-group select {
|
|
525
|
+
padding: 10px 12px;
|
|
526
|
+
font-size: 14px;
|
|
527
|
+
}
|
|
528
|
+
.form-group textarea { min-height: 100px; resize: vertical; }
|
|
529
|
+
|
|
530
|
+
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
531
|
+
|
|
532
|
+
.labels-select .selected-labels {
|
|
533
|
+
display: flex;
|
|
534
|
+
flex-wrap: wrap;
|
|
535
|
+
gap: 6px;
|
|
536
|
+
margin-top: 8px;
|
|
537
|
+
}
|
|
538
|
+
.label-tag {
|
|
539
|
+
background: #21262d;
|
|
540
|
+
padding: 4px 8px;
|
|
541
|
+
border-radius: 4px;
|
|
542
|
+
font-size: 12px;
|
|
543
|
+
display: flex;
|
|
544
|
+
align-items: center;
|
|
545
|
+
gap: 6px;
|
|
546
|
+
}
|
|
547
|
+
.remove-label {
|
|
548
|
+
cursor: pointer;
|
|
549
|
+
opacity: 0.6;
|
|
550
|
+
}
|
|
551
|
+
.remove-label:hover { opacity: 1; color: var(--red); }
|
|
552
|
+
|
|
553
|
+
.criteria-list { display: flex; flex-direction: column; gap: 8px; }
|
|
554
|
+
.criteria-item {
|
|
555
|
+
display: flex;
|
|
556
|
+
gap: 8px;
|
|
557
|
+
align-items: center;
|
|
558
|
+
}
|
|
559
|
+
.criteria-item input { flex: 1; }
|
|
560
|
+
.btn-remove {
|
|
561
|
+
background: none;
|
|
562
|
+
border: none;
|
|
563
|
+
color: var(--red);
|
|
564
|
+
font-size: 18px;
|
|
565
|
+
cursor: pointer;
|
|
566
|
+
}
|
|
567
|
+
.btn-add {
|
|
568
|
+
background: none;
|
|
569
|
+
border: 1px dashed var(--border);
|
|
570
|
+
color: var(--muted);
|
|
571
|
+
padding: 8px 12px;
|
|
572
|
+
border-radius: 6px;
|
|
573
|
+
cursor: pointer;
|
|
574
|
+
font-size: 13px;
|
|
575
|
+
margin-top: 8px;
|
|
576
|
+
}
|
|
577
|
+
.btn-add:hover { border-color: var(--accent); color: var(--accent); }
|
|
578
|
+
|
|
579
|
+
.comments-section {
|
|
580
|
+
border-top: 1px solid var(--border);
|
|
581
|
+
padding-top: 16px;
|
|
582
|
+
margin-top: 8px;
|
|
583
|
+
}
|
|
584
|
+
.comments-header {
|
|
585
|
+
display: flex;
|
|
586
|
+
align-items: center;
|
|
587
|
+
gap: 8px;
|
|
588
|
+
margin-bottom: 12px;
|
|
589
|
+
}
|
|
590
|
+
.comments-header h3 { font-size: 14px; font-weight: 600; }
|
|
591
|
+
.comments-count {
|
|
592
|
+
background: #21262d;
|
|
593
|
+
padding: 2px 8px;
|
|
594
|
+
border-radius: 10px;
|
|
595
|
+
font-size: 12px;
|
|
596
|
+
}
|
|
597
|
+
.comments-list {
|
|
598
|
+
background: var(--bg);
|
|
599
|
+
border-radius: 6px;
|
|
600
|
+
padding: 12px;
|
|
601
|
+
max-height: 200px;
|
|
602
|
+
overflow-y: auto;
|
|
603
|
+
}
|
|
604
|
+
.no-comments { color: var(--muted); font-size: 13px; text-align: center; }
|
|
605
|
+
.comment {
|
|
606
|
+
padding: 10px 0;
|
|
607
|
+
border-bottom: 1px solid var(--border);
|
|
608
|
+
}
|
|
609
|
+
.comment:last-child { border-bottom: none; }
|
|
610
|
+
.comment-meta {
|
|
611
|
+
display: flex;
|
|
612
|
+
justify-content: space-between;
|
|
613
|
+
margin-bottom: 6px;
|
|
614
|
+
}
|
|
615
|
+
.comment-column {
|
|
616
|
+
font-size: 11px;
|
|
617
|
+
color: var(--blue);
|
|
618
|
+
padding: 2px 6px;
|
|
619
|
+
background: rgba(77,171,247,0.1);
|
|
620
|
+
border-radius: 4px;
|
|
621
|
+
}
|
|
622
|
+
.comment-date { font-size: 11px; color: var(--muted); }
|
|
623
|
+
.comment-text { font-size: 13px; line-height: 1.5; }
|
|
624
|
+
.comment-text h2, .comment-text h3, .comment-text h4 { margin: 8px 0 4px; }
|
|
625
|
+
.comment-text code {
|
|
626
|
+
background: #21262d;
|
|
627
|
+
padding: 2px 6px;
|
|
628
|
+
border-radius: 4px;
|
|
629
|
+
font-size: 12px;
|
|
630
|
+
}
|
|
631
|
+
.comment-text ul { margin-left: 20px; }
|
|
632
|
+
|
|
633
|
+
.add-comment { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
634
|
+
.add-comment textarea { min-height: 60px; }
|
|
635
|
+
.btn-comment {
|
|
636
|
+
align-self: flex-end;
|
|
637
|
+
background: var(--accent);
|
|
638
|
+
color: #fff;
|
|
639
|
+
border: none;
|
|
640
|
+
padding: 8px 16px;
|
|
641
|
+
border-radius: 6px;
|
|
642
|
+
cursor: pointer;
|
|
643
|
+
font-size: 13px;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.modal-actions {
|
|
647
|
+
display: flex;
|
|
648
|
+
gap: 12px;
|
|
649
|
+
justify-content: flex-end;
|
|
650
|
+
margin-top: 24px;
|
|
651
|
+
padding-top: 24px;
|
|
652
|
+
border-top: 1px solid var(--border);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/* Action Modal */
|
|
656
|
+
.action-modal textarea {
|
|
657
|
+
width: 100%;
|
|
658
|
+
min-height: 300px;
|
|
659
|
+
font-family: monospace;
|
|
660
|
+
font-size: 13px;
|
|
661
|
+
background: var(--bg);
|
|
662
|
+
resize: vertical;
|
|
663
|
+
}
|
|
664
|
+
.action-toolbar {
|
|
665
|
+
display: flex;
|
|
666
|
+
gap: 8px;
|
|
667
|
+
margin-bottom: 12px;
|
|
668
|
+
}
|
|
669
|
+
.action-meta {
|
|
670
|
+
font-size: 12px;
|
|
671
|
+
color: var(--muted);
|
|
672
|
+
margin-bottom: 8px;
|
|
673
|
+
}
|
|
674
|
+
.action-empty {
|
|
675
|
+
background: var(--bg);
|
|
676
|
+
padding: 24px;
|
|
677
|
+
text-align: center;
|
|
678
|
+
border-radius: 6px;
|
|
679
|
+
color: var(--muted);
|
|
680
|
+
}
|
|
681
|
+
.pill {
|
|
682
|
+
background: var(--accent);
|
|
683
|
+
color: #fff;
|
|
684
|
+
padding: 4px 8px;
|
|
685
|
+
border-radius: 4px;
|
|
686
|
+
font-size: 11px;
|
|
687
|
+
font-weight: 600;
|
|
688
|
+
margin-right: 8px;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/* Context Menu */
|
|
692
|
+
.context-menu {
|
|
693
|
+
position: fixed;
|
|
694
|
+
background: var(--bg-tertiary);
|
|
695
|
+
border: 1px solid var(--border);
|
|
696
|
+
border-radius: 8px;
|
|
697
|
+
padding: 4px 0;
|
|
698
|
+
min-width: 140px;
|
|
699
|
+
z-index: 1001;
|
|
700
|
+
display: none;
|
|
701
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
|
|
702
|
+
}
|
|
703
|
+
.context-menu.active { display: block; }
|
|
704
|
+
.context-menu-item {
|
|
705
|
+
padding: 8px 16px;
|
|
706
|
+
font-size: 0.85rem;
|
|
707
|
+
cursor: pointer;
|
|
708
|
+
transition: background 0.2s;
|
|
709
|
+
}
|
|
710
|
+
.context-menu-item:hover { background: var(--bg-secondary); }
|
|
711
|
+
.context-menu-item.danger { color: var(--red); }
|
|
712
|
+
|
|
713
|
+
/* Notifications */
|
|
714
|
+
.notifications {
|
|
715
|
+
position: fixed;
|
|
716
|
+
top: 80px;
|
|
717
|
+
right: 20px;
|
|
718
|
+
z-index: 1001;
|
|
719
|
+
display: flex;
|
|
720
|
+
flex-direction: column;
|
|
721
|
+
gap: 10px;
|
|
722
|
+
max-width: 350px;
|
|
723
|
+
}
|
|
724
|
+
.notification {
|
|
725
|
+
background: var(--bg-tertiary);
|
|
726
|
+
border: 1px solid var(--border);
|
|
727
|
+
border-left: 4px solid var(--accent);
|
|
728
|
+
border-radius: 8px;
|
|
729
|
+
padding: 12px 16px;
|
|
730
|
+
display: flex;
|
|
731
|
+
align-items: flex-start;
|
|
732
|
+
gap: 12px;
|
|
733
|
+
animation: slideInRight 0.4s ease;
|
|
734
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
|
|
735
|
+
}
|
|
736
|
+
.notification.success { border-left-color: var(--green); }
|
|
737
|
+
.notification.error { border-left-color: var(--red); }
|
|
738
|
+
.notification.warning { border-left-color: var(--yellow); }
|
|
739
|
+
.notification.claude { border-left-color: #a855f7; }
|
|
740
|
+
.notification-icon { font-size: 1.2rem; flex-shrink: 0; }
|
|
741
|
+
.notification-content { flex: 1; }
|
|
742
|
+
.notification-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 2px; }
|
|
743
|
+
.notification-message { font-size: 0.8rem; color: var(--text-secondary); }
|
|
744
|
+
.notification-close {
|
|
745
|
+
background: none;
|
|
746
|
+
border: none;
|
|
747
|
+
color: var(--text-secondary);
|
|
748
|
+
cursor: pointer;
|
|
749
|
+
font-size: 16px;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
@keyframes slideInRight {
|
|
753
|
+
from { transform: translateX(100%); opacity: 0; }
|
|
754
|
+
to { transform: translateX(0); opacity: 1; }
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* Claude Log Section (in modal) */
|
|
758
|
+
.claude-log-section {
|
|
759
|
+
border-top: 1px solid var(--border);
|
|
760
|
+
padding-top: 16px;
|
|
761
|
+
margin-top: 8px;
|
|
762
|
+
}
|
|
763
|
+
.claude-log-header {
|
|
764
|
+
display: flex;
|
|
765
|
+
align-items: center;
|
|
766
|
+
gap: 8px;
|
|
767
|
+
margin-bottom: 12px;
|
|
768
|
+
}
|
|
769
|
+
.claude-log-header h3 { font-size: 14px; font-weight: 600; }
|
|
770
|
+
.claude-log-status {
|
|
771
|
+
font-size: 12px;
|
|
772
|
+
padding: 2px 8px;
|
|
773
|
+
border-radius: 10px;
|
|
774
|
+
background: #21262d;
|
|
775
|
+
}
|
|
776
|
+
.claude-log-status.processing { color: var(--yellow); animation: pulse 1s infinite; }
|
|
777
|
+
.claude-log-status.success { color: var(--green); }
|
|
778
|
+
.claude-log-status.error { color: var(--red); }
|
|
779
|
+
.claude-log {
|
|
780
|
+
background: #0d1117;
|
|
781
|
+
border: 1px solid var(--border);
|
|
782
|
+
border-radius: 6px;
|
|
783
|
+
padding: 16px;
|
|
784
|
+
max-height: 500px;
|
|
785
|
+
overflow-y: auto;
|
|
786
|
+
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
|
|
787
|
+
font-size: 13px;
|
|
788
|
+
line-height: 1.6;
|
|
789
|
+
white-space: pre-wrap;
|
|
790
|
+
word-break: break-word;
|
|
791
|
+
color: var(--text);
|
|
792
|
+
margin: 0;
|
|
793
|
+
}
|
|
794
|
+
@keyframes pulse {
|
|
795
|
+
0%, 100% { opacity: 1; }
|
|
796
|
+
50% { opacity: 0.5; }
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/* Footer */
|
|
800
|
+
footer {
|
|
801
|
+
position: fixed;
|
|
802
|
+
bottom: 0;
|
|
803
|
+
left: 0;
|
|
804
|
+
right: 0;
|
|
805
|
+
padding: 16px 24px;
|
|
806
|
+
text-align: center;
|
|
807
|
+
font-size: 0.9rem;
|
|
808
|
+
color: var(--text-secondary);
|
|
809
|
+
background: var(--bg-secondary);
|
|
810
|
+
border-top: 1px solid var(--border);
|
|
811
|
+
display: flex;
|
|
812
|
+
align-items: center;
|
|
813
|
+
justify-content: center;
|
|
814
|
+
gap: 16px;
|
|
815
|
+
height: 50px;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
@media (max-width: 768px) {
|
|
819
|
+
header { flex-direction: column; align-items: stretch; }
|
|
820
|
+
.filters { flex-wrap: wrap; }
|
|
821
|
+
.form-row { grid-template-columns: 1fr; }
|
|
822
|
+
}
|
|
823
|
+
`;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Get JavaScript for interactivity
|
|
827
|
+
*/
|
|
828
|
+
function getScript() {
|
|
829
|
+
return `
|
|
830
|
+
// i18n Translations
|
|
831
|
+
const translations = {
|
|
832
|
+
en: {
|
|
833
|
+
// Filter
|
|
834
|
+
'filter.allPriorities': 'All priorities',
|
|
835
|
+
'filter.search': 'Search...',
|
|
836
|
+
// Buttons
|
|
837
|
+
'btn.newTicket': '+ New ticket',
|
|
838
|
+
'btn.createTicket': 'Create ticket',
|
|
839
|
+
'btn.update': 'Update',
|
|
840
|
+
'btn.cancel': 'Cancel',
|
|
841
|
+
'btn.archive': 'Archive',
|
|
842
|
+
'btn.next': 'Next',
|
|
843
|
+
'btn.add': 'Add',
|
|
844
|
+
'btn.edit': 'Edit',
|
|
845
|
+
'btn.save': 'Save',
|
|
846
|
+
'btn.reload': 'Reload',
|
|
847
|
+
// Modal
|
|
848
|
+
'modal.newTicket': 'New ticket',
|
|
849
|
+
'modal.editTicket': 'Edit',
|
|
850
|
+
'modal.title': 'Title *',
|
|
851
|
+
'modal.titlePlaceholder': 'E.g.: Fix the login bug',
|
|
852
|
+
'modal.description': 'Description',
|
|
853
|
+
'modal.descriptionPlaceholder': 'Describe the context and details...',
|
|
854
|
+
'modal.priority': 'Priority',
|
|
855
|
+
'modal.releaseType': 'Release type',
|
|
856
|
+
'modal.labels': 'Labels',
|
|
857
|
+
'modal.selectLabel': 'Select a label...',
|
|
858
|
+
'modal.acceptanceCriteria': 'Acceptance criteria',
|
|
859
|
+
'modal.addCriteria': 'Add criteria',
|
|
860
|
+
'modal.comments': 'Comments',
|
|
861
|
+
'modal.noComments': 'No comments',
|
|
862
|
+
'modal.addCommentPlaceholder': 'Add a comment...',
|
|
863
|
+
'modal.claudeTerminal': 'Claude Terminal',
|
|
864
|
+
// Priority
|
|
865
|
+
'priority.p0': 'P0 - Critical',
|
|
866
|
+
'priority.p1': 'P1 - High',
|
|
867
|
+
'priority.p2': 'P2 - Normal',
|
|
868
|
+
'priority.p3': 'P3 - Low',
|
|
869
|
+
// Semver
|
|
870
|
+
'semver.patch': 'Patch (x.x.X) - Bug fix',
|
|
871
|
+
'semver.minor': 'Minor (x.X.0) - Feature',
|
|
872
|
+
'semver.major': 'Major (X.0.0) - Breaking',
|
|
873
|
+
'semver.none': 'No deployment',
|
|
874
|
+
// Status
|
|
875
|
+
'status.waiting': 'Waiting',
|
|
876
|
+
'status.processing': 'Running...',
|
|
877
|
+
'status.completed': 'Completed',
|
|
878
|
+
'status.failed': 'Failed',
|
|
879
|
+
'status.connected': 'Connected',
|
|
880
|
+
// Stats
|
|
881
|
+
'stats.total': 'Total',
|
|
882
|
+
// Board
|
|
883
|
+
'board.empty': 'Empty',
|
|
884
|
+
// Action modal
|
|
885
|
+
'action.instructions': 'Instructions',
|
|
886
|
+
'action.noInstructions': 'No instructions available',
|
|
887
|
+
'action.noFile': 'No ACTION file. Click "Edit" to create it.',
|
|
888
|
+
'action.modifiedOn': 'Modified on',
|
|
889
|
+
// Notifications
|
|
890
|
+
'notify.titleRequired': 'Title required',
|
|
891
|
+
'notify.titleMandatory': 'Title is mandatory',
|
|
892
|
+
'notify.ticketUpdated': 'Ticket updated',
|
|
893
|
+
'notify.ticketCreated': 'Ticket created',
|
|
894
|
+
'notify.ticketAdvanced': 'Ticket advanced',
|
|
895
|
+
'notify.ticketArchived': 'Ticket archived',
|
|
896
|
+
'notify.ticketMoved': 'moved',
|
|
897
|
+
'notify.moveTo': 'To',
|
|
898
|
+
'notify.commentAdded': 'Comment added',
|
|
899
|
+
'notify.actionUpdated': 'updated',
|
|
900
|
+
'notify.error': 'Error',
|
|
901
|
+
'notify.unableToSave': 'Unable to save',
|
|
902
|
+
'notify.loadingError': 'Loading error',
|
|
903
|
+
'notify.claudeStarted': 'Claude started',
|
|
904
|
+
'notify.claudeFinished': 'Claude finished',
|
|
905
|
+
'notify.claudeFailed': 'Claude failed',
|
|
906
|
+
'notify.processingSuccess': 'Processing successful',
|
|
907
|
+
'notify.checkLogs': 'Check logs',
|
|
908
|
+
// Confirm
|
|
909
|
+
'confirm.archive': 'Archive',
|
|
910
|
+
// Button states
|
|
911
|
+
'btn.updating': 'Updating...',
|
|
912
|
+
'btn.creating': 'Creating...',
|
|
913
|
+
'btn.moving': 'Moving...',
|
|
914
|
+
'btn.archiving': 'Archiving...',
|
|
915
|
+
'btn.sending': 'Sending...',
|
|
916
|
+
'btn.saving': 'Saving...',
|
|
917
|
+
},
|
|
918
|
+
fr: {
|
|
919
|
+
// Filter
|
|
920
|
+
'filter.allPriorities': 'Toutes priorités',
|
|
921
|
+
'filter.search': 'Rechercher...',
|
|
922
|
+
// Buttons
|
|
923
|
+
'btn.newTicket': '+ Nouveau ticket',
|
|
924
|
+
'btn.createTicket': 'Créer le ticket',
|
|
925
|
+
'btn.update': 'Mettre à jour',
|
|
926
|
+
'btn.cancel': 'Annuler',
|
|
927
|
+
'btn.archive': 'Archiver',
|
|
928
|
+
'btn.next': 'Suivant',
|
|
929
|
+
'btn.add': 'Ajouter',
|
|
930
|
+
'btn.edit': 'Modifier',
|
|
931
|
+
'btn.save': 'Enregistrer',
|
|
932
|
+
'btn.reload': 'Recharger',
|
|
933
|
+
// Modal
|
|
934
|
+
'modal.newTicket': 'Nouveau ticket',
|
|
935
|
+
'modal.editTicket': 'Modifier',
|
|
936
|
+
'modal.title': 'Titre *',
|
|
937
|
+
'modal.titlePlaceholder': 'Ex: Corriger le bug de connexion',
|
|
938
|
+
'modal.description': 'Description',
|
|
939
|
+
'modal.descriptionPlaceholder': 'Décrivez le contexte et les détails...',
|
|
940
|
+
'modal.priority': 'Priorité',
|
|
941
|
+
'modal.releaseType': 'Type de release',
|
|
942
|
+
'modal.labels': 'Labels',
|
|
943
|
+
'modal.selectLabel': 'Sélectionner un label...',
|
|
944
|
+
'modal.acceptanceCriteria': 'Critères d\\'acceptation',
|
|
945
|
+
'modal.addCriteria': 'Ajouter un critère',
|
|
946
|
+
'modal.comments': 'Commentaires',
|
|
947
|
+
'modal.noComments': 'Aucun commentaire',
|
|
948
|
+
'modal.addCommentPlaceholder': 'Ajouter un commentaire...',
|
|
949
|
+
'modal.claudeTerminal': 'Terminal Claude',
|
|
950
|
+
// Priority
|
|
951
|
+
'priority.p0': 'P0 - Critique',
|
|
952
|
+
'priority.p1': 'P1 - Haute',
|
|
953
|
+
'priority.p2': 'P2 - Normale',
|
|
954
|
+
'priority.p3': 'P3 - Basse',
|
|
955
|
+
// Semver
|
|
956
|
+
'semver.patch': 'Patch (x.x.X) - Bug fix',
|
|
957
|
+
'semver.minor': 'Minor (x.X.0) - Fonctionnalité',
|
|
958
|
+
'semver.major': 'Major (X.0.0) - Breaking',
|
|
959
|
+
'semver.none': 'Aucun déploiement',
|
|
960
|
+
// Status
|
|
961
|
+
'status.waiting': 'En attente',
|
|
962
|
+
'status.processing': 'En cours...',
|
|
963
|
+
'status.completed': 'Terminé',
|
|
964
|
+
'status.failed': 'Échoué',
|
|
965
|
+
'status.connected': 'Connecté',
|
|
966
|
+
// Stats
|
|
967
|
+
'stats.total': 'Total',
|
|
968
|
+
// Board
|
|
969
|
+
'board.empty': 'Vide',
|
|
970
|
+
// Action modal
|
|
971
|
+
'action.instructions': 'Instructions',
|
|
972
|
+
'action.noInstructions': 'Aucune instruction disponible',
|
|
973
|
+
'action.noFile': 'Aucun fichier ACTION. Cliquez sur "Modifier" pour le créer.',
|
|
974
|
+
'action.modifiedOn': 'Modifié le',
|
|
975
|
+
// Notifications
|
|
976
|
+
'notify.titleRequired': 'Titre requis',
|
|
977
|
+
'notify.titleMandatory': 'Le titre est obligatoire',
|
|
978
|
+
'notify.ticketUpdated': 'Ticket mis à jour',
|
|
979
|
+
'notify.ticketCreated': 'Ticket créé',
|
|
980
|
+
'notify.ticketAdvanced': 'Ticket avancé',
|
|
981
|
+
'notify.ticketArchived': 'Ticket archivé',
|
|
982
|
+
'notify.ticketMoved': 'déplacé',
|
|
983
|
+
'notify.moveTo': 'Vers',
|
|
984
|
+
'notify.commentAdded': 'Commentaire ajouté',
|
|
985
|
+
'notify.actionUpdated': 'mis à jour',
|
|
986
|
+
'notify.error': 'Erreur',
|
|
987
|
+
'notify.unableToSave': 'Impossible de sauvegarder',
|
|
988
|
+
'notify.loadingError': 'Erreur de chargement',
|
|
989
|
+
'notify.claudeStarted': 'Claude démarré',
|
|
990
|
+
'notify.claudeFinished': 'Claude terminé',
|
|
991
|
+
'notify.claudeFailed': 'Claude échoué',
|
|
992
|
+
'notify.processingSuccess': 'Traitement réussi',
|
|
993
|
+
'notify.checkLogs': 'Voir les logs',
|
|
994
|
+
// Confirm
|
|
995
|
+
'confirm.archive': 'Archiver',
|
|
996
|
+
// Button states
|
|
997
|
+
'btn.updating': 'Mise à jour...',
|
|
998
|
+
'btn.creating': 'Création...',
|
|
999
|
+
'btn.moving': 'Déplacement...',
|
|
1000
|
+
'btn.archiving': 'Archivage...',
|
|
1001
|
+
'btn.sending': 'Envoi...',
|
|
1002
|
+
'btn.saving': 'Sauvegarde...',
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
let currentLang = localStorage.getItem('autocode-ui-lang') || 'en';
|
|
1007
|
+
|
|
1008
|
+
function t(key) {
|
|
1009
|
+
return translations[currentLang][key] || translations['en'][key] || key;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function switchLanguage(lang) {
|
|
1013
|
+
currentLang = lang;
|
|
1014
|
+
currentActionLang = lang; // Sync ACTION file language
|
|
1015
|
+
localStorage.setItem('autocode-ui-lang', lang);
|
|
1016
|
+
localStorage.setItem('autocode-lang', lang);
|
|
1017
|
+
document.documentElement.lang = lang;
|
|
1018
|
+
const selector = document.getElementById('ui-lang');
|
|
1019
|
+
if (selector) selector.value = lang;
|
|
1020
|
+
|
|
1021
|
+
// Update all elements with data-i18n
|
|
1022
|
+
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
1023
|
+
const key = el.getAttribute('data-i18n');
|
|
1024
|
+
if (translations[lang] && translations[lang][key]) {
|
|
1025
|
+
el.textContent = translations[lang][key];
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// Update placeholders
|
|
1030
|
+
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
|
1031
|
+
const key = el.getAttribute('data-i18n-placeholder');
|
|
1032
|
+
if (translations[lang] && translations[lang][key]) {
|
|
1033
|
+
el.placeholder = translations[lang][key];
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// Update select options
|
|
1038
|
+
document.querySelectorAll('select option[data-i18n]').forEach(el => {
|
|
1039
|
+
const key = el.getAttribute('data-i18n');
|
|
1040
|
+
if (translations[lang] && translations[lang][key]) {
|
|
1041
|
+
el.textContent = translations[lang][key];
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// Re-render board to update dynamic content
|
|
1046
|
+
render();
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// State
|
|
1050
|
+
let filterPriority = '';
|
|
1051
|
+
let filterSearch = '';
|
|
1052
|
+
let selectedLabels = [];
|
|
1053
|
+
let criteriaCount = 0;
|
|
1054
|
+
let editingKey = null;
|
|
1055
|
+
let currentComments = [];
|
|
1056
|
+
let contextMenuTicket = null;
|
|
1057
|
+
let draggedTicket = null;
|
|
1058
|
+
let draggedFromColumn = null;
|
|
1059
|
+
let currentActionSlug = null;
|
|
1060
|
+
let currentActionLang = localStorage.getItem('autocode-lang') || 'en';
|
|
1061
|
+
let originalActionContent = '';
|
|
1062
|
+
let claudeProcessingTickets = new Set(); // Tickets currently being processed by Claude
|
|
1063
|
+
|
|
1064
|
+
// Initialize language selector on page load
|
|
1065
|
+
(function initLangSelector() {
|
|
1066
|
+
const langSelect = document.getElementById('ui-lang');
|
|
1067
|
+
if (langSelect) langSelect.value = currentLang;
|
|
1068
|
+
switchLanguage(currentLang);
|
|
1069
|
+
})();
|
|
1070
|
+
|
|
1071
|
+
// ========================================
|
|
1072
|
+
// NOTIFICATIONS
|
|
1073
|
+
// ========================================
|
|
1074
|
+
function showNotification(type, title, message, duration = 5000) {
|
|
1075
|
+
const container = document.getElementById('notifications');
|
|
1076
|
+
const id = 'notif-' + Date.now();
|
|
1077
|
+
const icons = { info: '\\u{1F535}', success: '\\u2705', warning: '\\u26A0\\uFE0F', error: '\\u274C', claude: '\\u{1F916}' };
|
|
1078
|
+
|
|
1079
|
+
const notif = document.createElement('div');
|
|
1080
|
+
notif.className = 'notification ' + (type === 'claude' ? 'info' : type);
|
|
1081
|
+
notif.id = id;
|
|
1082
|
+
notif.innerHTML = '<span class="notification-icon">' + (icons[type] || icons.info) + '</span>' +
|
|
1083
|
+
'<div class="notification-content"><div class="notification-title">' + title + '</div>' +
|
|
1084
|
+
(message ? '<div class="notification-message">' + message + '</div>' : '') + '</div>' +
|
|
1085
|
+
'<button class="notification-close" onclick="closeNotification(\\'' + id + '\\')">×</button>';
|
|
1086
|
+
|
|
1087
|
+
container.appendChild(notif);
|
|
1088
|
+
setTimeout(() => closeNotification(id), duration);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function closeNotification(id) {
|
|
1092
|
+
const notif = document.getElementById(id);
|
|
1093
|
+
if (notif) notif.remove();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// ========================================
|
|
1097
|
+
// RENDER
|
|
1098
|
+
// ========================================
|
|
1099
|
+
function render() {
|
|
1100
|
+
const board = document.getElementById('board');
|
|
1101
|
+
board.innerHTML = '';
|
|
1102
|
+
let stats = { total: 0, byPriority: { P0: 0, P1: 0, P2: 0, P3: 0 } };
|
|
1103
|
+
|
|
1104
|
+
COLUMNS.forEach(col => {
|
|
1105
|
+
let tickets = TICKETS.filter(t => t.column_slug === col.slug);
|
|
1106
|
+
if (filterPriority) tickets = tickets.filter(t => t.priority === filterPriority);
|
|
1107
|
+
if (filterSearch) {
|
|
1108
|
+
const s = filterSearch.toLowerCase();
|
|
1109
|
+
tickets = tickets.filter(t => t.title.toLowerCase().includes(s) || t.key.toLowerCase().includes(s));
|
|
1110
|
+
}
|
|
1111
|
+
tickets.sort((a, b) => {
|
|
1112
|
+
const p = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
1113
|
+
return p[a.priority] - p[b.priority];
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
stats.total += tickets.length;
|
|
1117
|
+
tickets.forEach(t => stats.byPriority[t.priority] = (stats.byPriority[t.priority] || 0) + 1);
|
|
1118
|
+
|
|
1119
|
+
const div = document.createElement('div');
|
|
1120
|
+
div.className = 'column';
|
|
1121
|
+
div.ondragover = onDragOver;
|
|
1122
|
+
div.ondragenter = onDragEnter;
|
|
1123
|
+
div.ondragleave = onDragLeave;
|
|
1124
|
+
div.ondrop = (e) => onDrop(e, col.slug, col.name);
|
|
1125
|
+
|
|
1126
|
+
div.innerHTML = '<div class="column-header">' +
|
|
1127
|
+
'<span class="column-title">' + col.name + '</span>' +
|
|
1128
|
+
'<div class="column-header-actions">' +
|
|
1129
|
+
'<button class="btn-action" title="ACTION.md" onclick="openActionModal(\\'' + col.slug + '\\')">\\u{1F4D8}</button>' +
|
|
1130
|
+
'<span class="column-count">' + tickets.length + '</span>' +
|
|
1131
|
+
'</div></div>' +
|
|
1132
|
+
'<div class="column-body">' +
|
|
1133
|
+
(tickets.length ? tickets.map(tk => {
|
|
1134
|
+
const isProcessing = claudeProcessingTickets.has(tk.key);
|
|
1135
|
+
return '<div class="ticket' + (isProcessing ? ' claude-processing' : '') + '" draggable="true" ' +
|
|
1136
|
+
'onclick="onTicketClick(\\'' + tk.key + '\\')" ' +
|
|
1137
|
+
'oncontextmenu="showContextMenu(event,\\'' + tk.key + '\\')" ' +
|
|
1138
|
+
'ondragstart="onDragStart(event,\\'' + tk.key + '\\',\\'' + col.slug + '\\')" ' +
|
|
1139
|
+
'ondragend="onDragEnd(event)">' +
|
|
1140
|
+
'<div class="ticket-key">' + tk.key + '</div>' +
|
|
1141
|
+
'<div class="ticket-title">' + escapeHtml(tk.title) + '</div>' +
|
|
1142
|
+
'<div class="ticket-meta"><span class="priority ' + tk.priority + '">' + tk.priority + '</span></div>' +
|
|
1143
|
+
(isProcessing ? '<div class="ticket-claude-indicator"><span class="claude-dot"></span>\\u{1F916} ' + t('status.processing') + '</div>' : '') +
|
|
1144
|
+
'</div>';
|
|
1145
|
+
}).join('') : '<div class="empty">' + t('board.empty') + '</div>') +
|
|
1146
|
+
'</div>';
|
|
1147
|
+
|
|
1148
|
+
board.appendChild(div);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
document.getElementById('stats').innerHTML =
|
|
1152
|
+
'<span class="stat connected">\\u{1F7E2} ' + t('status.connected') + '</span>' +
|
|
1153
|
+
'<span class="stat">' + t('stats.total') + ': ' + stats.total + '</span>' +
|
|
1154
|
+
'<span class="stat P0">P0: ' + (stats.byPriority.P0 || 0) + '</span>' +
|
|
1155
|
+
'<span class="stat P1">P1: ' + (stats.byPriority.P1 || 0) + '</span>' +
|
|
1156
|
+
'<span class="stat P2">P2: ' + (stats.byPriority.P2 || 0) + '</span>';
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function escapeHtml(text) {
|
|
1160
|
+
const div = document.createElement('div');
|
|
1161
|
+
div.textContent = text;
|
|
1162
|
+
return div.innerHTML;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// ========================================
|
|
1166
|
+
// FILTERS
|
|
1167
|
+
// ========================================
|
|
1168
|
+
document.getElementById('filter-priority').onchange = e => { filterPriority = e.target.value; render(); };
|
|
1169
|
+
document.getElementById('filter-search').oninput = e => { filterSearch = e.target.value.toLowerCase(); render(); };
|
|
1170
|
+
|
|
1171
|
+
// ========================================
|
|
1172
|
+
// MODAL TICKET
|
|
1173
|
+
// ========================================
|
|
1174
|
+
function openModal(key = null) {
|
|
1175
|
+
editingKey = key;
|
|
1176
|
+
document.getElementById('modal-overlay').classList.add('active');
|
|
1177
|
+
document.body.style.overflow = 'hidden';
|
|
1178
|
+
|
|
1179
|
+
const modalTitle = document.getElementById('modal-title');
|
|
1180
|
+
const saveBtn = document.getElementById('btn-save');
|
|
1181
|
+
const nextBtn = document.getElementById('btn-next');
|
|
1182
|
+
const archiveBtn = document.getElementById('btn-archive');
|
|
1183
|
+
const commentsSection = document.getElementById('comments-section');
|
|
1184
|
+
|
|
1185
|
+
if (key) {
|
|
1186
|
+
modalTitle.textContent = t('modal.editTicket') + ' ' + key;
|
|
1187
|
+
saveBtn.textContent = t('btn.update');
|
|
1188
|
+
nextBtn.style.display = 'inline-block';
|
|
1189
|
+
archiveBtn.style.display = 'inline-block';
|
|
1190
|
+
commentsSection.style.display = 'block';
|
|
1191
|
+
loadTicketForEdit(key);
|
|
1192
|
+
} else {
|
|
1193
|
+
modalTitle.textContent = t('modal.newTicket');
|
|
1194
|
+
saveBtn.textContent = t('btn.createTicket');
|
|
1195
|
+
nextBtn.style.display = 'none';
|
|
1196
|
+
archiveBtn.style.display = 'none';
|
|
1197
|
+
commentsSection.style.display = 'none';
|
|
1198
|
+
resetForm();
|
|
1199
|
+
resetComments();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function closeModal() {
|
|
1204
|
+
document.getElementById('modal-overlay').classList.remove('active');
|
|
1205
|
+
document.body.style.overflow = '';
|
|
1206
|
+
editingKey = null;
|
|
1207
|
+
resetForm();
|
|
1208
|
+
resetClaudeLog();
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function resetForm() {
|
|
1212
|
+
document.getElementById('ticket-title').value = '';
|
|
1213
|
+
document.getElementById('ticket-description').value = '';
|
|
1214
|
+
document.getElementById('ticket-priority').value = 'P2';
|
|
1215
|
+
document.getElementById('ticket-semver').value = 'patch';
|
|
1216
|
+
document.getElementById('ticket-labels').value = '';
|
|
1217
|
+
selectedLabels = [];
|
|
1218
|
+
criteriaCount = 0;
|
|
1219
|
+
document.getElementById('selected-labels').innerHTML = '';
|
|
1220
|
+
document.getElementById('criteria-list').innerHTML = '';
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
async function loadTicketForEdit(key) {
|
|
1224
|
+
try {
|
|
1225
|
+
const res = await fetch('/api/tickets/' + key);
|
|
1226
|
+
if (!res.ok) throw new Error('Ticket not found');
|
|
1227
|
+
const json = await res.json();
|
|
1228
|
+
if (!json.success || !json.data) throw new Error('Ticket not found');
|
|
1229
|
+
const ticket = json.data;
|
|
1230
|
+
|
|
1231
|
+
document.getElementById('ticket-title').value = ticket.title || '';
|
|
1232
|
+
document.getElementById('ticket-description').value = ticket.description || '';
|
|
1233
|
+
document.getElementById('ticket-priority').value = ticket.priority || 'P2';
|
|
1234
|
+
document.getElementById('ticket-semver').value = ticket.semver || 'patch';
|
|
1235
|
+
|
|
1236
|
+
selectedLabels = Array.isArray(ticket.labels) ? [...ticket.labels] : [];
|
|
1237
|
+
renderLabels();
|
|
1238
|
+
|
|
1239
|
+
criteriaCount = 0;
|
|
1240
|
+
document.getElementById('criteria-list').innerHTML = '';
|
|
1241
|
+
if (Array.isArray(ticket.acceptance_criteria)) {
|
|
1242
|
+
ticket.acceptance_criteria.forEach(c => addCriteria(c));
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
renderComments(ticket.comments || []);
|
|
1246
|
+
|
|
1247
|
+
// Fetch Claude log if exists
|
|
1248
|
+
fetchLog(key);
|
|
1249
|
+
} catch (e) {
|
|
1250
|
+
console.error('Error:', e);
|
|
1251
|
+
showNotification('error', 'Error', e.message);
|
|
1252
|
+
closeModal();
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
async function saveTicket() {
|
|
1257
|
+
const title = document.getElementById('ticket-title').value.trim();
|
|
1258
|
+
const description = document.getElementById('ticket-description').value.trim();
|
|
1259
|
+
const priority = document.getElementById('ticket-priority').value;
|
|
1260
|
+
const semver = document.getElementById('ticket-semver').value;
|
|
1261
|
+
const criteria = getCriteria();
|
|
1262
|
+
|
|
1263
|
+
if (!title) {
|
|
1264
|
+
showNotification('warning', t('notify.titleRequired'), t('notify.titleMandatory'));
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const btn = document.getElementById('btn-save');
|
|
1269
|
+
btn.disabled = true;
|
|
1270
|
+
|
|
1271
|
+
try {
|
|
1272
|
+
if (editingKey) {
|
|
1273
|
+
btn.textContent = t('btn.updating');
|
|
1274
|
+
await fetch('/api/tickets/' + editingKey, {
|
|
1275
|
+
method: 'POST',
|
|
1276
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1277
|
+
body: JSON.stringify({ title, description, priority, semver, labels: selectedLabels, acceptance_criteria: criteria })
|
|
1278
|
+
});
|
|
1279
|
+
showNotification('success', t('notify.ticketUpdated'), editingKey);
|
|
1280
|
+
} else {
|
|
1281
|
+
btn.textContent = t('btn.creating');
|
|
1282
|
+
const res = await fetch('/api/tickets', {
|
|
1283
|
+
method: 'POST',
|
|
1284
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1285
|
+
body: JSON.stringify({ title, priority, labels: selectedLabels, description, semver, acceptance_criteria: criteria })
|
|
1286
|
+
});
|
|
1287
|
+
const data = await res.json();
|
|
1288
|
+
showNotification('success', t('notify.ticketCreated'), data.key || '');
|
|
1289
|
+
}
|
|
1290
|
+
closeModal();
|
|
1291
|
+
loadTicketsFromAPI();
|
|
1292
|
+
} catch (e) {
|
|
1293
|
+
showNotification('error', t('notify.error'), e.message);
|
|
1294
|
+
btn.disabled = false;
|
|
1295
|
+
btn.textContent = editingKey ? t('btn.update') : t('btn.createTicket');
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
async function advanceTicket() {
|
|
1300
|
+
if (!editingKey) return;
|
|
1301
|
+
const btn = document.getElementById('btn-next');
|
|
1302
|
+
btn.disabled = true;
|
|
1303
|
+
btn.textContent = t('btn.moving');
|
|
1304
|
+
try {
|
|
1305
|
+
await fetch('/api/tickets/' + editingKey + '/next', { method: 'POST' });
|
|
1306
|
+
showNotification('info', t('notify.ticketAdvanced'), editingKey);
|
|
1307
|
+
closeModal();
|
|
1308
|
+
loadTicketsFromAPI();
|
|
1309
|
+
} catch (e) {
|
|
1310
|
+
showNotification('error', t('notify.error'), e.message);
|
|
1311
|
+
btn.disabled = false;
|
|
1312
|
+
btn.textContent = t('btn.next');
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
async function archiveTicket() {
|
|
1317
|
+
if (!editingKey) return;
|
|
1318
|
+
if (!confirm(t('confirm.archive') + ' ' + editingKey + '?')) return;
|
|
1319
|
+
|
|
1320
|
+
const btn = document.getElementById('btn-archive');
|
|
1321
|
+
btn.disabled = true;
|
|
1322
|
+
btn.textContent = t('btn.archiving');
|
|
1323
|
+
try {
|
|
1324
|
+
const lastColumn = COLUMNS[COLUMNS.length - 1];
|
|
1325
|
+
await fetch('/api/tickets/' + editingKey + '/move', {
|
|
1326
|
+
method: 'POST',
|
|
1327
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1328
|
+
body: JSON.stringify({ column: lastColumn.name, force: true })
|
|
1329
|
+
});
|
|
1330
|
+
showNotification('info', t('notify.ticketArchived'), editingKey);
|
|
1331
|
+
closeModal();
|
|
1332
|
+
loadTicketsFromAPI();
|
|
1333
|
+
} catch (e) {
|
|
1334
|
+
showNotification('error', t('notify.error'), e.message);
|
|
1335
|
+
btn.disabled = false;
|
|
1336
|
+
btn.textContent = t('btn.archive');
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Labels
|
|
1341
|
+
function addLabel(select) {
|
|
1342
|
+
const value = select.value;
|
|
1343
|
+
if (value && !selectedLabels.includes(value)) {
|
|
1344
|
+
selectedLabels.push(value);
|
|
1345
|
+
renderLabels();
|
|
1346
|
+
}
|
|
1347
|
+
select.value = '';
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function removeLabel(label) {
|
|
1351
|
+
selectedLabels = selectedLabels.filter(l => l !== label);
|
|
1352
|
+
renderLabels();
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function renderLabels() {
|
|
1356
|
+
const container = document.getElementById('selected-labels');
|
|
1357
|
+
container.innerHTML = selectedLabels.map(label =>
|
|
1358
|
+
'<span class="label-tag">' + label + '<span class="remove-label" onclick="removeLabel(\\'' + label + '\\')">×</span></span>'
|
|
1359
|
+
).join('');
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Criteria
|
|
1363
|
+
function addCriteria(value = '') {
|
|
1364
|
+
criteriaCount++;
|
|
1365
|
+
const id = criteriaCount;
|
|
1366
|
+
const container = document.getElementById('criteria-list');
|
|
1367
|
+
const div = document.createElement('div');
|
|
1368
|
+
div.className = 'criteria-item';
|
|
1369
|
+
div.id = 'criteria-' + id;
|
|
1370
|
+
div.innerHTML = '<input type="text" placeholder="Ex: L\\'utilisateur peut..." value="' + escapeHtml(value) + '">' +
|
|
1371
|
+
'<button type="button" class="btn-remove" onclick="removeCriteria(' + id + ')">×</button>';
|
|
1372
|
+
container.appendChild(div);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function removeCriteria(id) {
|
|
1376
|
+
const el = document.getElementById('criteria-' + id);
|
|
1377
|
+
if (el) el.remove();
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function getCriteria() {
|
|
1381
|
+
const items = document.querySelectorAll('.criteria-item input');
|
|
1382
|
+
return Array.from(items).map(i => i.value.trim()).filter(Boolean);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// ========================================
|
|
1386
|
+
// COMMENTS
|
|
1387
|
+
// ========================================
|
|
1388
|
+
function resetComments() {
|
|
1389
|
+
currentComments = [];
|
|
1390
|
+
document.getElementById('comments-list').innerHTML = '<div class="no-comments">' + t('modal.noComments') + '</div>';
|
|
1391
|
+
document.getElementById('comments-count').textContent = '0';
|
|
1392
|
+
document.getElementById('new-comment').value = '';
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function renderComments(comments) {
|
|
1396
|
+
currentComments = comments || [];
|
|
1397
|
+
const list = document.getElementById('comments-list');
|
|
1398
|
+
const count = document.getElementById('comments-count');
|
|
1399
|
+
count.textContent = currentComments.length;
|
|
1400
|
+
|
|
1401
|
+
if (currentComments.length === 0) {
|
|
1402
|
+
list.innerHTML = '<div class="no-comments">' + t('modal.noComments') + '</div>';
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const sorted = [...currentComments].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
1407
|
+
list.innerHTML = sorted.map(comment => {
|
|
1408
|
+
const date = new Date(comment.created_at);
|
|
1409
|
+
const dateStr = date.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
1410
|
+
return '<div class="comment">' +
|
|
1411
|
+
'<div class="comment-meta"><span class="comment-column">' + (comment.column || 'N/A') + '</span>' +
|
|
1412
|
+
'<span class="comment-date">' + dateStr + '</span></div>' +
|
|
1413
|
+
'<div class="comment-text">' + renderMarkdown(comment.text) + '</div></div>';
|
|
1414
|
+
}).join('');
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function renderMarkdown(text) {
|
|
1418
|
+
if (!text) return '';
|
|
1419
|
+
let html = escapeHtml(text);
|
|
1420
|
+
html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
|
|
1421
|
+
html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
|
|
1422
|
+
html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>');
|
|
1423
|
+
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
|
1424
|
+
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
|
1425
|
+
html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
|
1426
|
+
html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
1427
|
+
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
|
|
1428
|
+
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
|
|
1429
|
+
html = html.replace(/\\n/g, '<br>');
|
|
1430
|
+
return html;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async function addComment() {
|
|
1434
|
+
if (!editingKey) return;
|
|
1435
|
+
const textarea = document.getElementById('new-comment');
|
|
1436
|
+
const text = textarea.value.trim();
|
|
1437
|
+
if (!text) return;
|
|
1438
|
+
|
|
1439
|
+
const btn = document.querySelector('.btn-comment');
|
|
1440
|
+
btn.disabled = true;
|
|
1441
|
+
btn.textContent = t('btn.sending');
|
|
1442
|
+
|
|
1443
|
+
try {
|
|
1444
|
+
const res = await fetch('/api/tickets/' + editingKey + '/comments', {
|
|
1445
|
+
method: 'POST',
|
|
1446
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1447
|
+
body: JSON.stringify({ text })
|
|
1448
|
+
});
|
|
1449
|
+
const result = await res.json();
|
|
1450
|
+
textarea.value = '';
|
|
1451
|
+
if (result.comments) renderComments(result.comments);
|
|
1452
|
+
showNotification('success', t('notify.commentAdded'), '');
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
showNotification('error', t('notify.error'), e.message);
|
|
1455
|
+
} finally {
|
|
1456
|
+
btn.disabled = false;
|
|
1457
|
+
btn.textContent = t('btn.add');
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// ========================================
|
|
1462
|
+
// DRAG & DROP
|
|
1463
|
+
// ========================================
|
|
1464
|
+
function onDragStart(e, key, columnSlug) {
|
|
1465
|
+
draggedTicket = key;
|
|
1466
|
+
draggedFromColumn = columnSlug;
|
|
1467
|
+
e.target.classList.add('dragging');
|
|
1468
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
1469
|
+
e.dataTransfer.setData('text/plain', key);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function onDragEnd(e) {
|
|
1473
|
+
e.target.classList.remove('dragging');
|
|
1474
|
+
document.querySelectorAll('.column').forEach(c => c.classList.remove('drag-over'));
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function onDragOver(e) {
|
|
1478
|
+
e.preventDefault();
|
|
1479
|
+
e.dataTransfer.dropEffect = 'move';
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function onDragEnter(e) {
|
|
1483
|
+
e.preventDefault();
|
|
1484
|
+
const column = e.target.closest('.column');
|
|
1485
|
+
if (column) column.classList.add('drag-over');
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function onDragLeave(e) {
|
|
1489
|
+
const column = e.target.closest('.column');
|
|
1490
|
+
if (column && !column.contains(e.relatedTarget)) {
|
|
1491
|
+
column.classList.remove('drag-over');
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
async function onDrop(e, targetColumnSlug, targetColumnName) {
|
|
1496
|
+
e.preventDefault();
|
|
1497
|
+
document.querySelectorAll('.column').forEach(c => c.classList.remove('drag-over'));
|
|
1498
|
+
|
|
1499
|
+
if (!draggedTicket) return;
|
|
1500
|
+
if (draggedFromColumn === targetColumnSlug) {
|
|
1501
|
+
draggedTicket = null;
|
|
1502
|
+
draggedFromColumn = null;
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const key = draggedTicket;
|
|
1507
|
+
draggedTicket = null;
|
|
1508
|
+
draggedFromColumn = null;
|
|
1509
|
+
|
|
1510
|
+
try {
|
|
1511
|
+
await fetch('/api/tickets/' + key + '/move', {
|
|
1512
|
+
method: 'POST',
|
|
1513
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1514
|
+
body: JSON.stringify({ column: targetColumnName, force: true })
|
|
1515
|
+
});
|
|
1516
|
+
showNotification('info', key + ' ' + t('notify.ticketMoved'), t('notify.moveTo') + ' "' + targetColumnName + '"');
|
|
1517
|
+
loadTicketsFromAPI();
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
showNotification('error', t('notify.error'), err.message);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// ========================================
|
|
1524
|
+
// CONTEXT MENU
|
|
1525
|
+
// ========================================
|
|
1526
|
+
function showContextMenu(e, key) {
|
|
1527
|
+
e.preventDefault();
|
|
1528
|
+
contextMenuTicket = key;
|
|
1529
|
+
const menu = document.getElementById('context-menu');
|
|
1530
|
+
menu.style.left = e.clientX + 'px';
|
|
1531
|
+
menu.style.top = e.clientY + 'px';
|
|
1532
|
+
menu.classList.add('active');
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function hideContextMenu() {
|
|
1536
|
+
document.getElementById('context-menu').classList.remove('active');
|
|
1537
|
+
contextMenuTicket = null;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function editFromContext() {
|
|
1541
|
+
if (contextMenuTicket) openModal(contextMenuTicket);
|
|
1542
|
+
hideContextMenu();
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
async function archiveFromContext() {
|
|
1546
|
+
if (!contextMenuTicket) return;
|
|
1547
|
+
const key = contextMenuTicket;
|
|
1548
|
+
hideContextMenu();
|
|
1549
|
+
|
|
1550
|
+
if (confirm(t('confirm.archive') + ' ' + key + '?')) {
|
|
1551
|
+
try {
|
|
1552
|
+
const lastColumn = COLUMNS[COLUMNS.length - 1];
|
|
1553
|
+
await fetch('/api/tickets/' + key + '/move', {
|
|
1554
|
+
method: 'POST',
|
|
1555
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1556
|
+
body: JSON.stringify({ column: lastColumn.name, force: true })
|
|
1557
|
+
});
|
|
1558
|
+
showNotification('info', t('notify.ticketArchived'), key);
|
|
1559
|
+
loadTicketsFromAPI();
|
|
1560
|
+
} catch (err) {
|
|
1561
|
+
showNotification('error', t('notify.error'), err.message);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
document.addEventListener('click', hideContextMenu);
|
|
1567
|
+
|
|
1568
|
+
// ========================================
|
|
1569
|
+
// ACTION.md MODAL
|
|
1570
|
+
// ========================================
|
|
1571
|
+
function openActionModal(slug) {
|
|
1572
|
+
currentActionSlug = slug;
|
|
1573
|
+
const col = COLUMNS.find(c => c.slug === slug);
|
|
1574
|
+
document.getElementById('action-modal-title').textContent = col?.name || slug;
|
|
1575
|
+
document.getElementById('action-modal').classList.add('active');
|
|
1576
|
+
document.body.style.overflow = 'hidden';
|
|
1577
|
+
reloadActionContent();
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function closeActionModal() {
|
|
1581
|
+
document.getElementById('action-modal').classList.remove('active');
|
|
1582
|
+
document.body.style.overflow = '';
|
|
1583
|
+
currentActionSlug = null;
|
|
1584
|
+
originalActionContent = '';
|
|
1585
|
+
setActionEditMode(false);
|
|
1586
|
+
document.getElementById('action-content').value = '';
|
|
1587
|
+
document.getElementById('action-empty').style.display = 'none';
|
|
1588
|
+
document.getElementById('action-meta').textContent = '';
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function setActionEditMode(isEdit) {
|
|
1592
|
+
const textarea = document.getElementById('action-content');
|
|
1593
|
+
const saveBtn = document.getElementById('action-save-btn');
|
|
1594
|
+
const editBtn = document.getElementById('action-edit-btn');
|
|
1595
|
+
textarea.readOnly = !isEdit;
|
|
1596
|
+
saveBtn.disabled = !isEdit;
|
|
1597
|
+
editBtn.style.display = isEdit ? 'none' : 'inline-block';
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
async function reloadActionContent() {
|
|
1601
|
+
if (!currentActionSlug) return;
|
|
1602
|
+
setActionEditMode(false);
|
|
1603
|
+
const textarea = document.getElementById('action-content');
|
|
1604
|
+
const empty = document.getElementById('action-empty');
|
|
1605
|
+
const meta = document.getElementById('action-meta');
|
|
1606
|
+
|
|
1607
|
+
empty.style.display = 'none';
|
|
1608
|
+
textarea.style.display = 'block';
|
|
1609
|
+
textarea.value = '...';
|
|
1610
|
+
|
|
1611
|
+
try {
|
|
1612
|
+
const res = await fetch('/api/columns/' + currentActionSlug + '/actions?lang=' + currentActionLang);
|
|
1613
|
+
const data = res.ok ? await res.json() : {};
|
|
1614
|
+
|
|
1615
|
+
if (!res.ok || !data.success) {
|
|
1616
|
+
if (res.status === 404) {
|
|
1617
|
+
textarea.value = '';
|
|
1618
|
+
textarea.style.display = 'none';
|
|
1619
|
+
empty.textContent = t('action.noFile');
|
|
1620
|
+
empty.style.display = 'block';
|
|
1621
|
+
meta.textContent = '';
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
throw new Error(data.error || t('notify.loadingError'));
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const actionData = data.data || {};
|
|
1628
|
+
originalActionContent = actionData.content || '';
|
|
1629
|
+
textarea.value = originalActionContent;
|
|
1630
|
+
const updated = actionData.updated_at ? new Date(actionData.updated_at).toLocaleString(currentLang === 'fr' ? 'fr-FR' : 'en-US') : '';
|
|
1631
|
+
meta.textContent = (actionData.path || '') + (updated ? ' - ' + t('action.modifiedOn') + ' ' + updated : '');
|
|
1632
|
+
} catch (e) {
|
|
1633
|
+
empty.textContent = t('notify.error') + ': ' + e.message;
|
|
1634
|
+
empty.style.display = 'block';
|
|
1635
|
+
textarea.style.display = 'none';
|
|
1636
|
+
meta.textContent = '';
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function enterActionEdit() {
|
|
1641
|
+
if (!currentActionSlug) return;
|
|
1642
|
+
const textarea = document.getElementById('action-content');
|
|
1643
|
+
const empty = document.getElementById('action-empty');
|
|
1644
|
+
if (empty.style.display === 'block' && !textarea.value) {
|
|
1645
|
+
textarea.value = '# ' + document.getElementById('action-modal-title').textContent + '\\n\\n';
|
|
1646
|
+
empty.style.display = 'none';
|
|
1647
|
+
textarea.style.display = 'block';
|
|
1648
|
+
}
|
|
1649
|
+
setActionEditMode(true);
|
|
1650
|
+
textarea.focus();
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
async function saveActionContent() {
|
|
1654
|
+
if (!currentActionSlug) return;
|
|
1655
|
+
const btn = document.getElementById('action-save-btn');
|
|
1656
|
+
const textarea = document.getElementById('action-content');
|
|
1657
|
+
btn.disabled = true;
|
|
1658
|
+
btn.textContent = t('btn.saving');
|
|
1659
|
+
|
|
1660
|
+
try {
|
|
1661
|
+
const res = await fetch('/api/columns/' + currentActionSlug + '/actions?lang=' + currentActionLang, {
|
|
1662
|
+
method: 'POST',
|
|
1663
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1664
|
+
body: JSON.stringify({ content: textarea.value })
|
|
1665
|
+
});
|
|
1666
|
+
const data = await res.json();
|
|
1667
|
+
if (!res.ok || !data.success) throw new Error(data.error || t('notify.error'));
|
|
1668
|
+
|
|
1669
|
+
originalActionContent = textarea.value;
|
|
1670
|
+
setActionEditMode(false);
|
|
1671
|
+
showNotification('success', 'ACTION.' + currentActionLang + '.md ' + t('notify.actionUpdated'), currentActionSlug);
|
|
1672
|
+
} catch (e) {
|
|
1673
|
+
showNotification('error', t('notify.unableToSave'), e.message);
|
|
1674
|
+
} finally {
|
|
1675
|
+
btn.disabled = false;
|
|
1676
|
+
btn.textContent = t('btn.save');
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// ========================================
|
|
1681
|
+
// WEBSOCKET
|
|
1682
|
+
// ========================================
|
|
1683
|
+
let ws;
|
|
1684
|
+
|
|
1685
|
+
function connectWebSocket() {
|
|
1686
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1687
|
+
ws = new WebSocket(protocol + '//' + location.host + '/ws');
|
|
1688
|
+
|
|
1689
|
+
ws.onmessage = (event) => {
|
|
1690
|
+
try {
|
|
1691
|
+
const data = JSON.parse(event.data);
|
|
1692
|
+
switch (data.type) {
|
|
1693
|
+
case 'refresh':
|
|
1694
|
+
case 'ticket_updated':
|
|
1695
|
+
case 'ticket_created':
|
|
1696
|
+
case 'ticket_moved':
|
|
1697
|
+
loadTicketsFromAPI();
|
|
1698
|
+
break;
|
|
1699
|
+
case 'claude_start':
|
|
1700
|
+
onClaudeStart(data.ticket);
|
|
1701
|
+
break;
|
|
1702
|
+
case 'claude_stream':
|
|
1703
|
+
onClaudeStream(data.ticket);
|
|
1704
|
+
break;
|
|
1705
|
+
case 'claude_end':
|
|
1706
|
+
onClaudeEnd(data.ticket, data.success, data.duration);
|
|
1707
|
+
loadTicketsFromAPI();
|
|
1708
|
+
break;
|
|
1709
|
+
case 'claude_complete':
|
|
1710
|
+
if (data.success) {
|
|
1711
|
+
showNotification('claude', t('notify.claudeFinished') + ' ' + data.ticket, t('notify.processingSuccess'));
|
|
1712
|
+
} else {
|
|
1713
|
+
showNotification('error', t('notify.claudeFailed') + ' ' + data.ticket, t('notify.checkLogs'));
|
|
1714
|
+
}
|
|
1715
|
+
break;
|
|
1716
|
+
}
|
|
1717
|
+
} catch {}
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
ws.onclose = () => setTimeout(connectWebSocket, 2000);
|
|
1721
|
+
ws.onerror = () => ws.close();
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// ========================================
|
|
1725
|
+
// CLAUDE LOG (in modal)
|
|
1726
|
+
// ========================================
|
|
1727
|
+
let logPollingInterval = null;
|
|
1728
|
+
let claudeProcessingTicket = null;
|
|
1729
|
+
|
|
1730
|
+
function startLogPolling(key) {
|
|
1731
|
+
stopLogPolling();
|
|
1732
|
+
logPollingInterval = setInterval(() => fetchLog(key), 1000);
|
|
1733
|
+
fetchLog(key); // Immediate first fetch
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function stopLogPolling() {
|
|
1737
|
+
if (logPollingInterval) {
|
|
1738
|
+
clearInterval(logPollingInterval);
|
|
1739
|
+
logPollingInterval = null;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function resetClaudeLog() {
|
|
1744
|
+
stopLogPolling();
|
|
1745
|
+
document.getElementById('claude-log-section').style.display = 'none';
|
|
1746
|
+
document.getElementById('claude-log').textContent = '';
|
|
1747
|
+
document.getElementById('claude-log-status').className = 'claude-log-status';
|
|
1748
|
+
document.getElementById('claude-log-status').textContent = t('status.waiting');
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
async function fetchLog(key) {
|
|
1752
|
+
try {
|
|
1753
|
+
const res = await fetch('/api/tickets/' + key + '/log');
|
|
1754
|
+
const json = await res.json();
|
|
1755
|
+
if (json.success && json.data) {
|
|
1756
|
+
const section = document.getElementById('claude-log-section');
|
|
1757
|
+
const log = document.getElementById('claude-log');
|
|
1758
|
+
const status = document.getElementById('claude-log-status');
|
|
1759
|
+
|
|
1760
|
+
if (json.data.exists || json.data.content) {
|
|
1761
|
+
section.style.display = 'block';
|
|
1762
|
+
log.textContent = json.data.content || '';
|
|
1763
|
+
// Auto-scroll
|
|
1764
|
+
log.scrollTop = log.scrollHeight;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
} catch (e) {
|
|
1768
|
+
console.error('Log fetch error:', e);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function onClaudeStart(ticket) {
|
|
1773
|
+
claudeProcessingTicket = ticket;
|
|
1774
|
+
claudeProcessingTickets.add(ticket); // Track this ticket as being processed
|
|
1775
|
+
render(); // Refresh to show processing indicator
|
|
1776
|
+
|
|
1777
|
+
const status = document.getElementById('claude-log-status');
|
|
1778
|
+
if (status) {
|
|
1779
|
+
status.className = 'claude-log-status processing';
|
|
1780
|
+
status.textContent = '🤖 ' + t('status.processing');
|
|
1781
|
+
}
|
|
1782
|
+
// Start polling if modal is open for this ticket
|
|
1783
|
+
if (editingKey === ticket) {
|
|
1784
|
+
document.getElementById('claude-log-section').style.display = 'block';
|
|
1785
|
+
startLogPolling(ticket);
|
|
1786
|
+
}
|
|
1787
|
+
showNotification('claude', t('notify.claudeStarted'), ticket);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function onClaudeStream(ticket) {
|
|
1791
|
+
// Refresh log if modal is open for this ticket
|
|
1792
|
+
if (editingKey === ticket) {
|
|
1793
|
+
fetchLog(ticket);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function onClaudeEnd(ticket, success, duration) {
|
|
1798
|
+
claudeProcessingTicket = null;
|
|
1799
|
+
claudeProcessingTickets.delete(ticket); // Remove from processing set
|
|
1800
|
+
render(); // Refresh to hide processing indicator
|
|
1801
|
+
|
|
1802
|
+
const status = document.getElementById('claude-log-status');
|
|
1803
|
+
if (status && editingKey === ticket) {
|
|
1804
|
+
if (success) {
|
|
1805
|
+
status.className = 'claude-log-status success';
|
|
1806
|
+
status.textContent = '✅ ' + t('status.completed') + ' (' + (duration / 1000).toFixed(1) + 's)';
|
|
1807
|
+
} else {
|
|
1808
|
+
status.className = 'claude-log-status error';
|
|
1809
|
+
status.textContent = '❌ ' + t('status.failed');
|
|
1810
|
+
}
|
|
1811
|
+
fetchLog(ticket); // Final fetch
|
|
1812
|
+
stopLogPolling();
|
|
1813
|
+
}
|
|
1814
|
+
if (success) {
|
|
1815
|
+
showNotification('success', t('notify.claudeFinished'), ticket);
|
|
1816
|
+
} else {
|
|
1817
|
+
showNotification('error', t('notify.claudeFailed'), ticket);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// ========================================
|
|
1822
|
+
// API
|
|
1823
|
+
// ========================================
|
|
1824
|
+
async function loadTicketsFromAPI() {
|
|
1825
|
+
try {
|
|
1826
|
+
const res = await fetch('/api/tickets');
|
|
1827
|
+
const json = await res.json();
|
|
1828
|
+
if (json.success && json.data) {
|
|
1829
|
+
TICKETS.length = 0;
|
|
1830
|
+
(json.data.tickets || []).forEach(tk => TICKETS.push(tk));
|
|
1831
|
+
COLUMNS.length = 0;
|
|
1832
|
+
(json.data.columns || []).forEach(c => COLUMNS.push(c));
|
|
1833
|
+
render();
|
|
1834
|
+
}
|
|
1835
|
+
} catch (e) {
|
|
1836
|
+
console.error(t('notify.loadingError') + ':', e);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function onTicketClick(key) {
|
|
1841
|
+
openModal(key);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// ========================================
|
|
1845
|
+
// KEYBOARD
|
|
1846
|
+
// ========================================
|
|
1847
|
+
document.addEventListener('keydown', e => {
|
|
1848
|
+
if (e.key === 'Escape') {
|
|
1849
|
+
const actionModal = document.getElementById('action-modal');
|
|
1850
|
+
if (actionModal?.classList.contains('active')) {
|
|
1851
|
+
closeActionModal();
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
closeModal();
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
// ========================================
|
|
1859
|
+
// INIT
|
|
1860
|
+
// ========================================
|
|
1861
|
+
render();
|
|
1862
|
+
connectWebSocket();
|
|
1863
|
+
`;
|
|
1864
|
+
}
|
|
1865
|
+
//# sourceMappingURL=dashboard.js.map
|