claude-code-kanban 1.9.0
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 +21 -0
- package/README.md +144 -0
- package/package.json +45 -0
- package/public/index.html +3130 -0
- package/server.js +527 -0
|
@@ -0,0 +1,3130 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>CC Kanban</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Playfair+Display:wght@400;500;600&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
@font-face { font-display: swap; }
|
|
12
|
+
</style>
|
|
13
|
+
<script defer src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
14
|
+
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
|
|
15
|
+
<style>
|
|
16
|
+
:root {
|
|
17
|
+
--bg-deep: #101114;
|
|
18
|
+
--bg-surface: #16181c;
|
|
19
|
+
--bg-elevated: #1e2025;
|
|
20
|
+
--bg-hover: #282a30;
|
|
21
|
+
--border: #363840;
|
|
22
|
+
--text-primary: #f0f1f3;
|
|
23
|
+
--text-secondary: #c2c4c9;
|
|
24
|
+
--text-tertiary: #9a9da5;
|
|
25
|
+
--text-muted: #7d808a;
|
|
26
|
+
--accent: #E86F33;
|
|
27
|
+
--accent-dim: rgba(232, 111, 51, 0.22);
|
|
28
|
+
--accent-glow: rgba(232, 111, 51, 0.55);
|
|
29
|
+
--success: #3ecf8e;
|
|
30
|
+
--success-dim: rgba(62, 207, 142, 0.18);
|
|
31
|
+
--warning: #f0b429;
|
|
32
|
+
--warning-dim: rgba(240, 180, 41, 0.18);
|
|
33
|
+
--team: #60a5fa;
|
|
34
|
+
--team-dim: rgba(96, 165, 250, 0.18);
|
|
35
|
+
--mono: 'IBM Plex Mono', monospace;
|
|
36
|
+
--serif: 'Playfair Display', serif;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
40
|
+
|
|
41
|
+
body {
|
|
42
|
+
font-family: var(--mono);
|
|
43
|
+
font-size: 14px;
|
|
44
|
+
background: var(--bg-deep);
|
|
45
|
+
color: var(--text-primary);
|
|
46
|
+
line-height: 1.5;
|
|
47
|
+
min-height: 100vh;
|
|
48
|
+
-webkit-font-smoothing: antialiased;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Subtle scan-line texture */
|
|
52
|
+
body::before {
|
|
53
|
+
display: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Scrollbar */
|
|
57
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
58
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
59
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
60
|
+
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
|
61
|
+
|
|
62
|
+
/* Layout */
|
|
63
|
+
.app { display: flex; height: 100vh; }
|
|
64
|
+
|
|
65
|
+
/* Sidebar */
|
|
66
|
+
.sidebar {
|
|
67
|
+
width: 300px;
|
|
68
|
+
background: var(--bg-surface);
|
|
69
|
+
border-right: 1px solid var(--border);
|
|
70
|
+
box-shadow: 1px 0 12px rgba(0, 0, 0, 0.04);
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
flex-shrink: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.sidebar-header {
|
|
77
|
+
padding: 20px 20px 16px;
|
|
78
|
+
border-bottom: none;
|
|
79
|
+
background-image: linear-gradient(to right, transparent, var(--border), transparent);
|
|
80
|
+
background-size: 100% 1px;
|
|
81
|
+
background-repeat: no-repeat;
|
|
82
|
+
background-position: bottom;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.logo {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 10px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.logo-mark {
|
|
92
|
+
width: 24px;
|
|
93
|
+
height: 24px;
|
|
94
|
+
background: var(--accent);
|
|
95
|
+
border-radius: 6px;
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.logo-mark svg {
|
|
102
|
+
width: 14px;
|
|
103
|
+
height: 14px;
|
|
104
|
+
color: white;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.logo-text {
|
|
108
|
+
font-family: var(--serif);
|
|
109
|
+
font-size: 17px;
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
letter-spacing: -0.02em;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.connection {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 6px;
|
|
118
|
+
margin-top: 12px;
|
|
119
|
+
font-size: 12px;
|
|
120
|
+
color: var(--text-tertiary);
|
|
121
|
+
text-transform: uppercase;
|
|
122
|
+
letter-spacing: 0.05em;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.connection-dot {
|
|
126
|
+
width: 6px;
|
|
127
|
+
height: 6px;
|
|
128
|
+
border-radius: 50%;
|
|
129
|
+
background: var(--warning);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.connection-dot.live {
|
|
133
|
+
background: var(--success);
|
|
134
|
+
box-shadow: 0 0 8px var(--success);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.connection-dot.error {
|
|
138
|
+
background: #ef4444;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Sidebar sections */
|
|
142
|
+
.sidebar-section {
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-direction: column;
|
|
145
|
+
border-bottom: none;
|
|
146
|
+
background-image: linear-gradient(to right, transparent, var(--border), transparent);
|
|
147
|
+
background-size: 100% 1px;
|
|
148
|
+
background-repeat: no-repeat;
|
|
149
|
+
background-position: bottom;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.sidebar-section.flex-1 {
|
|
153
|
+
flex: 1;
|
|
154
|
+
border-bottom: none;
|
|
155
|
+
overflow: hidden;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.section-header {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
justify-content: space-between;
|
|
162
|
+
padding: 14px 20px 10px;
|
|
163
|
+
font-size: 11px;
|
|
164
|
+
font-weight: 500;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
letter-spacing: 0.12em;
|
|
167
|
+
color: var(--text-muted);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.filter-row {
|
|
171
|
+
display: flex;
|
|
172
|
+
gap: 6px;
|
|
173
|
+
padding: 0 16px 10px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.filter-dropdown {
|
|
177
|
+
flex: 1;
|
|
178
|
+
appearance: none;
|
|
179
|
+
background: var(--bg-deep);
|
|
180
|
+
border: 1px solid transparent;
|
|
181
|
+
border-radius: 6px;
|
|
182
|
+
padding: 7px 26px 7px 10px;
|
|
183
|
+
font-family: var(--mono);
|
|
184
|
+
font-size: 12px;
|
|
185
|
+
color: var(--text-secondary);
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%238b8d95' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
|
188
|
+
background-repeat: no-repeat;
|
|
189
|
+
background-position: right 8px center;
|
|
190
|
+
text-overflow: ellipsis;
|
|
191
|
+
min-width: 0;
|
|
192
|
+
transition: all 0.15s ease;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.filter-dropdown:hover {
|
|
196
|
+
border-color: var(--border);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.filter-dropdown option {
|
|
200
|
+
background: var(--bg-surface);
|
|
201
|
+
color: var(--text-primary);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.filter-dropdown:focus {
|
|
205
|
+
outline: none;
|
|
206
|
+
border-color: var(--accent);
|
|
207
|
+
box-shadow: 0 0 0 2px var(--accent-dim);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Live Updates */
|
|
211
|
+
.live-updates {
|
|
212
|
+
padding: 0 16px 12px;
|
|
213
|
+
max-height: 180px;
|
|
214
|
+
overflow-y: auto;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.live-empty {
|
|
218
|
+
padding: 16px;
|
|
219
|
+
text-align: center;
|
|
220
|
+
font-size: 11px;
|
|
221
|
+
color: var(--text-muted);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.live-item {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: flex-start;
|
|
227
|
+
gap: 10px;
|
|
228
|
+
padding: 10px 12px;
|
|
229
|
+
background: var(--bg-deep);
|
|
230
|
+
border: 1px solid transparent;
|
|
231
|
+
border-radius: 8px;
|
|
232
|
+
margin-bottom: 4px;
|
|
233
|
+
cursor: pointer;
|
|
234
|
+
transition: all 0.15s ease;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.live-item:hover {
|
|
238
|
+
background: var(--bg-hover);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.live-item .pulse {
|
|
242
|
+
width: 8px;
|
|
243
|
+
height: 8px;
|
|
244
|
+
margin-top: 4px;
|
|
245
|
+
background: var(--accent);
|
|
246
|
+
border-radius: 50%;
|
|
247
|
+
flex-shrink: 0;
|
|
248
|
+
animation: pulse 2s ease-in-out infinite;
|
|
249
|
+
box-shadow: 0 0 12px var(--accent-glow);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.live-item-content {
|
|
253
|
+
flex: 1;
|
|
254
|
+
min-width: 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.live-item-action {
|
|
258
|
+
font-size: 13px;
|
|
259
|
+
color: var(--text-primary);
|
|
260
|
+
white-space: nowrap;
|
|
261
|
+
overflow: hidden;
|
|
262
|
+
text-overflow: ellipsis;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.live-item-session {
|
|
266
|
+
font-size: 11px;
|
|
267
|
+
color: var(--text-tertiary);
|
|
268
|
+
margin-top: 2px;
|
|
269
|
+
white-space: nowrap;
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
text-overflow: ellipsis;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* Sessions */
|
|
275
|
+
.sessions-list {
|
|
276
|
+
flex: 1;
|
|
277
|
+
overflow-y: auto;
|
|
278
|
+
padding: 0 14px 12px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.session-item {
|
|
282
|
+
display: block;
|
|
283
|
+
width: 100%;
|
|
284
|
+
padding: 12px 14px;
|
|
285
|
+
margin-bottom: 2px;
|
|
286
|
+
background: transparent;
|
|
287
|
+
border: 1px solid transparent;
|
|
288
|
+
border-radius: 8px;
|
|
289
|
+
text-align: left;
|
|
290
|
+
cursor: pointer;
|
|
291
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.session-item:hover {
|
|
295
|
+
background: var(--bg-hover);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.session-item.active {
|
|
299
|
+
background: var(--bg-elevated);
|
|
300
|
+
border-color: var(--border);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.session-name {
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
justify-content: space-between;
|
|
307
|
+
gap: 8px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.session-name span {
|
|
311
|
+
font-size: 14px;
|
|
312
|
+
color: var(--text-primary);
|
|
313
|
+
white-space: nowrap;
|
|
314
|
+
overflow: hidden;
|
|
315
|
+
text-overflow: ellipsis;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.session-name .pulse {
|
|
319
|
+
width: 8px;
|
|
320
|
+
height: 8px;
|
|
321
|
+
background: var(--accent);
|
|
322
|
+
border-radius: 50%;
|
|
323
|
+
animation: pulse 2s ease-in-out infinite;
|
|
324
|
+
box-shadow: 0 0 12px var(--accent-glow);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@keyframes pulse {
|
|
328
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
329
|
+
50% { opacity: 0.7; transform: scale(0.9); }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.session-secondary {
|
|
333
|
+
font-size: 12px;
|
|
334
|
+
font-weight: 450;
|
|
335
|
+
color: var(--text-tertiary);
|
|
336
|
+
margin-top: 2px;
|
|
337
|
+
white-space: nowrap;
|
|
338
|
+
overflow: hidden;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.session-branch {
|
|
342
|
+
font-size: 10px;
|
|
343
|
+
color: var(--accent);
|
|
344
|
+
margin-top: 3px;
|
|
345
|
+
padding: 2px 6px;
|
|
346
|
+
background: var(--border);
|
|
347
|
+
border-radius: 3px;
|
|
348
|
+
display: inline-block;
|
|
349
|
+
font-family: var(--mono);
|
|
350
|
+
white-space: nowrap;
|
|
351
|
+
overflow: hidden;
|
|
352
|
+
text-overflow: ellipsis;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.session-branch {
|
|
356
|
+
font-size: 10px;
|
|
357
|
+
color: var(--accent);
|
|
358
|
+
margin-top: 3px;
|
|
359
|
+
padding: 2px 6px;
|
|
360
|
+
background: var(--border);
|
|
361
|
+
border-radius: 3px;
|
|
362
|
+
display: inline-block;
|
|
363
|
+
white-space: nowrap;
|
|
364
|
+
overflow: hidden;
|
|
365
|
+
text-overflow: ellipsis;
|
|
366
|
+
max-width: 100%;
|
|
367
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.session-progress {
|
|
371
|
+
display: flex;
|
|
372
|
+
align-items: center;
|
|
373
|
+
gap: 8px;
|
|
374
|
+
margin-top: 8px;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.progress-bar {
|
|
378
|
+
flex: 1;
|
|
379
|
+
height: 2px;
|
|
380
|
+
background: var(--border);
|
|
381
|
+
border-radius: 1px;
|
|
382
|
+
overflow: hidden;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.progress-fill {
|
|
386
|
+
height: 100%;
|
|
387
|
+
background: var(--accent);
|
|
388
|
+
transition: width 0.5s ease-out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.progress-text {
|
|
392
|
+
font-size: 11px;
|
|
393
|
+
font-weight: 500;
|
|
394
|
+
color: var(--text-tertiary);
|
|
395
|
+
font-variant-numeric: tabular-nums;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.session-time {
|
|
399
|
+
font-size: 11px;
|
|
400
|
+
font-weight: 450;
|
|
401
|
+
color: var(--text-tertiary);
|
|
402
|
+
margin-top: 6px;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* Footer */
|
|
406
|
+
.sidebar-footer {
|
|
407
|
+
padding: 14px 20px;
|
|
408
|
+
border-top: none;
|
|
409
|
+
background-image: linear-gradient(to right, transparent, var(--border), transparent);
|
|
410
|
+
background-size: 100% 1px;
|
|
411
|
+
background-repeat: no-repeat;
|
|
412
|
+
background-position: top;
|
|
413
|
+
font-size: 10px;
|
|
414
|
+
color: var(--text-muted);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.sidebar-footer a {
|
|
418
|
+
color: var(--text-tertiary);
|
|
419
|
+
text-decoration: none;
|
|
420
|
+
transition: color 0.15s;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.sidebar-footer a:hover {
|
|
424
|
+
color: var(--text-secondary);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* Main */
|
|
428
|
+
.main {
|
|
429
|
+
flex: 1;
|
|
430
|
+
display: flex;
|
|
431
|
+
flex-direction: column;
|
|
432
|
+
overflow: hidden;
|
|
433
|
+
background: var(--bg-deep);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* Empty state */
|
|
437
|
+
.empty-state {
|
|
438
|
+
flex: 1;
|
|
439
|
+
display: flex;
|
|
440
|
+
align-items: center;
|
|
441
|
+
justify-content: center;
|
|
442
|
+
flex-direction: column;
|
|
443
|
+
gap: 16px;
|
|
444
|
+
color: var(--text-muted);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.empty-state svg {
|
|
448
|
+
width: 48px;
|
|
449
|
+
height: 48px;
|
|
450
|
+
opacity: 0.5;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.empty-state p {
|
|
454
|
+
font-size: 13px;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* Session view */
|
|
458
|
+
.session-view {
|
|
459
|
+
flex: 1;
|
|
460
|
+
display: none;
|
|
461
|
+
flex-direction: column;
|
|
462
|
+
overflow: hidden;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.session-view.visible {
|
|
466
|
+
display: flex;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* Header */
|
|
470
|
+
.view-header {
|
|
471
|
+
padding: 16px 24px;
|
|
472
|
+
border-bottom: 1px solid var(--border);
|
|
473
|
+
background: var(--bg-surface);
|
|
474
|
+
display: flex;
|
|
475
|
+
align-items: center;
|
|
476
|
+
justify-content: space-between;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.view-title {
|
|
480
|
+
font-family: var(--serif);
|
|
481
|
+
font-size: 26px;
|
|
482
|
+
font-weight: 400;
|
|
483
|
+
letter-spacing: -0.03em;
|
|
484
|
+
display: flex;
|
|
485
|
+
align-items: center;
|
|
486
|
+
gap: 12px;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.view-meta {
|
|
490
|
+
font-size: 12px;
|
|
491
|
+
color: var(--text-tertiary);
|
|
492
|
+
margin-top: 4px;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.view-actions {
|
|
496
|
+
display: flex;
|
|
497
|
+
align-items: center;
|
|
498
|
+
gap: 16px;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.view-progress {
|
|
502
|
+
display: flex;
|
|
503
|
+
align-items: center;
|
|
504
|
+
gap: 10px;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.view-progress .progress-bar {
|
|
508
|
+
width: 120px;
|
|
509
|
+
height: 3px;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.view-progress .progress-text {
|
|
513
|
+
font-size: 14px;
|
|
514
|
+
font-weight: 500;
|
|
515
|
+
color: var(--accent);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.icon-btn {
|
|
519
|
+
width: 34px;
|
|
520
|
+
height: 34px;
|
|
521
|
+
display: flex;
|
|
522
|
+
align-items: center;
|
|
523
|
+
justify-content: center;
|
|
524
|
+
background: transparent;
|
|
525
|
+
border: 1px solid var(--border);
|
|
526
|
+
border-radius: 6px;
|
|
527
|
+
color: var(--text-tertiary);
|
|
528
|
+
cursor: pointer;
|
|
529
|
+
transition: all 0.15s ease;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.icon-btn:hover {
|
|
533
|
+
background: var(--bg-hover);
|
|
534
|
+
color: var(--text-primary);
|
|
535
|
+
border-color: var(--text-muted);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.icon-btn svg {
|
|
539
|
+
width: 16px;
|
|
540
|
+
height: 16px;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.icon-btn-danger {
|
|
544
|
+
color: #ef4444;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.icon-btn-danger:hover {
|
|
548
|
+
background: rgba(239, 68, 68, 0.1);
|
|
549
|
+
color: #dc2626;
|
|
550
|
+
border-color: #ef4444;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/* Kanban */
|
|
554
|
+
.kanban {
|
|
555
|
+
flex: 1;
|
|
556
|
+
display: flex;
|
|
557
|
+
gap: 24px;
|
|
558
|
+
padding: 24px;
|
|
559
|
+
overflow-x: auto;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.kanban-column {
|
|
563
|
+
flex: 1;
|
|
564
|
+
min-width: 280px;
|
|
565
|
+
max-width: 400px;
|
|
566
|
+
display: flex;
|
|
567
|
+
flex-direction: column;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.column-header {
|
|
571
|
+
display: flex;
|
|
572
|
+
align-items: center;
|
|
573
|
+
gap: 10px;
|
|
574
|
+
padding-bottom: 20px;
|
|
575
|
+
border-bottom: none;
|
|
576
|
+
margin-bottom: 16px;
|
|
577
|
+
background-image: linear-gradient(to bottom, transparent 80%, var(--border) 100%);
|
|
578
|
+
background-size: 100% 1px;
|
|
579
|
+
background-repeat: no-repeat;
|
|
580
|
+
background-position: bottom;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.column-dot {
|
|
584
|
+
width: 8px;
|
|
585
|
+
height: 8px;
|
|
586
|
+
border-radius: 50%;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.column-dot.pending { background: var(--text-tertiary); }
|
|
590
|
+
.column-dot.in-progress {
|
|
591
|
+
background: var(--accent);
|
|
592
|
+
box-shadow: 0 0 12px var(--accent-glow);
|
|
593
|
+
animation: pulse 2s ease-in-out infinite;
|
|
594
|
+
}
|
|
595
|
+
.column-dot.completed { background: var(--success); }
|
|
596
|
+
|
|
597
|
+
.column-title {
|
|
598
|
+
font-size: 14px;
|
|
599
|
+
font-weight: 500;
|
|
600
|
+
text-transform: uppercase;
|
|
601
|
+
letter-spacing: 0.06em;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.column-title.pending { color: var(--text-tertiary); }
|
|
605
|
+
.column-title.in-progress { color: var(--accent); }
|
|
606
|
+
.column-title.completed { color: var(--success); }
|
|
607
|
+
|
|
608
|
+
.column-count {
|
|
609
|
+
font-size: 11px;
|
|
610
|
+
padding: 2px 8px;
|
|
611
|
+
border-radius: 10px;
|
|
612
|
+
font-variant-numeric: tabular-nums;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.column-count.pending {
|
|
616
|
+
background: var(--bg-elevated);
|
|
617
|
+
color: var(--text-tertiary);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.column-count.in-progress {
|
|
621
|
+
background: var(--accent-dim);
|
|
622
|
+
color: var(--accent);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.column-count.completed {
|
|
626
|
+
background: var(--success-dim);
|
|
627
|
+
color: var(--success);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.column-tasks {
|
|
631
|
+
flex: 1;
|
|
632
|
+
overflow-y: auto;
|
|
633
|
+
display: flex;
|
|
634
|
+
flex-direction: column;
|
|
635
|
+
gap: 8px;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.column-empty {
|
|
639
|
+
text-align: center;
|
|
640
|
+
padding: 32px 16px;
|
|
641
|
+
color: var(--text-muted);
|
|
642
|
+
font-size: 12px;
|
|
643
|
+
border: 1px dashed var(--border);
|
|
644
|
+
border-radius: 8px;
|
|
645
|
+
margin-top: 4px;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.column-empty svg {
|
|
649
|
+
width: 24px;
|
|
650
|
+
height: 24px;
|
|
651
|
+
opacity: 0.5;
|
|
652
|
+
margin-bottom: 8px;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/* Task card */
|
|
656
|
+
.task-card {
|
|
657
|
+
padding: 16px;
|
|
658
|
+
padding-left: 18px;
|
|
659
|
+
background: var(--bg-surface);
|
|
660
|
+
border: 1px solid var(--border);
|
|
661
|
+
border-left: 2px solid var(--text-muted);
|
|
662
|
+
border-radius: 8px;
|
|
663
|
+
cursor: pointer;
|
|
664
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.task-card:hover {
|
|
668
|
+
background: var(--bg-elevated);
|
|
669
|
+
border-color: var(--text-muted);
|
|
670
|
+
border-left-color: var(--text-muted);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.task-card:focus-visible {
|
|
674
|
+
outline: 2px solid var(--accent);
|
|
675
|
+
outline-offset: 2px;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.task-card.in-progress {
|
|
679
|
+
border-color: var(--accent);
|
|
680
|
+
border-left: 2px solid var(--accent);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.task-card.in-progress:hover {
|
|
684
|
+
border-left-color: var(--accent);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.task-card.completed {
|
|
688
|
+
opacity: 0.85;
|
|
689
|
+
border-left: 2px solid var(--success);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.task-card.completed:hover {
|
|
693
|
+
border-left-color: var(--success);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.task-card.blocked {
|
|
697
|
+
opacity: 0.7;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.task-id {
|
|
701
|
+
font-size: 11px;
|
|
702
|
+
color: var(--text-muted);
|
|
703
|
+
margin-bottom: 6px;
|
|
704
|
+
display: flex;
|
|
705
|
+
align-items: center;
|
|
706
|
+
gap: 8px;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.task-badge {
|
|
710
|
+
font-size: 10px;
|
|
711
|
+
font-weight: 600;
|
|
712
|
+
padding: 3px 8px;
|
|
713
|
+
border-radius: 4px;
|
|
714
|
+
text-transform: uppercase;
|
|
715
|
+
letter-spacing: 0.04em;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.task-badge.blocked {
|
|
719
|
+
background: rgba(220, 80, 30, 0.15);
|
|
720
|
+
color: #dc4e1e;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.task-title {
|
|
724
|
+
font-size: 14px;
|
|
725
|
+
color: var(--text-primary);
|
|
726
|
+
line-height: 1.4;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.task-card.completed .task-title {
|
|
730
|
+
text-decoration: line-through;
|
|
731
|
+
color: var(--text-tertiary);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.task-session {
|
|
735
|
+
font-size: 12px;
|
|
736
|
+
color: var(--accent);
|
|
737
|
+
margin-top: 6px;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.task-active {
|
|
741
|
+
display: flex;
|
|
742
|
+
align-items: center;
|
|
743
|
+
gap: 6px;
|
|
744
|
+
margin-top: 10px;
|
|
745
|
+
padding-top: 10px;
|
|
746
|
+
border-top: 1px solid var(--border);
|
|
747
|
+
font-size: 11px;
|
|
748
|
+
font-weight: 500;
|
|
749
|
+
color: var(--accent);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.task-active::before {
|
|
753
|
+
content: '';
|
|
754
|
+
width: 6px;
|
|
755
|
+
height: 6px;
|
|
756
|
+
background: var(--accent);
|
|
757
|
+
border-radius: 50%;
|
|
758
|
+
animation: pulse 2s ease-in-out infinite;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.task-blocked {
|
|
762
|
+
font-size: 11px;
|
|
763
|
+
font-weight: 500;
|
|
764
|
+
color: var(--text-muted);
|
|
765
|
+
margin-top: 8px;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.task-desc {
|
|
769
|
+
font-size: 12px;
|
|
770
|
+
font-weight: 450;
|
|
771
|
+
color: var(--text-tertiary);
|
|
772
|
+
margin-top: 8px;
|
|
773
|
+
display: -webkit-box;
|
|
774
|
+
-webkit-line-clamp: 2;
|
|
775
|
+
-webkit-box-orient: vertical;
|
|
776
|
+
overflow: hidden;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/* Detail panel */
|
|
780
|
+
.detail-panel {
|
|
781
|
+
position: fixed;
|
|
782
|
+
top: 0;
|
|
783
|
+
right: 0;
|
|
784
|
+
width: 440px;
|
|
785
|
+
height: 100vh;
|
|
786
|
+
background: var(--bg-surface);
|
|
787
|
+
border-left: 1px solid var(--border);
|
|
788
|
+
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
|
|
789
|
+
display: flex;
|
|
790
|
+
flex-direction: column;
|
|
791
|
+
display: none;
|
|
792
|
+
z-index: 100;
|
|
793
|
+
overflow: hidden;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.detail-panel.visible {
|
|
797
|
+
display: flex;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.detail-header {
|
|
801
|
+
padding: 16px 20px;
|
|
802
|
+
border-bottom: none;
|
|
803
|
+
background-image: linear-gradient(to right, transparent, var(--border), transparent);
|
|
804
|
+
background-size: 100% 1px;
|
|
805
|
+
background-repeat: no-repeat;
|
|
806
|
+
background-position: bottom;
|
|
807
|
+
display: flex;
|
|
808
|
+
align-items: center;
|
|
809
|
+
justify-content: space-between;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.detail-header h3 {
|
|
813
|
+
font-family: var(--serif);
|
|
814
|
+
font-size: 14px;
|
|
815
|
+
font-weight: 500;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.detail-close {
|
|
819
|
+
width: 28px;
|
|
820
|
+
height: 28px;
|
|
821
|
+
display: flex;
|
|
822
|
+
align-items: center;
|
|
823
|
+
justify-content: center;
|
|
824
|
+
background: transparent;
|
|
825
|
+
border: none;
|
|
826
|
+
color: var(--text-muted);
|
|
827
|
+
cursor: pointer;
|
|
828
|
+
border-radius: 4px;
|
|
829
|
+
transition: all 0.15s;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.detail-close:hover {
|
|
833
|
+
background: var(--bg-hover);
|
|
834
|
+
color: var(--text-primary);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
.detail-close svg {
|
|
838
|
+
width: 16px;
|
|
839
|
+
height: 16px;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.detail-content {
|
|
843
|
+
flex: 1;
|
|
844
|
+
overflow-y: auto;
|
|
845
|
+
padding: 20px;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.detail-section {
|
|
849
|
+
margin-bottom: 16px;
|
|
850
|
+
padding-bottom: 16px;
|
|
851
|
+
border-bottom: none;
|
|
852
|
+
background-image: linear-gradient(to right, transparent, var(--border), transparent);
|
|
853
|
+
background-size: 100% 1px;
|
|
854
|
+
background-repeat: no-repeat;
|
|
855
|
+
background-position: bottom;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.detail-section:last-child {
|
|
859
|
+
background-image: none;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.detail-label {
|
|
863
|
+
font-size: 11px;
|
|
864
|
+
font-weight: 500;
|
|
865
|
+
text-transform: uppercase;
|
|
866
|
+
letter-spacing: 0.08em;
|
|
867
|
+
color: var(--text-muted);
|
|
868
|
+
margin-bottom: 8px;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.detail-title {
|
|
872
|
+
font-family: var(--serif);
|
|
873
|
+
font-size: 22px;
|
|
874
|
+
line-height: 1.4;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.detail-status {
|
|
878
|
+
display: inline-flex;
|
|
879
|
+
align-items: center;
|
|
880
|
+
gap: 6px;
|
|
881
|
+
font-size: 11px;
|
|
882
|
+
font-weight: 600;
|
|
883
|
+
padding: 4px 10px;
|
|
884
|
+
border-radius: 20px;
|
|
885
|
+
letter-spacing: 0.03em;
|
|
886
|
+
text-transform: uppercase;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.detail-status.pending {
|
|
890
|
+
background: var(--bg-elevated);
|
|
891
|
+
color: var(--text-muted);
|
|
892
|
+
border: 1px solid var(--border);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.detail-status.in_progress {
|
|
896
|
+
background: var(--accent-dim);
|
|
897
|
+
color: var(--accent);
|
|
898
|
+
border: 1px solid var(--accent);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.detail-status.completed {
|
|
902
|
+
background: var(--success-dim);
|
|
903
|
+
color: var(--success);
|
|
904
|
+
border: 1px solid var(--success);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.detail-status .dot {
|
|
908
|
+
width: 8px;
|
|
909
|
+
height: 8px;
|
|
910
|
+
border-radius: 50%;
|
|
911
|
+
background: currentColor;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.detail-status.in_progress .dot {
|
|
915
|
+
animation: pulse 2s ease-in-out infinite;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.detail-box {
|
|
919
|
+
padding: 12px;
|
|
920
|
+
border-radius: 6px;
|
|
921
|
+
font-size: 12px;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.detail-box.active {
|
|
925
|
+
background: rgba(232, 111, 51, 0.08);
|
|
926
|
+
border: 1px solid rgba(232, 111, 51, 0.2);
|
|
927
|
+
color: var(--text-primary);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.detail-box.active strong {
|
|
931
|
+
color: var(--accent);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.detail-box.blocked {
|
|
935
|
+
background: var(--warning-dim);
|
|
936
|
+
border: 1px solid rgba(240, 180, 41, 0.35);
|
|
937
|
+
color: var(--warning);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
.detail-box.blocks {
|
|
941
|
+
background: var(--team-dim);
|
|
942
|
+
border: 1px solid rgba(96, 165, 250, 0.35);
|
|
943
|
+
color: var(--team);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.detail-desc {
|
|
947
|
+
font-size: 14px;
|
|
948
|
+
line-height: 1.7;
|
|
949
|
+
color: var(--text-secondary);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.detail-desc pre {
|
|
953
|
+
background: var(--bg-elevated);
|
|
954
|
+
padding: 12px;
|
|
955
|
+
border-radius: 6px;
|
|
956
|
+
overflow-x: auto;
|
|
957
|
+
margin: 12px 0;
|
|
958
|
+
font-size: 12px;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.detail-desc code {
|
|
962
|
+
background: var(--bg-elevated);
|
|
963
|
+
padding: 2px 6px;
|
|
964
|
+
border-radius: 3px;
|
|
965
|
+
font-size: 0.9em;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
.detail-desc pre code {
|
|
969
|
+
background: transparent;
|
|
970
|
+
padding: 0;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.detail-desc hr {
|
|
974
|
+
border: none;
|
|
975
|
+
border-top: 1px solid var(--border);
|
|
976
|
+
margin: 16px 0;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.detail-desc h4 {
|
|
980
|
+
font-size: 10px;
|
|
981
|
+
font-weight: 600;
|
|
982
|
+
text-transform: uppercase;
|
|
983
|
+
letter-spacing: 0.05em;
|
|
984
|
+
color: var(--accent);
|
|
985
|
+
margin: 0 0 8px 0;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.detail-desc p {
|
|
989
|
+
margin: 0 0 12px 0;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.detail-desc p:last-child {
|
|
993
|
+
margin-bottom: 0;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/* Note form */
|
|
997
|
+
.note-section {
|
|
998
|
+
margin-top: 24px;
|
|
999
|
+
padding-top: 20px;
|
|
1000
|
+
border-top: 1px solid var(--border);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.note-form {
|
|
1004
|
+
display: flex;
|
|
1005
|
+
flex-direction: column;
|
|
1006
|
+
gap: 10px;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.note-input {
|
|
1010
|
+
width: 100%;
|
|
1011
|
+
padding: 10px 12px;
|
|
1012
|
+
background: var(--bg-elevated);
|
|
1013
|
+
border: 1px solid var(--border);
|
|
1014
|
+
border-radius: 6px;
|
|
1015
|
+
color: var(--text-primary);
|
|
1016
|
+
font-family: var(--mono);
|
|
1017
|
+
font-size: 12px;
|
|
1018
|
+
line-height: 1.5;
|
|
1019
|
+
resize: vertical;
|
|
1020
|
+
min-height: 60px;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.note-input:focus {
|
|
1024
|
+
outline: none;
|
|
1025
|
+
border-color: var(--accent);
|
|
1026
|
+
box-shadow: 0 0 0 2px var(--accent-dim);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.note-input::placeholder {
|
|
1030
|
+
color: var(--text-muted);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.note-submit {
|
|
1034
|
+
align-self: flex-end;
|
|
1035
|
+
padding: 8px 16px;
|
|
1036
|
+
background: var(--accent);
|
|
1037
|
+
border: none;
|
|
1038
|
+
border-radius: 5px;
|
|
1039
|
+
color: white;
|
|
1040
|
+
font-family: var(--mono);
|
|
1041
|
+
font-size: 11px;
|
|
1042
|
+
font-weight: 500;
|
|
1043
|
+
cursor: pointer;
|
|
1044
|
+
transition: all 0.15s ease;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.note-submit:hover {
|
|
1048
|
+
filter: brightness(1.1);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/* Team badge */
|
|
1052
|
+
.session-indicators {
|
|
1053
|
+
display: flex;
|
|
1054
|
+
align-items: center;
|
|
1055
|
+
gap: 6px;
|
|
1056
|
+
flex-shrink: 0;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.team-badge {
|
|
1060
|
+
display: inline-flex;
|
|
1061
|
+
align-items: center;
|
|
1062
|
+
gap: 3px;
|
|
1063
|
+
font-size: 11px;
|
|
1064
|
+
font-weight: 500;
|
|
1065
|
+
color: var(--text-muted);
|
|
1066
|
+
flex-shrink: 0;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.team-badge .member-count {
|
|
1070
|
+
font-weight: 600;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
.team-info-btn {
|
|
1074
|
+
width: 24px;
|
|
1075
|
+
height: 24px;
|
|
1076
|
+
display: inline-flex;
|
|
1077
|
+
align-items: center;
|
|
1078
|
+
justify-content: center;
|
|
1079
|
+
font-size: 12px;
|
|
1080
|
+
background: var(--bg-deep);
|
|
1081
|
+
border: 1px solid transparent;
|
|
1082
|
+
border-radius: 4px;
|
|
1083
|
+
color: var(--team);
|
|
1084
|
+
cursor: pointer;
|
|
1085
|
+
font-size: 11px;
|
|
1086
|
+
flex-shrink: 0;
|
|
1087
|
+
transition: all 0.15s ease;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.team-info-btn:hover {
|
|
1091
|
+
background: var(--team-dim);
|
|
1092
|
+
border-color: var(--team);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/* Task owner badge */
|
|
1096
|
+
.task-owner-badge {
|
|
1097
|
+
display: inline-flex;
|
|
1098
|
+
align-items: center;
|
|
1099
|
+
gap: 4px;
|
|
1100
|
+
font-size: 10px;
|
|
1101
|
+
font-weight: 500;
|
|
1102
|
+
padding: 3px 8px;
|
|
1103
|
+
border-radius: 4px;
|
|
1104
|
+
text-transform: none;
|
|
1105
|
+
letter-spacing: 0;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/* Team modal member card */
|
|
1109
|
+
.team-member-card {
|
|
1110
|
+
padding: 12px;
|
|
1111
|
+
background: var(--bg-elevated);
|
|
1112
|
+
border: 1px solid var(--border);
|
|
1113
|
+
border-radius: 8px;
|
|
1114
|
+
margin-bottom: 8px;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
.team-member-card .member-name {
|
|
1118
|
+
font-size: 13px;
|
|
1119
|
+
font-weight: 500;
|
|
1120
|
+
color: var(--text-primary);
|
|
1121
|
+
display: flex;
|
|
1122
|
+
align-items: center;
|
|
1123
|
+
gap: 6px;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.team-member-card .member-detail {
|
|
1127
|
+
font-size: 11px;
|
|
1128
|
+
color: var(--text-tertiary);
|
|
1129
|
+
margin-top: 4px;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
.team-member-card .member-tasks {
|
|
1133
|
+
font-size: 11px;
|
|
1134
|
+
color: var(--accent);
|
|
1135
|
+
margin-top: 4px;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
.team-modal-desc {
|
|
1139
|
+
font-size: 12px;
|
|
1140
|
+
color: var(--text-secondary);
|
|
1141
|
+
font-style: italic;
|
|
1142
|
+
margin-bottom: 16px;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.team-modal-meta {
|
|
1146
|
+
font-size: 11px;
|
|
1147
|
+
color: var(--text-muted);
|
|
1148
|
+
margin-top: 16px;
|
|
1149
|
+
padding-top: 12px;
|
|
1150
|
+
border-top: 1px solid var(--border);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/* Owner filter — overlaid, zero layout impact */
|
|
1154
|
+
.kanban {
|
|
1155
|
+
position: relative;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
.owner-filter-bar {
|
|
1159
|
+
display: none;
|
|
1160
|
+
position: absolute;
|
|
1161
|
+
top: 24px;
|
|
1162
|
+
right: 24px;
|
|
1163
|
+
z-index: 2;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.owner-filter-bar.visible {
|
|
1167
|
+
display: flex;
|
|
1168
|
+
align-items: center;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.owner-filter-bar .filter-dropdown {
|
|
1172
|
+
flex: none;
|
|
1173
|
+
width: auto;
|
|
1174
|
+
max-width: 180px;
|
|
1175
|
+
font-size: 13px;
|
|
1176
|
+
padding: 6px 10px;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/* Light mode */
|
|
1180
|
+
body.light {
|
|
1181
|
+
--bg-deep: #e8e6e3;
|
|
1182
|
+
--bg-surface: #f4f3f1;
|
|
1183
|
+
--bg-elevated: #dddbd8;
|
|
1184
|
+
--bg-hover: #d2d0cc;
|
|
1185
|
+
--border: #a09b94;
|
|
1186
|
+
--text-primary: #0a0a0a;
|
|
1187
|
+
--text-secondary: #444444;
|
|
1188
|
+
--text-tertiary: #666666;
|
|
1189
|
+
--text-muted: #888888;
|
|
1190
|
+
--accent-dim: rgba(232, 111, 51, 0.18);
|
|
1191
|
+
--accent-glow: rgba(232, 111, 51, 0.5);
|
|
1192
|
+
--success: #1a8a5a;
|
|
1193
|
+
--success-dim: rgba(26, 138, 90, 0.15);
|
|
1194
|
+
--warning: #b07d0a;
|
|
1195
|
+
--warning-dim: rgba(176, 125, 10, 0.15);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
body.light::before {
|
|
1199
|
+
display: none;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/* Interactive elements */
|
|
1203
|
+
.icon-btn.delete:hover {
|
|
1204
|
+
background: rgba(239, 68, 68, 0.1);
|
|
1205
|
+
border-color: #ef4444;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.column-header {
|
|
1209
|
+
display: flex;
|
|
1210
|
+
align-items: center;
|
|
1211
|
+
gap: 10px;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.column-header .icon-btn {
|
|
1215
|
+
width: 28px;
|
|
1216
|
+
height: 28px;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
.column-header .icon-btn svg {
|
|
1220
|
+
width: 14px;
|
|
1221
|
+
height: 14px;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/* Search input */
|
|
1225
|
+
.search-container {
|
|
1226
|
+
position: relative;
|
|
1227
|
+
padding: 0 16px 8px;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.search-input {
|
|
1231
|
+
width: 100%;
|
|
1232
|
+
padding: 9px 32px 9px 12px;
|
|
1233
|
+
background: var(--bg-deep);
|
|
1234
|
+
border: 1px solid transparent;
|
|
1235
|
+
border-radius: 6px;
|
|
1236
|
+
color: var(--text-primary);
|
|
1237
|
+
font-family: var(--mono);
|
|
1238
|
+
font-size: 13px;
|
|
1239
|
+
transition: all 0.15s ease;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
.search-input:hover {
|
|
1243
|
+
border-color: var(--border);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
.search-input:focus {
|
|
1247
|
+
outline: none;
|
|
1248
|
+
border-color: var(--accent);
|
|
1249
|
+
box-shadow: 0 0 0 2px var(--accent-dim);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.search-input::placeholder {
|
|
1253
|
+
color: var(--text-muted);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
.search-clear {
|
|
1257
|
+
position: absolute;
|
|
1258
|
+
right: 18px;
|
|
1259
|
+
top: 8px;
|
|
1260
|
+
width: 20px;
|
|
1261
|
+
height: 20px;
|
|
1262
|
+
background: none;
|
|
1263
|
+
border: none;
|
|
1264
|
+
color: var(--text-tertiary);
|
|
1265
|
+
cursor: pointer;
|
|
1266
|
+
display: none;
|
|
1267
|
+
align-items: center;
|
|
1268
|
+
justify-content: center;
|
|
1269
|
+
padding: 0;
|
|
1270
|
+
border-radius: 3px;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
.search-clear:hover {
|
|
1274
|
+
background: var(--bg-hover);
|
|
1275
|
+
color: var(--text-primary);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
.search-clear.visible {
|
|
1279
|
+
display: flex;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/* Modal */
|
|
1283
|
+
.modal-overlay {
|
|
1284
|
+
position: fixed;
|
|
1285
|
+
inset: 0;
|
|
1286
|
+
background: rgba(0, 0, 0, 0.5);
|
|
1287
|
+
display: none;
|
|
1288
|
+
align-items: center;
|
|
1289
|
+
justify-content: center;
|
|
1290
|
+
z-index: 10000;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.modal-overlay.visible {
|
|
1294
|
+
display: flex;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.modal {
|
|
1298
|
+
background: var(--bg-surface);
|
|
1299
|
+
border: 1px solid var(--border);
|
|
1300
|
+
border-radius: 12px;
|
|
1301
|
+
width: 90%;
|
|
1302
|
+
max-width: 500px;
|
|
1303
|
+
padding: 24px;
|
|
1304
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
.modal-header {
|
|
1308
|
+
display: flex;
|
|
1309
|
+
align-items: center;
|
|
1310
|
+
justify-content: space-between;
|
|
1311
|
+
margin-bottom: 20px;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
.modal-title {
|
|
1315
|
+
font-size: 18px;
|
|
1316
|
+
font-weight: 600;
|
|
1317
|
+
color: var(--text-primary);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.modal-close {
|
|
1321
|
+
width: 32px;
|
|
1322
|
+
height: 32px;
|
|
1323
|
+
border: none;
|
|
1324
|
+
background: none;
|
|
1325
|
+
color: var(--text-secondary);
|
|
1326
|
+
cursor: pointer;
|
|
1327
|
+
display: flex;
|
|
1328
|
+
align-items: center;
|
|
1329
|
+
justify-content: center;
|
|
1330
|
+
border-radius: 6px;
|
|
1331
|
+
transition: all 0.15s ease;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
.modal-close:hover {
|
|
1335
|
+
background: var(--bg-hover);
|
|
1336
|
+
color: var(--text-primary);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
.modal-body {
|
|
1340
|
+
margin-bottom: 24px;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
.form-group {
|
|
1344
|
+
margin-bottom: 16px;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
.form-group:last-child {
|
|
1348
|
+
margin-bottom: 0;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
.form-label {
|
|
1352
|
+
display: block;
|
|
1353
|
+
font-size: 12px;
|
|
1354
|
+
font-weight: 500;
|
|
1355
|
+
color: var(--text-secondary);
|
|
1356
|
+
margin-bottom: 6px;
|
|
1357
|
+
text-transform: uppercase;
|
|
1358
|
+
letter-spacing: 0.5px;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.form-input {
|
|
1362
|
+
width: 100%;
|
|
1363
|
+
padding: 10px 12px;
|
|
1364
|
+
background: var(--bg-elevated);
|
|
1365
|
+
border: 1px solid var(--border);
|
|
1366
|
+
border-radius: 6px;
|
|
1367
|
+
color: var(--text-primary);
|
|
1368
|
+
font-family: var(--mono);
|
|
1369
|
+
font-size: 14px;
|
|
1370
|
+
transition: all 0.15s ease;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
.form-input:focus {
|
|
1374
|
+
outline: none;
|
|
1375
|
+
border-color: var(--accent);
|
|
1376
|
+
box-shadow: 0 0 0 3px var(--accent-dim);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
.form-input::placeholder {
|
|
1380
|
+
color: var(--text-muted);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
textarea.form-input {
|
|
1384
|
+
resize: vertical;
|
|
1385
|
+
min-height: 80px;
|
|
1386
|
+
font-family: var(--mono);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
select.form-input[multiple] {
|
|
1390
|
+
padding: 4px;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
select.form-input optgroup {
|
|
1394
|
+
background: var(--bg-surface);
|
|
1395
|
+
color: var(--accent);
|
|
1396
|
+
font-weight: 600;
|
|
1397
|
+
font-size: 11px;
|
|
1398
|
+
text-transform: uppercase;
|
|
1399
|
+
letter-spacing: 0.05em;
|
|
1400
|
+
padding: 8px 8px 4px 8px;
|
|
1401
|
+
margin-top: 4px;
|
|
1402
|
+
font-style: normal;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
select.form-input optgroup:first-child {
|
|
1406
|
+
margin-top: 0;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
select.form-input option {
|
|
1410
|
+
padding: 8px 8px 8px 16px;
|
|
1411
|
+
background: var(--bg-elevated);
|
|
1412
|
+
color: var(--text-primary);
|
|
1413
|
+
font-weight: 400;
|
|
1414
|
+
font-size: 13px;
|
|
1415
|
+
border-radius: 4px;
|
|
1416
|
+
margin: 2px 4px;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
select.form-input option:checked {
|
|
1420
|
+
background: var(--accent);
|
|
1421
|
+
color: var(--bg-deep);
|
|
1422
|
+
font-weight: 500;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
.modal-footer {
|
|
1426
|
+
display: flex;
|
|
1427
|
+
gap: 12px;
|
|
1428
|
+
justify-content: flex-end;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
.btn {
|
|
1432
|
+
padding: 10px 20px;
|
|
1433
|
+
border: none;
|
|
1434
|
+
border-radius: 6px;
|
|
1435
|
+
font-family: var(--mono);
|
|
1436
|
+
font-size: 13px;
|
|
1437
|
+
font-weight: 500;
|
|
1438
|
+
cursor: pointer;
|
|
1439
|
+
transition: all 0.15s ease;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
.btn-primary {
|
|
1443
|
+
background: var(--accent);
|
|
1444
|
+
color: white;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
.btn-primary:hover {
|
|
1448
|
+
background: #d96329;
|
|
1449
|
+
transform: translateY(-1px);
|
|
1450
|
+
box-shadow: 0 4px 12px var(--accent-glow);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.btn-secondary {
|
|
1454
|
+
background: var(--bg-elevated);
|
|
1455
|
+
color: var(--text-primary);
|
|
1456
|
+
border: 1px solid var(--border);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
.btn-secondary:hover {
|
|
1460
|
+
background: var(--bg-hover);
|
|
1461
|
+
border-color: var(--text-muted);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/* Skip navigation */
|
|
1465
|
+
.skip-link {
|
|
1466
|
+
position: absolute;
|
|
1467
|
+
top: -100%;
|
|
1468
|
+
left: 16px;
|
|
1469
|
+
z-index: 100000;
|
|
1470
|
+
padding: 8px 16px;
|
|
1471
|
+
background: var(--accent);
|
|
1472
|
+
color: white;
|
|
1473
|
+
font-size: 13px;
|
|
1474
|
+
border-radius: 0 0 6px 6px;
|
|
1475
|
+
text-decoration: none;
|
|
1476
|
+
transition: top 0.2s;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
.skip-link:focus {
|
|
1480
|
+
top: 0;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/* Visually hidden (a11y) */
|
|
1484
|
+
.sr-only {
|
|
1485
|
+
position: absolute;
|
|
1486
|
+
width: 1px;
|
|
1487
|
+
height: 1px;
|
|
1488
|
+
padding: 0;
|
|
1489
|
+
margin: -1px;
|
|
1490
|
+
overflow: hidden;
|
|
1491
|
+
clip: rect(0, 0, 0, 0);
|
|
1492
|
+
white-space: nowrap;
|
|
1493
|
+
border: 0;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/* System theme detection */
|
|
1497
|
+
@media (prefers-color-scheme: light) {
|
|
1498
|
+
body:not(.dark-forced) {
|
|
1499
|
+
--bg-deep: #e8e6e3;
|
|
1500
|
+
--bg-surface: #f4f3f1;
|
|
1501
|
+
--bg-elevated: #dddbd8;
|
|
1502
|
+
--bg-hover: #d2d0cc;
|
|
1503
|
+
--border: #a09b94;
|
|
1504
|
+
--text-primary: #0a0a0a;
|
|
1505
|
+
--text-secondary: #444444;
|
|
1506
|
+
--text-tertiary: #666666;
|
|
1507
|
+
--text-muted: #888888;
|
|
1508
|
+
--accent-dim: rgba(232, 111, 51, 0.18);
|
|
1509
|
+
--accent-glow: rgba(232, 111, 51, 0.5);
|
|
1510
|
+
--success: #1a8a5a;
|
|
1511
|
+
--success-dim: rgba(26, 138, 90, 0.15);
|
|
1512
|
+
--warning: #b07d0a;
|
|
1513
|
+
--warning-dim: rgba(176, 125, 10, 0.15);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
body:not(.dark-forced)::before {
|
|
1517
|
+
display: none;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/* Card entrance animation */
|
|
1522
|
+
@keyframes fadeSlideIn {
|
|
1523
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
1524
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
.column-tasks .task-card {
|
|
1528
|
+
animation: fadeSlideIn 150ms ease-out both;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
.column-tasks .task-card:nth-child(1) { animation-delay: 0ms; }
|
|
1532
|
+
.column-tasks .task-card:nth-child(2) { animation-delay: 30ms; }
|
|
1533
|
+
.column-tasks .task-card:nth-child(3) { animation-delay: 60ms; }
|
|
1534
|
+
.column-tasks .task-card:nth-child(4) { animation-delay: 90ms; }
|
|
1535
|
+
.column-tasks .task-card:nth-child(5) { animation-delay: 120ms; }
|
|
1536
|
+
.column-tasks .task-card:nth-child(6) { animation-delay: 150ms; }
|
|
1537
|
+
.column-tasks .task-card:nth-child(7) { animation-delay: 180ms; }
|
|
1538
|
+
.column-tasks .task-card:nth-child(8) { animation-delay: 210ms; }
|
|
1539
|
+
.column-tasks .task-card:nth-child(9) { animation-delay: 240ms; }
|
|
1540
|
+
.column-tasks .task-card:nth-child(10) { animation-delay: 270ms; }
|
|
1541
|
+
|
|
1542
|
+
/* Connection status breathing */
|
|
1543
|
+
@keyframes breathe {
|
|
1544
|
+
0%, 100% { opacity: 1; }
|
|
1545
|
+
50% { opacity: 0.65; }
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
.connection-dot.live {
|
|
1549
|
+
animation: breathe 3s ease-in-out infinite;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/* Progress bar shimmer */
|
|
1553
|
+
@keyframes shimmer {
|
|
1554
|
+
0% { background-position: -200% 0; }
|
|
1555
|
+
100% { background-position: 200% 0; }
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
.progress-fill.shimmer {
|
|
1559
|
+
background: linear-gradient(90deg, var(--accent) 0%, rgba(232,111,51,0.75) 50%, var(--accent) 100%);
|
|
1560
|
+
background-size: 200% 100%;
|
|
1561
|
+
animation: shimmer 2s linear infinite;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/* Session list hover accent bar */
|
|
1565
|
+
.session-item {
|
|
1566
|
+
position: relative;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
.session-item::before {
|
|
1570
|
+
content: '';
|
|
1571
|
+
position: absolute;
|
|
1572
|
+
left: 0;
|
|
1573
|
+
top: 50%;
|
|
1574
|
+
transform: translateY(-50%);
|
|
1575
|
+
width: 0;
|
|
1576
|
+
height: 60%;
|
|
1577
|
+
background: var(--accent);
|
|
1578
|
+
border-radius: 0 2px 2px 0;
|
|
1579
|
+
transition: width 0.15s ease;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
.session-item:hover::before {
|
|
1583
|
+
width: 2px;
|
|
1584
|
+
}
|
|
1585
|
+
</style>
|
|
1586
|
+
</head>
|
|
1587
|
+
<body>
|
|
1588
|
+
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
1589
|
+
<div class="app">
|
|
1590
|
+
<!-- Sidebar -->
|
|
1591
|
+
<aside class="sidebar">
|
|
1592
|
+
<header class="sidebar-header">
|
|
1593
|
+
<div class="logo">
|
|
1594
|
+
<div class="logo-mark">
|
|
1595
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
1596
|
+
<path d="M5 13l4 4L19 7"/>
|
|
1597
|
+
</svg>
|
|
1598
|
+
</div>
|
|
1599
|
+
<span class="logo-text">Claude Tasks</span>
|
|
1600
|
+
</div>
|
|
1601
|
+
<div id="connection-status" class="connection">
|
|
1602
|
+
<span class="connection-dot"></span>
|
|
1603
|
+
<span>Connecting</span>
|
|
1604
|
+
</div>
|
|
1605
|
+
</header>
|
|
1606
|
+
|
|
1607
|
+
<!-- Live Updates -->
|
|
1608
|
+
<div class="sidebar-section">
|
|
1609
|
+
<div class="section-header">
|
|
1610
|
+
<span>Live Updates</span>
|
|
1611
|
+
</div>
|
|
1612
|
+
<div id="live-updates" class="live-updates">
|
|
1613
|
+
<div class="live-empty">No active tasks</div>
|
|
1614
|
+
</div>
|
|
1615
|
+
</div>
|
|
1616
|
+
|
|
1617
|
+
<!-- Tasks -->
|
|
1618
|
+
<div class="sidebar-section flex-1">
|
|
1619
|
+
<div class="section-header">
|
|
1620
|
+
<span>Sessions</span>
|
|
1621
|
+
<button onclick="showAllTasks()" style="background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px; padding: 3px 8px; font-size: 10px; color: var(--text-secondary); cursor: pointer; font-family: var(--mono);">All Tasks</button>
|
|
1622
|
+
</div>
|
|
1623
|
+
<div class="search-container">
|
|
1624
|
+
<input
|
|
1625
|
+
id="search-input"
|
|
1626
|
+
type="text"
|
|
1627
|
+
class="search-input"
|
|
1628
|
+
placeholder="Search tasks, sessions, projects..."
|
|
1629
|
+
oninput="handleSearch(this.value)"
|
|
1630
|
+
/>
|
|
1631
|
+
<button id="search-clear-btn" class="search-clear" onclick="clearSearch()" title="Clear search" aria-label="Clear search">
|
|
1632
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1633
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
1634
|
+
</svg>
|
|
1635
|
+
</button>
|
|
1636
|
+
</div>
|
|
1637
|
+
<div class="filter-row">
|
|
1638
|
+
<select id="project-filter" class="filter-dropdown" onchange="filterByProject(this.value)" aria-label="Filter by project">
|
|
1639
|
+
<option value="">All Projects</option>
|
|
1640
|
+
</select>
|
|
1641
|
+
<select id="session-filter" class="filter-dropdown" onchange="filterBySessions(this.value)" aria-label="Filter by session status">
|
|
1642
|
+
<option value="all">All Sessions</option>
|
|
1643
|
+
<option value="active">Active Only</option>
|
|
1644
|
+
</select>
|
|
1645
|
+
</div>
|
|
1646
|
+
<div class="filter-row">
|
|
1647
|
+
<select id="session-limit" class="filter-dropdown" onchange="changeSessionLimit(this.value)" aria-label="Number of sessions to show">
|
|
1648
|
+
<option value="10">Show 10</option>
|
|
1649
|
+
<option value="20">Show 20</option>
|
|
1650
|
+
<option value="50">Show 50</option>
|
|
1651
|
+
<option value="all">Show All</option>
|
|
1652
|
+
</select>
|
|
1653
|
+
</div>
|
|
1654
|
+
<div id="sessions-list" class="sessions-list"></div>
|
|
1655
|
+
</div>
|
|
1656
|
+
|
|
1657
|
+
</aside>
|
|
1658
|
+
|
|
1659
|
+
<!-- Main -->
|
|
1660
|
+
<main class="main">
|
|
1661
|
+
<div id="no-session" class="empty-state">
|
|
1662
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
1663
|
+
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
|
1664
|
+
</svg>
|
|
1665
|
+
<p>Select a session to view tasks</p>
|
|
1666
|
+
</div>
|
|
1667
|
+
|
|
1668
|
+
<div id="session-view" class="session-view">
|
|
1669
|
+
<header class="view-header">
|
|
1670
|
+
<div>
|
|
1671
|
+
<h1 id="session-title" class="view-title">Session</h1>
|
|
1672
|
+
<p id="session-meta" class="view-meta"></p>
|
|
1673
|
+
</div>
|
|
1674
|
+
<div class="view-actions">
|
|
1675
|
+
<div class="view-progress">
|
|
1676
|
+
<div class="progress-bar">
|
|
1677
|
+
<div id="progress-bar" class="progress-fill" style="width: 0%"></div>
|
|
1678
|
+
</div>
|
|
1679
|
+
<span id="progress-percent" class="progress-text">0%</span>
|
|
1680
|
+
</div>
|
|
1681
|
+
<button id="theme-toggle" class="icon-btn" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
|
|
1682
|
+
<svg id="theme-icon-dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1683
|
+
<path d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
|
|
1684
|
+
</svg>
|
|
1685
|
+
<svg id="theme-icon-light" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none">
|
|
1686
|
+
<circle cx="12" cy="12" r="5"/>
|
|
1687
|
+
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
|
1688
|
+
</svg>
|
|
1689
|
+
</button>
|
|
1690
|
+
<button class="icon-btn" onclick="showHelpModal()" title="Keyboard shortcuts (?)" aria-label="Keyboard shortcuts">
|
|
1691
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1692
|
+
<circle cx="12" cy="12" r="10"/>
|
|
1693
|
+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
|
1694
|
+
<circle cx="12" cy="17" r="0.5" fill="currentColor"/>
|
|
1695
|
+
</svg>
|
|
1696
|
+
</button>
|
|
1697
|
+
<a href="https://github.com/NikiforovAll/claude-task-viewer" target="_blank" class="icon-btn" title="View on GitHub" aria-label="View on GitHub">
|
|
1698
|
+
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
|
|
1699
|
+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
|
|
1700
|
+
</svg>
|
|
1701
|
+
</a>
|
|
1702
|
+
</div>
|
|
1703
|
+
</header>
|
|
1704
|
+
|
|
1705
|
+
<div class="kanban" id="main-content">
|
|
1706
|
+
<div id="owner-filter-bar" class="owner-filter-bar">
|
|
1707
|
+
<select id="owner-filter" class="filter-dropdown" onchange="filterByOwner(this.value)" aria-label="Filter by team member">
|
|
1708
|
+
<option value="">All Members</option>
|
|
1709
|
+
</select>
|
|
1710
|
+
</div>
|
|
1711
|
+
<div class="kanban-column" aria-label="Pending tasks">
|
|
1712
|
+
<div class="column-header">
|
|
1713
|
+
<span class="column-dot pending"></span>
|
|
1714
|
+
<span class="column-title pending">Pending</span>
|
|
1715
|
+
<span id="pending-count" class="column-count pending">0</span>
|
|
1716
|
+
</div>
|
|
1717
|
+
<div id="pending-tasks" class="column-tasks" role="list"></div>
|
|
1718
|
+
</div>
|
|
1719
|
+
|
|
1720
|
+
<div class="kanban-column" aria-label="In progress tasks">
|
|
1721
|
+
<div class="column-header">
|
|
1722
|
+
<span class="column-dot in-progress"></span>
|
|
1723
|
+
<span class="column-title in-progress">In Progress</span>
|
|
1724
|
+
<span id="in-progress-count" class="column-count in-progress">0</span>
|
|
1725
|
+
</div>
|
|
1726
|
+
<div id="in-progress-tasks" class="column-tasks" role="list"></div>
|
|
1727
|
+
</div>
|
|
1728
|
+
|
|
1729
|
+
<div class="kanban-column" aria-label="Completed tasks">
|
|
1730
|
+
<div class="column-header">
|
|
1731
|
+
<span class="column-dot completed"></span>
|
|
1732
|
+
<span class="column-title completed">Completed</span>
|
|
1733
|
+
<span id="completed-count" class="column-count completed">0</span>
|
|
1734
|
+
</div>
|
|
1735
|
+
<div id="completed-tasks" class="column-tasks" role="list"></div>
|
|
1736
|
+
</div>
|
|
1737
|
+
</div>
|
|
1738
|
+
</div>
|
|
1739
|
+
</main>
|
|
1740
|
+
|
|
1741
|
+
<!-- Detail panel -->
|
|
1742
|
+
<aside id="detail-panel" class="detail-panel">
|
|
1743
|
+
<header class="detail-header">
|
|
1744
|
+
<h3>Task Details</h3>
|
|
1745
|
+
<button id="close-detail" class="detail-close" aria-label="Close detail panel">
|
|
1746
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1747
|
+
<path d="M6 18L18 6M6 6l12 12"/>
|
|
1748
|
+
</svg>
|
|
1749
|
+
</button>
|
|
1750
|
+
</header>
|
|
1751
|
+
<div id="detail-content" class="detail-content"></div>
|
|
1752
|
+
</aside>
|
|
1753
|
+
</div>
|
|
1754
|
+
|
|
1755
|
+
<script>
|
|
1756
|
+
// State
|
|
1757
|
+
let sessions = [];
|
|
1758
|
+
let currentSessionId = null;
|
|
1759
|
+
let currentTasks = [];
|
|
1760
|
+
let viewMode = 'session';
|
|
1761
|
+
let sessionFilter = localStorage.getItem('sessionFilter') || 'all'; // 'all' or 'active'
|
|
1762
|
+
let sessionLimit = localStorage.getItem('sessionLimit') || '20'; // '10', '20', '50', 'all'
|
|
1763
|
+
let filterProject = null; // null = all projects, or project path to filter
|
|
1764
|
+
let searchQuery = ''; // Search query for fuzzy search
|
|
1765
|
+
let allTasksCache = []; // Cache all tasks for search
|
|
1766
|
+
let bulkDeleteSessionId = null; // Track session for bulk delete
|
|
1767
|
+
let ownerFilter = '';
|
|
1768
|
+
|
|
1769
|
+
// DOM
|
|
1770
|
+
const sessionsList = document.getElementById('sessions-list');
|
|
1771
|
+
const noSession = document.getElementById('no-session');
|
|
1772
|
+
const sessionView = document.getElementById('session-view');
|
|
1773
|
+
const sessionTitle = document.getElementById('session-title');
|
|
1774
|
+
const sessionMeta = document.getElementById('session-meta');
|
|
1775
|
+
const progressPercent = document.getElementById('progress-percent');
|
|
1776
|
+
const progressBar = document.getElementById('progress-bar');
|
|
1777
|
+
const pendingTasks = document.getElementById('pending-tasks');
|
|
1778
|
+
const inProgressTasks = document.getElementById('in-progress-tasks');
|
|
1779
|
+
const completedTasks = document.getElementById('completed-tasks');
|
|
1780
|
+
const pendingCount = document.getElementById('pending-count');
|
|
1781
|
+
const inProgressCount = document.getElementById('in-progress-count');
|
|
1782
|
+
const completedCount = document.getElementById('completed-count');
|
|
1783
|
+
const detailPanel = document.getElementById('detail-panel');
|
|
1784
|
+
const detailContent = document.getElementById('detail-content');
|
|
1785
|
+
const connectionStatus = document.getElementById('connection-status');
|
|
1786
|
+
|
|
1787
|
+
let lastSessionsHash = '';
|
|
1788
|
+
let lastTasksHash = '';
|
|
1789
|
+
|
|
1790
|
+
async function fetchSessions() {
|
|
1791
|
+
console.log('[fetchSessions] Starting...');
|
|
1792
|
+
try {
|
|
1793
|
+
const res = await fetch(`/api/sessions?limit=${sessionLimit}`);
|
|
1794
|
+
const newSessions = await res.json();
|
|
1795
|
+
const tasksRes = await fetch('/api/tasks/all');
|
|
1796
|
+
const newTasks = await tasksRes.json();
|
|
1797
|
+
|
|
1798
|
+
const sessionsHash = JSON.stringify(newSessions);
|
|
1799
|
+
const tasksHash = JSON.stringify(newTasks);
|
|
1800
|
+
if (sessionsHash === lastSessionsHash && tasksHash === lastTasksHash) {
|
|
1801
|
+
console.log('[fetchSessions] No changes, skipping render');
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
lastSessionsHash = sessionsHash;
|
|
1805
|
+
lastTasksHash = tasksHash;
|
|
1806
|
+
|
|
1807
|
+
sessions = newSessions;
|
|
1808
|
+
allTasksCache = newTasks;
|
|
1809
|
+
console.log('[fetchSessions] Sessions loaded:', sessions.length);
|
|
1810
|
+
renderSessions();
|
|
1811
|
+
console.log('[fetchSessions] Render complete');
|
|
1812
|
+
fetchLiveUpdates();
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
console.error('Failed to fetch sessions:', error);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function handleSearch(query) {
|
|
1819
|
+
searchQuery = query.toLowerCase().trim();
|
|
1820
|
+
|
|
1821
|
+
// Show/hide clear button
|
|
1822
|
+
const clearBtn = document.getElementById('search-clear-btn');
|
|
1823
|
+
if (searchQuery) {
|
|
1824
|
+
clearBtn.classList.add('visible');
|
|
1825
|
+
} else {
|
|
1826
|
+
clearBtn.classList.remove('visible');
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
renderSessions();
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function clearSearch() {
|
|
1833
|
+
const searchInput = document.getElementById('search-input');
|
|
1834
|
+
searchInput.value = '';
|
|
1835
|
+
searchQuery = '';
|
|
1836
|
+
document.getElementById('search-clear-btn').classList.remove('visible');
|
|
1837
|
+
renderSessions();
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function deleteAllSessionTasks(sessionId) {
|
|
1841
|
+
const session = sessions.find(s => s.id === sessionId);
|
|
1842
|
+
if (!session) return;
|
|
1843
|
+
|
|
1844
|
+
// When viewing a single session, currentTasks already contains only that session's tasks
|
|
1845
|
+
// When viewing "All Tasks", tasks have sessionId property, so we filter
|
|
1846
|
+
const sessionTasks = currentSessionId === sessionId
|
|
1847
|
+
? currentTasks
|
|
1848
|
+
: currentTasks.filter(t => t.sessionId === sessionId);
|
|
1849
|
+
|
|
1850
|
+
if (sessionTasks.length === 0) {
|
|
1851
|
+
alert('No tasks to delete in this session');
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
bulkDeleteSessionId = sessionId;
|
|
1856
|
+
|
|
1857
|
+
const displayName = session.name || sessionId;
|
|
1858
|
+
const message = `Delete all ${sessionTasks.length} task(s) from session "${displayName}"?`;
|
|
1859
|
+
|
|
1860
|
+
document.getElementById('delete-session-tasks-message').textContent = message;
|
|
1861
|
+
|
|
1862
|
+
const modal = document.getElementById('delete-session-tasks-modal');
|
|
1863
|
+
modal.classList.add('visible');
|
|
1864
|
+
|
|
1865
|
+
// Handle ESC key
|
|
1866
|
+
const keyHandler = (e) => {
|
|
1867
|
+
if (e.key === 'Escape') {
|
|
1868
|
+
e.preventDefault();
|
|
1869
|
+
closeDeleteSessionTasksModal();
|
|
1870
|
+
document.removeEventListener('keydown', keyHandler);
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
document.addEventListener('keydown', keyHandler);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function closeDeleteSessionTasksModal() {
|
|
1877
|
+
const modal = document.getElementById('delete-session-tasks-modal');
|
|
1878
|
+
modal.classList.remove('visible');
|
|
1879
|
+
bulkDeleteSessionId = null;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
async function confirmDeleteSessionTasks() {
|
|
1883
|
+
if (!bulkDeleteSessionId) return;
|
|
1884
|
+
|
|
1885
|
+
const sessionId = bulkDeleteSessionId;
|
|
1886
|
+
closeDeleteSessionTasksModal();
|
|
1887
|
+
|
|
1888
|
+
// Get tasks to delete
|
|
1889
|
+
const sessionTasks = currentSessionId === sessionId
|
|
1890
|
+
? currentTasks
|
|
1891
|
+
: currentTasks.filter(t => t.sessionId === sessionId);
|
|
1892
|
+
|
|
1893
|
+
// Sort tasks by dependency order (blocked tasks first, then blockers)
|
|
1894
|
+
const sortedTasks = topologicalSort(sessionTasks);
|
|
1895
|
+
|
|
1896
|
+
let successCount = 0;
|
|
1897
|
+
let failedCount = 0;
|
|
1898
|
+
const failedTasks = [];
|
|
1899
|
+
|
|
1900
|
+
for (const task of sortedTasks) {
|
|
1901
|
+
try {
|
|
1902
|
+
const res = await fetch(`/api/tasks/${sessionId}/${task.id}`, {
|
|
1903
|
+
method: 'DELETE'
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
if (res.ok) {
|
|
1907
|
+
successCount++;
|
|
1908
|
+
} else {
|
|
1909
|
+
failedCount++;
|
|
1910
|
+
const error = await res.json();
|
|
1911
|
+
failedTasks.push({ id: task.id, subject: task.subject, error: error.error });
|
|
1912
|
+
console.error(`Failed to delete task ${task.id}:`, error);
|
|
1913
|
+
}
|
|
1914
|
+
} catch (error) {
|
|
1915
|
+
failedCount++;
|
|
1916
|
+
failedTasks.push({ id: task.id, subject: task.subject, error: 'Network error' });
|
|
1917
|
+
console.error(`Error deleting task ${task.id}:`, error);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Show result modal
|
|
1922
|
+
showDeleteResultModal(successCount, failedCount, failedTasks);
|
|
1923
|
+
|
|
1924
|
+
// Close detail panel if open
|
|
1925
|
+
closeDetailPanel();
|
|
1926
|
+
|
|
1927
|
+
// Refresh the view
|
|
1928
|
+
await refreshCurrentView();
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// Topological sort for task deletion order
|
|
1932
|
+
function topologicalSort(tasks) {
|
|
1933
|
+
const result = [];
|
|
1934
|
+
const visited = new Set();
|
|
1935
|
+
const visiting = new Set();
|
|
1936
|
+
const taskMap = new Map(tasks.map(t => [t.id, t]));
|
|
1937
|
+
|
|
1938
|
+
function visit(taskId) {
|
|
1939
|
+
if (visited.has(taskId)) return;
|
|
1940
|
+
if (visiting.has(taskId)) return; // Cycle - skip
|
|
1941
|
+
|
|
1942
|
+
visiting.add(taskId);
|
|
1943
|
+
const task = taskMap.get(taskId);
|
|
1944
|
+
|
|
1945
|
+
if (task && task.blocks && task.blocks.length > 0) {
|
|
1946
|
+
// Visit all tasks that this task blocks (dependencies first)
|
|
1947
|
+
for (const blockedId of task.blocks) {
|
|
1948
|
+
if (taskMap.has(blockedId)) {
|
|
1949
|
+
visit(blockedId);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
visiting.delete(taskId);
|
|
1955
|
+
visited.add(taskId);
|
|
1956
|
+
if (task) result.push(task);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// Visit all tasks
|
|
1960
|
+
for (const task of tasks) {
|
|
1961
|
+
visit(task.id);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
return result;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function showDeleteResultModal(successCount, failedCount, failedTasks) {
|
|
1968
|
+
const modal = document.getElementById('delete-result-modal');
|
|
1969
|
+
const messageEl = document.getElementById('delete-result-message');
|
|
1970
|
+
const detailsEl = document.getElementById('delete-result-details');
|
|
1971
|
+
|
|
1972
|
+
if (failedCount === 0) {
|
|
1973
|
+
messageEl.textContent = `Successfully deleted all ${successCount} task(s).`;
|
|
1974
|
+
detailsEl.style.display = 'none';
|
|
1975
|
+
} else {
|
|
1976
|
+
messageEl.textContent = `Deleted ${successCount} task(s). Failed to delete ${failedCount} task(s).`;
|
|
1977
|
+
|
|
1978
|
+
const failedList = failedTasks.map(t =>
|
|
1979
|
+
`<li><strong>${escapeHtml(t.subject)}</strong> (#${escapeHtml(t.id)}): ${escapeHtml(t.error)}</li>`
|
|
1980
|
+
).join('');
|
|
1981
|
+
detailsEl.innerHTML = `<ul style="margin: 8px 0 0 0; padding-left: 20px;">${failedList}</ul>`;
|
|
1982
|
+
detailsEl.style.display = 'block';
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
modal.classList.add('visible');
|
|
1986
|
+
|
|
1987
|
+
// Handle ESC key
|
|
1988
|
+
const keyHandler = (e) => {
|
|
1989
|
+
if (e.key === 'Escape') {
|
|
1990
|
+
e.preventDefault();
|
|
1991
|
+
closeDeleteResultModal();
|
|
1992
|
+
document.removeEventListener('keydown', keyHandler);
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
document.addEventListener('keydown', keyHandler);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
function closeDeleteResultModal() {
|
|
1999
|
+
const modal = document.getElementById('delete-result-modal');
|
|
2000
|
+
modal.classList.remove('visible');
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
function fuzzyMatch(text, query) {
|
|
2004
|
+
if (!query) return true;
|
|
2005
|
+
if (!text) return false;
|
|
2006
|
+
|
|
2007
|
+
text = text.toLowerCase();
|
|
2008
|
+
query = query.toLowerCase();
|
|
2009
|
+
|
|
2010
|
+
// Prioritize exact substring match
|
|
2011
|
+
if (text.includes(query)) return true;
|
|
2012
|
+
|
|
2013
|
+
// Split by common delimiters to search in individual words
|
|
2014
|
+
const words = text.split(/[\s\-_\/\.]+/);
|
|
2015
|
+
|
|
2016
|
+
// Check if query matches start of any word
|
|
2017
|
+
for (const word of words) {
|
|
2018
|
+
if (word.startsWith(query)) return true;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// Check if any word contains the query
|
|
2022
|
+
for (const word of words) {
|
|
2023
|
+
if (word.includes(query)) return true;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
return false;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
async function fetchLiveUpdates() {
|
|
2030
|
+
try {
|
|
2031
|
+
const res = await fetch('/api/tasks/all');
|
|
2032
|
+
const allTasks = await res.json();
|
|
2033
|
+
let activeTasks = allTasks.filter(t => t.status === 'in_progress');
|
|
2034
|
+
if (filterProject) {
|
|
2035
|
+
activeTasks = activeTasks.filter(t => t.project === filterProject);
|
|
2036
|
+
}
|
|
2037
|
+
renderLiveUpdates(activeTasks);
|
|
2038
|
+
} catch (error) {
|
|
2039
|
+
console.error('Failed to fetch live updates:', error);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function renderLiveUpdates(activeTasks) {
|
|
2044
|
+
const container = document.getElementById('live-updates');
|
|
2045
|
+
|
|
2046
|
+
if (activeTasks.length === 0) {
|
|
2047
|
+
container.innerHTML = '<div class="live-empty">No active tasks</div>';
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
container.innerHTML = activeTasks.map(task => `
|
|
2052
|
+
<div class="live-item" onclick="openLiveTask('${task.sessionId}', '${task.id}')">
|
|
2053
|
+
<span class="pulse"></span>
|
|
2054
|
+
<div class="live-item-content">
|
|
2055
|
+
<div class="live-item-action">${escapeHtml(task.activeForm || task.subject)}</div>
|
|
2056
|
+
<div class="live-item-session">${escapeHtml(task.sessionName || task.sessionId.slice(0, 8))}</div>
|
|
2057
|
+
</div>
|
|
2058
|
+
</div>
|
|
2059
|
+
`).join('');
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
async function openLiveTask(sessionId, taskId) {
|
|
2063
|
+
await fetchTasks(sessionId);
|
|
2064
|
+
showTaskDetail(taskId, sessionId);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
let lastCurrentTasksHash = '';
|
|
2068
|
+
|
|
2069
|
+
async function fetchTasks(sessionId) {
|
|
2070
|
+
try {
|
|
2071
|
+
viewMode = 'session';
|
|
2072
|
+
const res = await fetch(`/api/sessions/${sessionId}`);
|
|
2073
|
+
|
|
2074
|
+
let newTasks;
|
|
2075
|
+
if (res.ok) {
|
|
2076
|
+
newTasks = await res.json();
|
|
2077
|
+
} else if (res.status === 404) {
|
|
2078
|
+
newTasks = [];
|
|
2079
|
+
} else {
|
|
2080
|
+
throw new Error(`Failed to fetch tasks: ${res.status}`);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
const hash = JSON.stringify(newTasks);
|
|
2084
|
+
if (sessionId === currentSessionId && hash === lastCurrentTasksHash) {
|
|
2085
|
+
console.log('[fetchTasks] No changes, skipping render');
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
lastCurrentTasksHash = hash;
|
|
2089
|
+
|
|
2090
|
+
currentTasks = newTasks;
|
|
2091
|
+
currentSessionId = sessionId;
|
|
2092
|
+
ownerFilter = '';
|
|
2093
|
+
renderSession();
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
console.error('Failed to fetch tasks:', error);
|
|
2096
|
+
currentTasks = [];
|
|
2097
|
+
currentSessionId = sessionId;
|
|
2098
|
+
lastCurrentTasksHash = '';
|
|
2099
|
+
renderSession();
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
async function showAllTasks() {
|
|
2104
|
+
try {
|
|
2105
|
+
viewMode = 'all';
|
|
2106
|
+
currentSessionId = null;
|
|
2107
|
+
ownerFilter = '';
|
|
2108
|
+
const res = await fetch('/api/tasks/all');
|
|
2109
|
+
let tasks = await res.json();
|
|
2110
|
+
if (filterProject) {
|
|
2111
|
+
tasks = tasks.filter(t => t.project === filterProject);
|
|
2112
|
+
}
|
|
2113
|
+
currentTasks = tasks;
|
|
2114
|
+
renderAllTasks();
|
|
2115
|
+
renderSessions();
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
console.error('Failed to fetch all tasks:', error);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
function renderAllTasks() {
|
|
2122
|
+
noSession.style.display = 'none';
|
|
2123
|
+
sessionView.classList.add('visible');
|
|
2124
|
+
document.getElementById('owner-filter-bar').classList.remove('visible');
|
|
2125
|
+
|
|
2126
|
+
const totalTasks = currentTasks.length;
|
|
2127
|
+
const completed = currentTasks.filter(t => t.status === 'completed').length;
|
|
2128
|
+
const percent = totalTasks > 0 ? Math.round((completed / totalTasks) * 100) : 0;
|
|
2129
|
+
|
|
2130
|
+
const projectName = filterProject ? filterProject.split('/').pop() : null;
|
|
2131
|
+
sessionTitle.textContent = filterProject ? `Tasks: ${projectName}` : 'All Tasks';
|
|
2132
|
+
sessionMeta.textContent = filterProject
|
|
2133
|
+
? `${totalTasks} tasks in this project`
|
|
2134
|
+
: `${totalTasks} tasks across ${sessions.length} sessions`;
|
|
2135
|
+
progressPercent.textContent = `${percent}%`;
|
|
2136
|
+
progressBar.style.width = `${percent}%`;
|
|
2137
|
+
|
|
2138
|
+
renderKanban();
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function renderSessions() {
|
|
2142
|
+
// Update project dropdown
|
|
2143
|
+
updateProjectDropdown();
|
|
2144
|
+
|
|
2145
|
+
let filteredSessions = sessions;
|
|
2146
|
+
if (sessionFilter === 'active') {
|
|
2147
|
+
filteredSessions = filteredSessions.filter(s => s.pending > 0 || s.inProgress > 0);
|
|
2148
|
+
}
|
|
2149
|
+
if (filterProject) {
|
|
2150
|
+
filteredSessions = filteredSessions.filter(s => s.project === filterProject);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// Apply search filter
|
|
2154
|
+
if (searchQuery) {
|
|
2155
|
+
filteredSessions = filteredSessions.filter(session => {
|
|
2156
|
+
// Search in session name and ID
|
|
2157
|
+
if (session.name && fuzzyMatch(session.name, searchQuery)) return true;
|
|
2158
|
+
if (session.id && fuzzyMatch(session.id, searchQuery)) return true;
|
|
2159
|
+
|
|
2160
|
+
// Search in project path
|
|
2161
|
+
if (session.project && fuzzyMatch(session.project, searchQuery)) return true;
|
|
2162
|
+
|
|
2163
|
+
// Search in description
|
|
2164
|
+
if (session.description && fuzzyMatch(session.description, searchQuery)) return true;
|
|
2165
|
+
|
|
2166
|
+
// Search in tasks for this session
|
|
2167
|
+
const sessionTasks = allTasksCache.filter(t => t.sessionId === session.id);
|
|
2168
|
+
return sessionTasks.some(task =>
|
|
2169
|
+
(task.subject && fuzzyMatch(task.subject, searchQuery)) ||
|
|
2170
|
+
(task.description && fuzzyMatch(task.description, searchQuery)) ||
|
|
2171
|
+
(task.activeForm && fuzzyMatch(task.activeForm, searchQuery))
|
|
2172
|
+
);
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
if (filteredSessions.length === 0) {
|
|
2177
|
+
let emptyMsg = 'No sessions found';
|
|
2178
|
+
let emptyHint = 'Tasks appear when you use Claude Code';
|
|
2179
|
+
|
|
2180
|
+
if (searchQuery) {
|
|
2181
|
+
emptyMsg = `No results for "${searchQuery}"`;
|
|
2182
|
+
emptyHint = 'Try a different search term or clear the search';
|
|
2183
|
+
} else if (filterProject && sessionFilter === 'active') {
|
|
2184
|
+
emptyMsg = 'No active sessions for this project';
|
|
2185
|
+
emptyHint = 'Try "All Sessions" or "All Projects"';
|
|
2186
|
+
} else if (filterProject) {
|
|
2187
|
+
emptyMsg = 'No sessions for this project';
|
|
2188
|
+
emptyHint = 'Select "All Projects" to see all';
|
|
2189
|
+
} else if (sessionFilter === 'active') {
|
|
2190
|
+
emptyMsg = 'No active sessions';
|
|
2191
|
+
emptyHint = 'Select "All Sessions" to see all';
|
|
2192
|
+
}
|
|
2193
|
+
sessionsList.innerHTML = `
|
|
2194
|
+
<div style="padding: 24px 12px; text-align: center; color: var(--text-muted); font-size: 12px;">
|
|
2195
|
+
<p>${emptyMsg}</p>
|
|
2196
|
+
<p style="margin-top: 8px; font-size: 11px;">${emptyHint}</p>
|
|
2197
|
+
</div>
|
|
2198
|
+
`;
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
sessionsList.innerHTML = filteredSessions.map(session => {
|
|
2203
|
+
const total = session.taskCount;
|
|
2204
|
+
const percent = total > 0 ? Math.round((session.completed / total) * 100) : 0;
|
|
2205
|
+
const isActive = session.id === currentSessionId && viewMode === 'session';
|
|
2206
|
+
const hasInProgress = session.inProgress > 0;
|
|
2207
|
+
const sessionName = session.name || session.id.slice(0, 8) + '...';
|
|
2208
|
+
const projectName = session.project ? session.project.split('/').pop() : null;
|
|
2209
|
+
const primaryName = projectName || sessionName;
|
|
2210
|
+
const secondaryName = projectName ? sessionName : null;
|
|
2211
|
+
|
|
2212
|
+
// Format git branch for display
|
|
2213
|
+
const gitBranch = session.gitBranch ? escapeHtml(session.gitBranch) : null;
|
|
2214
|
+
|
|
2215
|
+
// Format timestamps for display
|
|
2216
|
+
const createdDisplay = session.createdAt ? formatDate(session.createdAt) : '';
|
|
2217
|
+
const modifiedDisplay = formatDate(session.modifiedAt);
|
|
2218
|
+
const timeDisplay = session.createdAt && createdDisplay !== modifiedDisplay
|
|
2219
|
+
? `Created ${createdDisplay} · Modified ${modifiedDisplay}`
|
|
2220
|
+
: modifiedDisplay;
|
|
2221
|
+
|
|
2222
|
+
// Build tooltip
|
|
2223
|
+
const tooltip = [timeDisplay, gitBranch ? `Branch: ${gitBranch}` : ''].filter(Boolean).join(' | ');
|
|
2224
|
+
|
|
2225
|
+
const isTeam = session.isTeam;
|
|
2226
|
+
const memberCount = session.memberCount || 0;
|
|
2227
|
+
|
|
2228
|
+
return `
|
|
2229
|
+
<button onclick="fetchTasks('${session.id}')" class="session-item ${isActive ? 'active' : ''}" title="${tooltip}">
|
|
2230
|
+
<div class="session-name">
|
|
2231
|
+
<span>${escapeHtml(primaryName)}</span>
|
|
2232
|
+
<span class="session-indicators">
|
|
2233
|
+
${isTeam ? `<span class="team-badge" title="${memberCount} team members"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${memberCount}</span>` : ''}
|
|
2234
|
+
${isTeam ? `<span class="team-info-btn" onclick="event.stopPropagation(); showTeamModalForSession('${session.id}')" title="View team info">ℹ</span>` : ''}
|
|
2235
|
+
${hasInProgress ? '<span class="pulse"></span>' : ''}
|
|
2236
|
+
</span>
|
|
2237
|
+
</div>
|
|
2238
|
+
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
2239
|
+
${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
|
|
2240
|
+
<div class="session-progress">
|
|
2241
|
+
<div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
|
|
2242
|
+
<span class="progress-text">${session.completed}/${total}</span>
|
|
2243
|
+
</div>
|
|
2244
|
+
<div class="session-time">${formatDate(session.modifiedAt)}</div>
|
|
2245
|
+
</button>
|
|
2246
|
+
`;
|
|
2247
|
+
}).join('');
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
function renderSession() {
|
|
2251
|
+
noSession.style.display = 'none';
|
|
2252
|
+
sessionView.classList.add('visible');
|
|
2253
|
+
|
|
2254
|
+
const session = sessions.find(s => s.id === currentSessionId);
|
|
2255
|
+
if (!session) return;
|
|
2256
|
+
|
|
2257
|
+
const displayName = session.name || currentSessionId;
|
|
2258
|
+
|
|
2259
|
+
// Create header with delete button
|
|
2260
|
+
sessionTitle.innerHTML = `
|
|
2261
|
+
<span style="flex: 1;">${escapeHtml(displayName)}</span>
|
|
2262
|
+
<button class="icon-btn icon-btn-danger" onclick="deleteAllSessionTasks('${session.id}')" title="Delete all tasks in this session">
|
|
2263
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2264
|
+
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
|
2265
|
+
</svg>
|
|
2266
|
+
</button>
|
|
2267
|
+
`;
|
|
2268
|
+
|
|
2269
|
+
// Build meta text with project path, branch, and description
|
|
2270
|
+
const projectName = session.project ? session.project.split('/').pop() : null;
|
|
2271
|
+
const metaParts = [`${currentTasks.length} tasks`];
|
|
2272
|
+
if (projectName) {
|
|
2273
|
+
metaParts.push(projectName);
|
|
2274
|
+
}
|
|
2275
|
+
if (session.gitBranch) {
|
|
2276
|
+
metaParts.push(session.gitBranch);
|
|
2277
|
+
}
|
|
2278
|
+
if (session.description) {
|
|
2279
|
+
metaParts.push(session.description);
|
|
2280
|
+
}
|
|
2281
|
+
metaParts.push(formatDate(session.modifiedAt));
|
|
2282
|
+
sessionMeta.textContent = metaParts.join(' · ');
|
|
2283
|
+
|
|
2284
|
+
const completed = currentTasks.filter(t => t.status === 'completed').length;
|
|
2285
|
+
const percent = currentTasks.length > 0 ? Math.round((completed / currentTasks.length) * 100) : 0;
|
|
2286
|
+
|
|
2287
|
+
progressPercent.textContent = `${percent}%`;
|
|
2288
|
+
progressBar.style.width = `${percent}%`;
|
|
2289
|
+
const hasInProgress = currentTasks.some(t => t.status === 'in_progress');
|
|
2290
|
+
progressBar.classList.toggle('shimmer', hasInProgress && percent < 100);
|
|
2291
|
+
|
|
2292
|
+
updateOwnerFilter();
|
|
2293
|
+
renderKanban();
|
|
2294
|
+
renderSessions();
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
function renderTaskCard(task) {
|
|
2298
|
+
const isBlocked = task.blockedBy && task.blockedBy.length > 0;
|
|
2299
|
+
const taskId = viewMode === 'all' ? `${task.sessionId?.slice(0,4)}-${task.id}` : task.id;
|
|
2300
|
+
const sessionLabel = viewMode === 'all' && task.sessionName ? task.sessionName : null;
|
|
2301
|
+
const statusClass = task.status.replace('_', '-');
|
|
2302
|
+
const actualSessionId = task.sessionId || currentSessionId;
|
|
2303
|
+
|
|
2304
|
+
return `
|
|
2305
|
+
<div
|
|
2306
|
+
role="listitem"
|
|
2307
|
+
tabindex="0"
|
|
2308
|
+
onclick="showTaskDetail('${task.id}', '${actualSessionId}')"
|
|
2309
|
+
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();showTaskDetail('${task.id}','${actualSessionId}')}"
|
|
2310
|
+
class="task-card ${statusClass} ${isBlocked ? 'blocked' : ''}"
|
|
2311
|
+
aria-label="${escapeHtml(task.subject)} — ${task.status.replace('_',' ')}">
|
|
2312
|
+
<div class="task-id">
|
|
2313
|
+
<span>#${taskId}</span>
|
|
2314
|
+
${isBlocked ? '<span class="task-badge blocked">Blocked</span>' : ''}
|
|
2315
|
+
${task.owner ? (() => { const c = getOwnerColor(task.owner); return `<span class="task-owner-badge" style="background:${c.bg};color:${c.color}">${escapeHtml(task.owner)}</span>`; })() : ''}
|
|
2316
|
+
</div>
|
|
2317
|
+
<div class="task-title">${escapeHtml(task.subject)}</div>
|
|
2318
|
+
${sessionLabel ? `<div class="task-session">${escapeHtml(sessionLabel)}</div>` : ''}
|
|
2319
|
+
${task.status === 'in_progress' && task.activeForm ? `<div class="task-active">${escapeHtml(task.activeForm)}</div>` : ''}
|
|
2320
|
+
${isBlocked ? `<div class="task-blocked">Waiting on ${task.blockedBy.map(id => '#' + id).join(', ')}</div>` : ''}
|
|
2321
|
+
${task.description ? `<div class="task-desc">${escapeHtml(task.description.split('\n')[0])}</div>` : ''}
|
|
2322
|
+
</div>
|
|
2323
|
+
`;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
function renderKanban() {
|
|
2327
|
+
let filtered = currentTasks;
|
|
2328
|
+
if (ownerFilter) {
|
|
2329
|
+
filtered = filtered.filter(t => t.owner === ownerFilter);
|
|
2330
|
+
}
|
|
2331
|
+
const pending = filtered.filter(t => t.status === 'pending');
|
|
2332
|
+
const inProgress = filtered.filter(t => t.status === 'in_progress');
|
|
2333
|
+
const completed = filtered.filter(t => t.status === 'completed');
|
|
2334
|
+
|
|
2335
|
+
pendingCount.textContent = pending.length;
|
|
2336
|
+
inProgressCount.textContent = inProgress.length;
|
|
2337
|
+
completedCount.textContent = completed.length;
|
|
2338
|
+
|
|
2339
|
+
const emptyIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>`;
|
|
2340
|
+
|
|
2341
|
+
pendingTasks.innerHTML = pending.length > 0
|
|
2342
|
+
? pending.map(renderTaskCard).join('')
|
|
2343
|
+
: `<div class="column-empty">${emptyIcon}<div>No pending tasks</div></div>`;
|
|
2344
|
+
|
|
2345
|
+
inProgressTasks.innerHTML = inProgress.length > 0
|
|
2346
|
+
? inProgress.map(renderTaskCard).join('')
|
|
2347
|
+
: `<div class="column-empty">${emptyIcon}<div>No active tasks</div></div>`;
|
|
2348
|
+
|
|
2349
|
+
completedTasks.innerHTML = completed.length > 0
|
|
2350
|
+
? completed.map(renderTaskCard).join('')
|
|
2351
|
+
: `<div class="column-empty">${emptyIcon}<div>No completed tasks</div></div>`;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function getAvailableTasksOptions(currentTaskId = null) {
|
|
2355
|
+
const pending = currentTasks.filter(t => t.status === 'pending' && t.id !== currentTaskId);
|
|
2356
|
+
const inProgress = currentTasks.filter(t => t.status === 'in_progress' && t.id !== currentTaskId);
|
|
2357
|
+
const completed = currentTasks.filter(t => t.status === 'completed' && t.id !== currentTaskId);
|
|
2358
|
+
|
|
2359
|
+
// Build options grouped by status
|
|
2360
|
+
let options = '';
|
|
2361
|
+
|
|
2362
|
+
if (pending.length > 0) {
|
|
2363
|
+
options += '<optgroup label="Pending">';
|
|
2364
|
+
pending.forEach((t, idx) => {
|
|
2365
|
+
options += `<option value="${t.id}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
|
|
2366
|
+
});
|
|
2367
|
+
options += '</optgroup>';
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
if (inProgress.length > 0) {
|
|
2371
|
+
options += '<optgroup label="In Progress">';
|
|
2372
|
+
inProgress.forEach((t, idx) => {
|
|
2373
|
+
options += `<option value="${t.id}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
|
|
2374
|
+
});
|
|
2375
|
+
options += '</optgroup>';
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
if (completed.length > 0) {
|
|
2379
|
+
options += '<optgroup label="Completed">';
|
|
2380
|
+
completed.forEach((t, idx) => {
|
|
2381
|
+
options += `<option value="${t.id}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
|
|
2382
|
+
});
|
|
2383
|
+
options += '</optgroup>';
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
return options;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
async function showTaskDetail(taskId, sessionId = null) {
|
|
2390
|
+
let task = currentTasks.find(t => t.id === taskId && (!sessionId || t.sessionId === sessionId));
|
|
2391
|
+
|
|
2392
|
+
// If task not found in currentTasks, fetch it from the session
|
|
2393
|
+
if (!task && sessionId && sessionId !== 'undefined') {
|
|
2394
|
+
try {
|
|
2395
|
+
const res = await fetch(`/api/sessions/${sessionId}`);
|
|
2396
|
+
const tasks = await res.json();
|
|
2397
|
+
task = tasks.find(t => t.id === taskId);
|
|
2398
|
+
if (!task) return;
|
|
2399
|
+
} catch (error) {
|
|
2400
|
+
console.error('Failed to fetch task:', error);
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
if (!task) return;
|
|
2406
|
+
|
|
2407
|
+
detailPanel.classList.add('visible');
|
|
2408
|
+
|
|
2409
|
+
const statusLabels = {
|
|
2410
|
+
completed: '<span class="detail-status completed"><span class="dot"></span>Completed</span>',
|
|
2411
|
+
in_progress: '<span class="detail-status in_progress"><span class="dot"></span>In Progress</span>',
|
|
2412
|
+
pending: '<span class="detail-status pending"><span class="dot"></span>Pending</span>'
|
|
2413
|
+
};
|
|
2414
|
+
|
|
2415
|
+
const isBlocked = task.blockedBy && task.blockedBy.length > 0;
|
|
2416
|
+
const actualSessionId = task.sessionId || sessionId || currentSessionId;
|
|
2417
|
+
|
|
2418
|
+
detailContent.innerHTML = `
|
|
2419
|
+
<div class="detail-section">
|
|
2420
|
+
<div style="display: flex; justify-content: space-between; align-items: start;">
|
|
2421
|
+
<div style="flex: 1;">
|
|
2422
|
+
<div class="detail-label">Task #${task.id}</div>
|
|
2423
|
+
<h2 class="detail-title">${escapeHtml(task.subject)}</h2>
|
|
2424
|
+
</div>
|
|
2425
|
+
<div style="display: flex; gap: 8px;">
|
|
2426
|
+
<button id="delete-task-btn" class="icon-btn" title="Delete task (D)" aria-label="Delete task" style="color: #ef4444; border-color: #ef4444;">
|
|
2427
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2428
|
+
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
2429
|
+
</svg>
|
|
2430
|
+
</button>
|
|
2431
|
+
</div>
|
|
2432
|
+
</div>
|
|
2433
|
+
</div>
|
|
2434
|
+
|
|
2435
|
+
<div class="detail-section" style="display: flex; gap: 12px; align-items: center;">
|
|
2436
|
+
<div>${statusLabels[task.status] || ''}</div>
|
|
2437
|
+
${task.owner ? `<div style="font-size: 13px; color: ${getOwnerColor(task.owner).color}; font-weight: 500;">${escapeHtml(task.owner)}</div>` : ''}
|
|
2438
|
+
${isBlocked && task.status !== 'in_progress' ? '<div style="font-size: 10px; color: var(--warning);">Blocked</div>' : ''}
|
|
2439
|
+
</div>
|
|
2440
|
+
|
|
2441
|
+
<div class="detail-section">
|
|
2442
|
+
<div class="detail-label">Description</div>
|
|
2443
|
+
<div class="detail-desc">${task.description ? DOMPurify.sanitize(marked.parse(task.description)) : '<em style="color: var(--text-muted);">No description</em>'}</div>
|
|
2444
|
+
</div>
|
|
2445
|
+
|
|
2446
|
+
${task.activeForm && task.status === 'in_progress' ? `
|
|
2447
|
+
<div class="detail-section">
|
|
2448
|
+
<div class="detail-box active">
|
|
2449
|
+
<strong>Currently:</strong> ${escapeHtml(task.activeForm)}
|
|
2450
|
+
</div>
|
|
2451
|
+
</div>
|
|
2452
|
+
` : ''}
|
|
2453
|
+
|
|
2454
|
+
<div class="detail-section">
|
|
2455
|
+
<div class="detail-label">Blocked By</div>
|
|
2456
|
+
<div class="detail-desc">
|
|
2457
|
+
${task.blockedBy && task.blockedBy.length > 0
|
|
2458
|
+
? `<div class="detail-box blocked"><strong>Blocked by:</strong> ${task.blockedBy.map(id => '#' + id).join(', ')}</div>`
|
|
2459
|
+
: '<em style="color: var(--text-muted); font-size: 13px;">No dependencies</em>'}
|
|
2460
|
+
</div>
|
|
2461
|
+
</div>
|
|
2462
|
+
|
|
2463
|
+
<div class="detail-section">
|
|
2464
|
+
<div class="detail-label">Blocks</div>
|
|
2465
|
+
<div class="detail-desc">
|
|
2466
|
+
${task.blocks && task.blocks.length > 0
|
|
2467
|
+
? `<div class="detail-box blocks"><strong>Blocks:</strong> ${task.blocks.map(id => '#' + id).join(', ')}</div>`
|
|
2468
|
+
: '<em style="color: var(--text-muted); font-size: 13px;">No tasks blocked</em>'}
|
|
2469
|
+
</div>
|
|
2470
|
+
</div>
|
|
2471
|
+
|
|
2472
|
+
<div class="detail-section note-section">
|
|
2473
|
+
<label for="note-input" class="detail-label">Add Note</label>
|
|
2474
|
+
<form class="note-form" onsubmit="addNote(event, '${task.id}', '${actualSessionId}')">
|
|
2475
|
+
<textarea id="note-input" class="note-input" placeholder="Add a note for Claude..." rows="3"></textarea>
|
|
2476
|
+
<button type="submit" class="note-submit">Add Note</button>
|
|
2477
|
+
</form>
|
|
2478
|
+
</div>
|
|
2479
|
+
`;
|
|
2480
|
+
|
|
2481
|
+
// Setup button handlers
|
|
2482
|
+
document.getElementById('delete-task-btn').onclick = () => deleteTask(task.id, actualSessionId);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
async function addNote(event, taskId, sessionId) {
|
|
2486
|
+
event.preventDefault();
|
|
2487
|
+
const input = document.getElementById('note-input');
|
|
2488
|
+
const note = input.value.trim();
|
|
2489
|
+
if (!note) return;
|
|
2490
|
+
|
|
2491
|
+
try {
|
|
2492
|
+
const res = await fetch(`/api/tasks/${sessionId}/${taskId}/note`, {
|
|
2493
|
+
method: 'POST',
|
|
2494
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2495
|
+
body: JSON.stringify({ note })
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2498
|
+
if (res.ok) {
|
|
2499
|
+
input.value = '';
|
|
2500
|
+
// Refresh to show updated description
|
|
2501
|
+
if (viewMode === 'all') {
|
|
2502
|
+
const tasksRes = await fetch('/api/tasks/all');
|
|
2503
|
+
currentTasks = await tasksRes.json();
|
|
2504
|
+
} else {
|
|
2505
|
+
await fetchTasks(sessionId);
|
|
2506
|
+
}
|
|
2507
|
+
showTaskDetail(taskId, sessionId);
|
|
2508
|
+
}
|
|
2509
|
+
} catch (error) {
|
|
2510
|
+
console.error('Failed to add note:', error);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
function closeDetailPanel() {
|
|
2515
|
+
detailPanel.classList.remove('visible');
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
let deleteTaskId = null;
|
|
2519
|
+
let deleteSessionId = null;
|
|
2520
|
+
|
|
2521
|
+
function showBlockedTaskModal(task) {
|
|
2522
|
+
const messageDiv = document.getElementById('blocked-task-message');
|
|
2523
|
+
|
|
2524
|
+
const blockedByList = task.blockedBy.map(id => {
|
|
2525
|
+
const blockingTask = currentTasks.find(t => t.id === id);
|
|
2526
|
+
if (blockingTask) {
|
|
2527
|
+
return `<li><strong>#${blockingTask.id}</strong> - ${escapeHtml(blockingTask.subject)}</li>`;
|
|
2528
|
+
}
|
|
2529
|
+
return `<li><strong>#${id}</strong></li>`;
|
|
2530
|
+
}).join('');
|
|
2531
|
+
|
|
2532
|
+
messageDiv.innerHTML = `
|
|
2533
|
+
<p style="margin-bottom: 12px;">Task <strong>#${task.id}</strong> - ${escapeHtml(task.subject)} is currently blocked by:</p>
|
|
2534
|
+
<ul style="margin: 0 0 16px 20px; padding: 0;">${blockedByList}</ul>
|
|
2535
|
+
<p style="margin: 0; color: var(--text-secondary); font-size: 13px;">
|
|
2536
|
+
Please resolve these dependencies before moving this task to <strong>In Progress</strong>.
|
|
2537
|
+
</p>
|
|
2538
|
+
`;
|
|
2539
|
+
|
|
2540
|
+
const modal = document.getElementById('blocked-task-modal');
|
|
2541
|
+
modal.classList.add('visible');
|
|
2542
|
+
|
|
2543
|
+
// Handle ESC key
|
|
2544
|
+
const keyHandler = (e) => {
|
|
2545
|
+
if (e.key === 'Escape') {
|
|
2546
|
+
e.preventDefault();
|
|
2547
|
+
closeBlockedTaskModal();
|
|
2548
|
+
document.removeEventListener('keydown', keyHandler);
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
document.addEventListener('keydown', keyHandler);
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
function closeBlockedTaskModal() {
|
|
2555
|
+
const modal = document.getElementById('blocked-task-modal');
|
|
2556
|
+
modal.classList.remove('visible');
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
function deleteTask(taskId, sessionId) {
|
|
2560
|
+
const task = currentTasks.find(t => t.id === taskId);
|
|
2561
|
+
if (!task) return;
|
|
2562
|
+
|
|
2563
|
+
deleteTaskId = taskId;
|
|
2564
|
+
deleteSessionId = sessionId;
|
|
2565
|
+
|
|
2566
|
+
const message = document.getElementById('delete-confirm-message');
|
|
2567
|
+
message.textContent = `Delete task "${task.subject}"? This cannot be undone.`;
|
|
2568
|
+
|
|
2569
|
+
const modal = document.getElementById('delete-confirm-modal');
|
|
2570
|
+
modal.classList.add('visible');
|
|
2571
|
+
|
|
2572
|
+
// Handle ESC key
|
|
2573
|
+
const keyHandler = (e) => {
|
|
2574
|
+
if (e.key === 'Escape') {
|
|
2575
|
+
e.preventDefault();
|
|
2576
|
+
closeDeleteConfirmModal();
|
|
2577
|
+
document.removeEventListener('keydown', keyHandler);
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
document.addEventListener('keydown', keyHandler);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
function closeDeleteConfirmModal() {
|
|
2584
|
+
const modal = document.getElementById('delete-confirm-modal');
|
|
2585
|
+
modal.classList.remove('visible');
|
|
2586
|
+
deleteTaskId = null;
|
|
2587
|
+
deleteSessionId = null;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
async function confirmDelete() {
|
|
2591
|
+
if (!deleteTaskId || !deleteSessionId) return;
|
|
2592
|
+
|
|
2593
|
+
const taskId = deleteTaskId;
|
|
2594
|
+
const sessionId = deleteSessionId;
|
|
2595
|
+
|
|
2596
|
+
closeDeleteConfirmModal();
|
|
2597
|
+
|
|
2598
|
+
try {
|
|
2599
|
+
const res = await fetch(`/api/tasks/${sessionId}/${taskId}`, {
|
|
2600
|
+
method: 'DELETE'
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
if (res.ok) {
|
|
2604
|
+
closeDetailPanel();
|
|
2605
|
+
await refreshCurrentView();
|
|
2606
|
+
} else {
|
|
2607
|
+
const error = await res.json();
|
|
2608
|
+
alert('Failed to delete task: ' + (error.error || 'Unknown error'));
|
|
2609
|
+
}
|
|
2610
|
+
} catch (error) {
|
|
2611
|
+
console.error('Failed to delete task:', error);
|
|
2612
|
+
alert('Failed to delete task');
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
function showHelpModal() {
|
|
2617
|
+
const modal = document.getElementById('help-modal');
|
|
2618
|
+
modal.classList.add('visible');
|
|
2619
|
+
|
|
2620
|
+
// Handle keyboard shortcuts
|
|
2621
|
+
const keyHandler = (e) => {
|
|
2622
|
+
if (e.key === 'Escape' || e.key === '?') {
|
|
2623
|
+
e.preventDefault();
|
|
2624
|
+
closeHelpModal();
|
|
2625
|
+
document.removeEventListener('keydown', keyHandler);
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
document.addEventListener('keydown', keyHandler);
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
function closeHelpModal() {
|
|
2632
|
+
const modal = document.getElementById('help-modal');
|
|
2633
|
+
modal.classList.remove('visible');
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
async function refreshCurrentView() {
|
|
2637
|
+
fetchLiveUpdates();
|
|
2638
|
+
if (viewMode === 'all') {
|
|
2639
|
+
await showAllTasks();
|
|
2640
|
+
} else if (currentSessionId) {
|
|
2641
|
+
await fetchTasks(currentSessionId);
|
|
2642
|
+
} else {
|
|
2643
|
+
await fetchSessions();
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
document.getElementById('close-detail').onclick = closeDetailPanel;
|
|
2648
|
+
|
|
2649
|
+
document.addEventListener('keydown', (e) => {
|
|
2650
|
+
// Ignore if typing in input/textarea
|
|
2651
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
if (e.key === 'Escape' && detailPanel.classList.contains('visible')) {
|
|
2656
|
+
closeDetailPanel();
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
if (detailPanel.classList.contains('visible')) {
|
|
2660
|
+
// Get task ID from detail panel
|
|
2661
|
+
const labelElement = document.querySelector('.detail-label');
|
|
2662
|
+
if (!labelElement) return;
|
|
2663
|
+
|
|
2664
|
+
const taskId = labelElement.textContent.match(/\d+/)?.[0];
|
|
2665
|
+
if (!taskId) return;
|
|
2666
|
+
|
|
2667
|
+
const task = currentTasks.find(t => t.id === taskId);
|
|
2668
|
+
if (!task) return;
|
|
2669
|
+
|
|
2670
|
+
const sessionId = task.sessionId || currentSessionId;
|
|
2671
|
+
|
|
2672
|
+
if (e.key === 'd' || e.key === 'D') {
|
|
2673
|
+
e.preventDefault();
|
|
2674
|
+
deleteTask(taskId, sessionId);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
|
|
2679
|
+
e.preventDefault();
|
|
2680
|
+
showHelpModal();
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
|
|
2684
|
+
function setupEventSource() {
|
|
2685
|
+
let retryDelay = 1000;
|
|
2686
|
+
let eventSource;
|
|
2687
|
+
|
|
2688
|
+
function connect() {
|
|
2689
|
+
eventSource = new EventSource('/api/events');
|
|
2690
|
+
|
|
2691
|
+
eventSource.onopen = () => {
|
|
2692
|
+
retryDelay = 1000; // Reset on successful connection
|
|
2693
|
+
connectionStatus.innerHTML = `
|
|
2694
|
+
<span class="connection-dot live"></span>
|
|
2695
|
+
<span>Connected</span>
|
|
2696
|
+
`;
|
|
2697
|
+
};
|
|
2698
|
+
|
|
2699
|
+
eventSource.onerror = () => {
|
|
2700
|
+
eventSource.close();
|
|
2701
|
+
connectionStatus.innerHTML = `
|
|
2702
|
+
<span class="connection-dot error"></span>
|
|
2703
|
+
<span>Reconnecting...</span>
|
|
2704
|
+
`;
|
|
2705
|
+
setTimeout(connect, retryDelay);
|
|
2706
|
+
retryDelay = Math.min(retryDelay * 2, 30000); // Max 30s
|
|
2707
|
+
};
|
|
2708
|
+
|
|
2709
|
+
let refreshTimer = null;
|
|
2710
|
+
function debouncedRefresh(sessionId, isMetadata) {
|
|
2711
|
+
clearTimeout(refreshTimer);
|
|
2712
|
+
refreshTimer = setTimeout(() => {
|
|
2713
|
+
fetchSessions().catch(err => console.error('[SSE] fetchSessions failed:', err));
|
|
2714
|
+
if (currentSessionId && (isMetadata || sessionId === currentSessionId)) {
|
|
2715
|
+
fetchTasks(currentSessionId);
|
|
2716
|
+
}
|
|
2717
|
+
}, 500);
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
eventSource.onmessage = (event) => {
|
|
2721
|
+
const data = JSON.parse(event.data);
|
|
2722
|
+
console.log('[SSE] Event received:', data);
|
|
2723
|
+
if (data.type === 'update' || data.type === 'metadata-update') {
|
|
2724
|
+
debouncedRefresh(data.sessionId, data.type === 'metadata-update');
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
if (data.type === 'team-update') {
|
|
2728
|
+
console.log('[SSE] Team update:', data.teamName);
|
|
2729
|
+
debouncedRefresh(data.teamName, false);
|
|
2730
|
+
if (currentSessionId && data.teamName === currentSessionId) {
|
|
2731
|
+
fetchTasks(currentSessionId);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
connect();
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
function formatDate(dateStr) {
|
|
2741
|
+
const date = new Date(dateStr);
|
|
2742
|
+
const now = new Date();
|
|
2743
|
+
const diff = now - date;
|
|
2744
|
+
|
|
2745
|
+
if (diff < 60000) return 'just now';
|
|
2746
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
2747
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
2748
|
+
return date.toLocaleDateString();
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
function escapeHtml(text) {
|
|
2752
|
+
const div = document.createElement('div');
|
|
2753
|
+
div.textContent = text;
|
|
2754
|
+
return div.innerHTML;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
const ownerColors = [
|
|
2758
|
+
{ bg: 'rgba(37, 99, 235, 0.14)', color: '#1d5bbf' }, // blue
|
|
2759
|
+
{ bg: 'rgba(168, 85, 247, 0.14)', color: '#7c3aed' }, // purple
|
|
2760
|
+
{ bg: 'rgba(14, 165, 133, 0.14)', color: '#0d7d65' }, // teal
|
|
2761
|
+
{ bg: 'rgba(220, 80, 30, 0.14)', color: '#c04a1a' }, // red-orange
|
|
2762
|
+
{ bg: 'rgba(202, 138, 4, 0.14)', color: '#92700c' }, // amber
|
|
2763
|
+
{ bg: 'rgba(219, 39, 119, 0.14)', color: '#b5246a' }, // pink
|
|
2764
|
+
{ bg: 'rgba(22, 163, 74, 0.14)', color: '#15803d' }, // green
|
|
2765
|
+
{ bg: 'rgba(99, 102, 241, 0.14)', color: '#4f46e5' }, // indigo
|
|
2766
|
+
];
|
|
2767
|
+
const ownerColorCache = {};
|
|
2768
|
+
function getOwnerColor(name) {
|
|
2769
|
+
if (ownerColorCache[name]) return ownerColorCache[name];
|
|
2770
|
+
let hash = 5381;
|
|
2771
|
+
for (let i = 0; i < name.length; i++) {
|
|
2772
|
+
hash = ((hash * 33) ^ name.charCodeAt(i)) | 0;
|
|
2773
|
+
}
|
|
2774
|
+
const c = ownerColors[Math.abs(hash) % ownerColors.length];
|
|
2775
|
+
ownerColorCache[name] = c;
|
|
2776
|
+
return c;
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
function filterBySessions(value) {
|
|
2780
|
+
sessionFilter = value;
|
|
2781
|
+
localStorage.setItem('sessionFilter', sessionFilter);
|
|
2782
|
+
renderSessions();
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
function changeSessionLimit(value) {
|
|
2786
|
+
sessionLimit = value;
|
|
2787
|
+
localStorage.setItem('sessionLimit', sessionLimit);
|
|
2788
|
+
fetchSessions();
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
function filterByProject(project) {
|
|
2792
|
+
filterProject = project || null;
|
|
2793
|
+
renderSessions();
|
|
2794
|
+
fetchLiveUpdates();
|
|
2795
|
+
showAllTasks();
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
function updateProjectDropdown() {
|
|
2799
|
+
const dropdown = document.getElementById('project-filter');
|
|
2800
|
+
const projects = [...new Set(sessions.map(s => s.project).filter(Boolean))].sort();
|
|
2801
|
+
|
|
2802
|
+
dropdown.innerHTML = '<option value="">All Projects</option>' +
|
|
2803
|
+
projects.map(p => {
|
|
2804
|
+
const name = p.split('/').pop();
|
|
2805
|
+
const selected = p === filterProject ? ' selected' : '';
|
|
2806
|
+
return `<option value="${p}"${selected} title="${escapeHtml(p)}">${escapeHtml(name)}</option>`;
|
|
2807
|
+
}).join('');
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
function toggleTheme() {
|
|
2811
|
+
const isCurrentlyLight = document.body.classList.contains('light');
|
|
2812
|
+
if (isCurrentlyLight) {
|
|
2813
|
+
document.body.classList.remove('light');
|
|
2814
|
+
document.body.classList.add('dark-forced');
|
|
2815
|
+
localStorage.setItem('theme', 'dark');
|
|
2816
|
+
} else {
|
|
2817
|
+
document.body.classList.add('light');
|
|
2818
|
+
document.body.classList.remove('dark-forced');
|
|
2819
|
+
localStorage.setItem('theme', 'light');
|
|
2820
|
+
}
|
|
2821
|
+
updateThemeIcon();
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
function updateThemeIcon() {
|
|
2825
|
+
const saved = localStorage.getItem('theme');
|
|
2826
|
+
const isLight = document.body.classList.contains('light') ||
|
|
2827
|
+
(!saved && window.matchMedia('(prefers-color-scheme: light)').matches);
|
|
2828
|
+
document.getElementById('theme-icon-dark').style.display = isLight ? 'none' : 'block';
|
|
2829
|
+
document.getElementById('theme-icon-light').style.display = isLight ? 'block' : 'none';
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function loadTheme() {
|
|
2833
|
+
const saved = localStorage.getItem('theme');
|
|
2834
|
+
if (saved === 'light') {
|
|
2835
|
+
document.body.classList.add('light');
|
|
2836
|
+
document.body.classList.remove('dark-forced');
|
|
2837
|
+
} else if (saved === 'dark') {
|
|
2838
|
+
document.body.classList.remove('light');
|
|
2839
|
+
document.body.classList.add('dark-forced');
|
|
2840
|
+
}
|
|
2841
|
+
// If no saved preference, system prefers-color-scheme CSS handles it
|
|
2842
|
+
updateThemeIcon();
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
function loadPreferences() {
|
|
2846
|
+
document.getElementById('session-filter').value = sessionFilter;
|
|
2847
|
+
document.getElementById('session-limit').value = sessionLimit;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
async function showTeamModalForSession(sessionId) {
|
|
2851
|
+
const session = sessions.find(s => s.id === sessionId);
|
|
2852
|
+
if (!session || !session.isTeam) return;
|
|
2853
|
+
try {
|
|
2854
|
+
const res = await fetch(`/api/teams/${sessionId}`);
|
|
2855
|
+
if (!res.ok) return;
|
|
2856
|
+
const teamConfig = await res.json();
|
|
2857
|
+
showTeamModal(teamConfig, currentSessionId === sessionId ? currentTasks : []);
|
|
2858
|
+
} catch (e) {
|
|
2859
|
+
console.error('Failed to fetch team config:', e);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
function showTeamModal(teamConfig, tasks) {
|
|
2864
|
+
const modal = document.getElementById('team-modal');
|
|
2865
|
+
const titleEl = document.getElementById('team-modal-title');
|
|
2866
|
+
const bodyEl = document.getElementById('team-modal-body');
|
|
2867
|
+
|
|
2868
|
+
titleEl.textContent = `Team: ${teamConfig.team_name || teamConfig.name || 'Unknown'}`;
|
|
2869
|
+
|
|
2870
|
+
const ownerCounts = {};
|
|
2871
|
+
tasks.forEach(t => {
|
|
2872
|
+
if (t.owner) {
|
|
2873
|
+
ownerCounts[t.owner] = (ownerCounts[t.owner] || 0) + 1;
|
|
2874
|
+
}
|
|
2875
|
+
});
|
|
2876
|
+
|
|
2877
|
+
const members = teamConfig.members || [];
|
|
2878
|
+
const description = teamConfig.description || '';
|
|
2879
|
+
const lead = members.find(m => m.agentType === 'team-lead' || m.name === 'team-lead');
|
|
2880
|
+
|
|
2881
|
+
let html = '';
|
|
2882
|
+
if (description) {
|
|
2883
|
+
html += `<div class="team-modal-desc">"${escapeHtml(description)}"</div>`;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
html += `<div style="font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-bottom: 10px;">Members (${members.length})</div>`;
|
|
2887
|
+
|
|
2888
|
+
members.forEach(member => {
|
|
2889
|
+
const taskCount = ownerCounts[member.name] || 0;
|
|
2890
|
+
html += `
|
|
2891
|
+
<div class="team-member-card">
|
|
2892
|
+
<div class="member-name">🟢 ${escapeHtml(member.name)}</div>
|
|
2893
|
+
<div class="member-detail">Role: ${escapeHtml(member.agentType || 'unknown')}</div>
|
|
2894
|
+
${member.model ? `<div class="member-detail">Model: ${escapeHtml(member.model)}</div>` : ''}
|
|
2895
|
+
<div class="member-tasks">Tasks: ${taskCount} assigned</div>
|
|
2896
|
+
</div>
|
|
2897
|
+
`;
|
|
2898
|
+
});
|
|
2899
|
+
|
|
2900
|
+
const metaParts = [];
|
|
2901
|
+
if (teamConfig.created_at) {
|
|
2902
|
+
metaParts.push(`Created: ${new Date(teamConfig.created_at).toLocaleString()}`);
|
|
2903
|
+
}
|
|
2904
|
+
if (lead) {
|
|
2905
|
+
metaParts.push(`Lead: ${lead.name}`);
|
|
2906
|
+
}
|
|
2907
|
+
if (teamConfig.working_dir) {
|
|
2908
|
+
metaParts.push(`Working dir: ${teamConfig.working_dir}`);
|
|
2909
|
+
}
|
|
2910
|
+
if (metaParts.length > 0) {
|
|
2911
|
+
html += `<div class="team-modal-meta">${metaParts.map(p => escapeHtml(p)).join('<br>')}</div>`;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
bodyEl.innerHTML = html;
|
|
2915
|
+
modal.classList.add('visible');
|
|
2916
|
+
|
|
2917
|
+
const keyHandler = (e) => {
|
|
2918
|
+
if (e.key === 'Escape') {
|
|
2919
|
+
e.preventDefault();
|
|
2920
|
+
closeTeamModal();
|
|
2921
|
+
document.removeEventListener('keydown', keyHandler);
|
|
2922
|
+
}
|
|
2923
|
+
};
|
|
2924
|
+
document.addEventListener('keydown', keyHandler);
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
function closeTeamModal() {
|
|
2928
|
+
document.getElementById('team-modal').classList.remove('visible');
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
function updateOwnerFilter() {
|
|
2932
|
+
const bar = document.getElementById('owner-filter-bar');
|
|
2933
|
+
const select = document.getElementById('owner-filter');
|
|
2934
|
+
|
|
2935
|
+
const session = sessions.find(s => s.id === currentSessionId);
|
|
2936
|
+
if (!session || !session.isTeam) {
|
|
2937
|
+
bar.classList.remove('visible');
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
bar.classList.add('visible');
|
|
2942
|
+
const owners = [...new Set(currentTasks.map(t => t.owner).filter(Boolean))].sort();
|
|
2943
|
+
select.innerHTML = '<option value="">All Members</option>' +
|
|
2944
|
+
owners.map(o => {
|
|
2945
|
+
const c = getOwnerColor(o);
|
|
2946
|
+
return `<option value="${escapeHtml(o)}" style="color:${c.color};background:${c.bg}"${o === ownerFilter ? ' selected' : ''}>${escapeHtml(o)}</option>`;
|
|
2947
|
+
}).join('');
|
|
2948
|
+
const current = ownerFilter ? getOwnerColor(ownerFilter) : null;
|
|
2949
|
+
select.style.color = current ? current.color : '';
|
|
2950
|
+
select.style.backgroundColor = current ? current.bg : '';
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
function filterByOwner(value) {
|
|
2954
|
+
ownerFilter = value;
|
|
2955
|
+
const select = document.getElementById('owner-filter');
|
|
2956
|
+
const c = value ? getOwnerColor(value) : null;
|
|
2957
|
+
select.style.color = c ? c.color : '';
|
|
2958
|
+
select.style.backgroundColor = c ? c.bg : '';
|
|
2959
|
+
renderKanban();
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// Init
|
|
2963
|
+
loadTheme();
|
|
2964
|
+
loadPreferences();
|
|
2965
|
+
setupEventSource();
|
|
2966
|
+
|
|
2967
|
+
// Fetch sessions and show newest one by default
|
|
2968
|
+
fetchSessions().then(() => {
|
|
2969
|
+
if (sessions.length > 0) {
|
|
2970
|
+
// Sessions are already sorted by newest first from API
|
|
2971
|
+
fetchTasks(sessions[0].id);
|
|
2972
|
+
} else {
|
|
2973
|
+
showAllTasks();
|
|
2974
|
+
}
|
|
2975
|
+
});
|
|
2976
|
+
</script>
|
|
2977
|
+
|
|
2978
|
+
<!-- Help Modal -->
|
|
2979
|
+
<div id="help-modal" class="modal-overlay" onclick="closeHelpModal()">
|
|
2980
|
+
<div class="modal" onclick="event.stopPropagation()">
|
|
2981
|
+
<div class="modal-header">
|
|
2982
|
+
<h3 class="modal-title">Keyboard Shortcuts</h3>
|
|
2983
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closeHelpModal()">
|
|
2984
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2985
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
2986
|
+
</svg>
|
|
2987
|
+
</button>
|
|
2988
|
+
</div>
|
|
2989
|
+
<div class="modal-body">
|
|
2990
|
+
<div style="display: grid; gap: 16px;">
|
|
2991
|
+
<div>
|
|
2992
|
+
<h4 style="margin: 0 0 8px 0; color: var(--text-primary); font-size: 14px; font-weight: 600;">Global</h4>
|
|
2993
|
+
<table style="width: 100%; font-size: 13px;">
|
|
2994
|
+
<tr>
|
|
2995
|
+
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">?</kbd></td>
|
|
2996
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Show keyboard shortcuts</td>
|
|
2997
|
+
</tr>
|
|
2998
|
+
<tr>
|
|
2999
|
+
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Esc</kbd></td>
|
|
3000
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Close panels or cancel</td>
|
|
3001
|
+
</tr>
|
|
3002
|
+
</table>
|
|
3003
|
+
</div>
|
|
3004
|
+
<div>
|
|
3005
|
+
<h4 style="margin: 0 0 8px 0; color: var(--text-primary); font-size: 14px; font-weight: 600;">Task Actions</h4>
|
|
3006
|
+
<table style="width: 100%; font-size: 13px;">
|
|
3007
|
+
<tr>
|
|
3008
|
+
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">D</kbd></td>
|
|
3009
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Delete selected task</td>
|
|
3010
|
+
</tr>
|
|
3011
|
+
</table>
|
|
3012
|
+
</div>
|
|
3013
|
+
</div>
|
|
3014
|
+
</div>
|
|
3015
|
+
<div class="modal-footer">
|
|
3016
|
+
<button class="btn btn-primary" onclick="closeHelpModal()">Got it</button>
|
|
3017
|
+
</div>
|
|
3018
|
+
</div>
|
|
3019
|
+
</div>
|
|
3020
|
+
|
|
3021
|
+
<!-- Delete Confirmation Modal -->
|
|
3022
|
+
<div id="delete-confirm-modal" class="modal-overlay" onclick="closeDeleteConfirmModal()">
|
|
3023
|
+
<div class="modal" onclick="event.stopPropagation()" style="max-width: 400px;">
|
|
3024
|
+
<div class="modal-header">
|
|
3025
|
+
<h3 class="modal-title">Delete Task</h3>
|
|
3026
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closeDeleteConfirmModal()">
|
|
3027
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
3028
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
3029
|
+
</svg>
|
|
3030
|
+
</button>
|
|
3031
|
+
</div>
|
|
3032
|
+
<div class="modal-body">
|
|
3033
|
+
<p id="delete-confirm-message" style="margin: 0; color: var(--text-primary);"></p>
|
|
3034
|
+
</div>
|
|
3035
|
+
<div class="modal-footer">
|
|
3036
|
+
<button class="btn btn-secondary" onclick="closeDeleteConfirmModal()">Cancel</button>
|
|
3037
|
+
<button class="btn btn-primary" onclick="confirmDelete()" style="background: #ef4444; border-color: #ef4444;">Delete</button>
|
|
3038
|
+
</div>
|
|
3039
|
+
</div>
|
|
3040
|
+
</div>
|
|
3041
|
+
|
|
3042
|
+
<!-- Delete All Session Tasks Confirmation Modal -->
|
|
3043
|
+
<div id="delete-session-tasks-modal" class="modal-overlay" onclick="closeDeleteSessionTasksModal()">
|
|
3044
|
+
<div class="modal" onclick="event.stopPropagation()" style="max-width: 500px;">
|
|
3045
|
+
<div class="modal-header">
|
|
3046
|
+
<h3 class="modal-title">Delete All Tasks</h3>
|
|
3047
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closeDeleteSessionTasksModal()">
|
|
3048
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
3049
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
3050
|
+
</svg>
|
|
3051
|
+
</button>
|
|
3052
|
+
</div>
|
|
3053
|
+
<div class="modal-body">
|
|
3054
|
+
<p id="delete-session-tasks-message" style="margin: 0 0 12px 0; color: var(--text-primary);"></p>
|
|
3055
|
+
<p style="margin: 0; font-size: 13px; color: var(--text-secondary);">This action cannot be undone.</p>
|
|
3056
|
+
</div>
|
|
3057
|
+
<div class="modal-footer">
|
|
3058
|
+
<button class="btn btn-secondary" onclick="closeDeleteSessionTasksModal()">Cancel</button>
|
|
3059
|
+
<button class="btn btn-primary" onclick="confirmDeleteSessionTasks()" style="background: #ef4444; border-color: #ef4444;">Delete All</button>
|
|
3060
|
+
</div>
|
|
3061
|
+
</div>
|
|
3062
|
+
</div>
|
|
3063
|
+
|
|
3064
|
+
<!-- Delete Result Modal -->
|
|
3065
|
+
<div id="delete-result-modal" class="modal-overlay" onclick="closeDeleteResultModal()">
|
|
3066
|
+
<div class="modal" onclick="event.stopPropagation()" style="max-width: 500px;">
|
|
3067
|
+
<div class="modal-header">
|
|
3068
|
+
<h3 class="modal-title">Deletion Result</h3>
|
|
3069
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closeDeleteResultModal()">
|
|
3070
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
3071
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
3072
|
+
</svg>
|
|
3073
|
+
</button>
|
|
3074
|
+
</div>
|
|
3075
|
+
<div class="modal-body">
|
|
3076
|
+
<p id="delete-result-message" style="margin: 0; color: var(--text-primary);"></p>
|
|
3077
|
+
<div id="delete-result-details" style="margin-top: 12px; font-size: 13px; color: var(--text-secondary);"></div>
|
|
3078
|
+
</div>
|
|
3079
|
+
<div class="modal-footer">
|
|
3080
|
+
<button class="btn btn-primary" onclick="closeDeleteResultModal()">Close</button>
|
|
3081
|
+
</div>
|
|
3082
|
+
</div>
|
|
3083
|
+
</div>
|
|
3084
|
+
|
|
3085
|
+
<!-- Team Info Modal -->
|
|
3086
|
+
<div id="team-modal" class="modal-overlay" onclick="closeTeamModal()">
|
|
3087
|
+
<div class="modal" onclick="event.stopPropagation()" style="max-width: 520px; max-height: 80vh; display: flex; flex-direction: column;">
|
|
3088
|
+
<div class="modal-header">
|
|
3089
|
+
<h3 id="team-modal-title" class="modal-title">Team</h3>
|
|
3090
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closeTeamModal()">
|
|
3091
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
3092
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
3093
|
+
</svg>
|
|
3094
|
+
</button>
|
|
3095
|
+
</div>
|
|
3096
|
+
<div id="team-modal-body" class="modal-body" style="overflow-y: auto; flex: 1;"></div>
|
|
3097
|
+
<div class="modal-footer">
|
|
3098
|
+
<button class="btn btn-primary" onclick="closeTeamModal()">Close</button>
|
|
3099
|
+
</div>
|
|
3100
|
+
</div>
|
|
3101
|
+
</div>
|
|
3102
|
+
|
|
3103
|
+
<!-- Blocked Task Warning Modal -->
|
|
3104
|
+
<div id="blocked-task-modal" class="modal-overlay" onclick="closeBlockedTaskModal()">
|
|
3105
|
+
<div class="modal" onclick="event.stopPropagation()" style="max-width: 450px;">
|
|
3106
|
+
<div class="modal-header">
|
|
3107
|
+
<h3 class="modal-title" style="display: flex; align-items: center; gap: 8px;">
|
|
3108
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" style="width: 20px; height: 20px;">
|
|
3109
|
+
<circle cx="12" cy="12" r="10"/>
|
|
3110
|
+
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
3111
|
+
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
3112
|
+
</svg>
|
|
3113
|
+
Cannot Start Blocked Task
|
|
3114
|
+
</h3>
|
|
3115
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closeBlockedTaskModal()">
|
|
3116
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
3117
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
3118
|
+
</svg>
|
|
3119
|
+
</button>
|
|
3120
|
+
</div>
|
|
3121
|
+
<div class="modal-body">
|
|
3122
|
+
<div id="blocked-task-message" style="color: var(--text-primary); line-height: 1.6;"></div>
|
|
3123
|
+
</div>
|
|
3124
|
+
<div class="modal-footer">
|
|
3125
|
+
<button class="btn btn-primary" onclick="closeBlockedTaskModal()">OK</button>
|
|
3126
|
+
</div>
|
|
3127
|
+
</div>
|
|
3128
|
+
</div>
|
|
3129
|
+
</body>
|
|
3130
|
+
</html>
|