claudeboard 2.15.4 → 3.1.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.
@@ -1,1980 +0,0 @@
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>ClaudeBoard</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
8
- <style>
9
- :root {
10
- --bg: #1a1d27;
11
- --bg2: #141620;
12
- --col-bg: #1e2130;
13
- --col-header: #252840;
14
- --card-bg: #2a2d42;
15
- --card-hover: #313550;
16
- --border: #323654;
17
- --text: #e2e4f0;
18
- --muted: #6b7094;
19
- --dim: #454868;
20
- --accent: #6c8aff;
21
- --green: #4ade80;
22
- --yellow: #fbbf24;
23
- --red: #f87171;
24
- --purple: #c084fc;
25
- --orange: #fb923c;
26
- --todo-col: #6b7094;
27
- --prog-col: #fbbf24;
28
- --done-col: #4ade80;
29
- --err-col: #f87171;
30
- --font: 'Inter', sans-serif;
31
- --mono: 'JetBrains Mono', monospace;
32
- }
33
-
34
- * { margin: 0; padding: 0; box-sizing: border-box; }
35
-
36
- html, body {
37
- height: 100%;
38
- background: var(--bg2);
39
- color: var(--text);
40
- font-family: var(--font);
41
- overflow: hidden;
42
- }
43
-
44
- /* ── HEADER ── */
45
- .header {
46
- height: 56px;
47
- background: var(--bg);
48
- border-bottom: 1px solid var(--border);
49
- display: flex;
50
- align-items: center;
51
- padding: 0 20px;
52
- gap: 16px;
53
- flex-shrink: 0;
54
- position: relative;
55
- z-index: 10;
56
- }
57
-
58
- .logo {
59
- display: flex;
60
- align-items: center;
61
- gap: 8px;
62
- font-size: 14px;
63
- font-weight: 700;
64
- letter-spacing: 0.08em;
65
- color: var(--accent);
66
- text-transform: uppercase;
67
- }
68
-
69
- .logo-pulse {
70
- width: 8px; height: 8px;
71
- background: var(--accent);
72
- border-radius: 50%;
73
- box-shadow: 0 0 0 0 rgba(108,138,255,0.4);
74
- animation: pulse-ring 2s ease-out infinite;
75
- }
76
-
77
- @keyframes pulse-ring {
78
- 0% { box-shadow: 0 0 0 0 rgba(108,138,255,0.5); }
79
- 70% { box-shadow: 0 0 0 6px rgba(108,138,255,0); }
80
- 100% { box-shadow: 0 0 0 0 rgba(108,138,255,0); }
81
- }
82
-
83
- .header-sep { width: 1px; height: 24px; background: var(--border); }
84
-
85
- .project-badge {
86
- background: rgba(108,138,255,0.12);
87
- border: 1px solid rgba(108,138,255,0.25);
88
- border-radius: 6px;
89
- padding: 4px 10px;
90
- font-size: 12px;
91
- font-weight: 600;
92
- color: var(--accent);
93
- letter-spacing: 0.05em;
94
- }
95
-
96
- .header-stats {
97
- display: flex;
98
- align-items: center;
99
- gap: 4px;
100
- margin-left: 8px;
101
- }
102
-
103
- .hstat {
104
- display: flex;
105
- align-items: center;
106
- gap: 5px;
107
- padding: 4px 10px;
108
- border-radius: 6px;
109
- font-size: 12px;
110
- font-weight: 600;
111
- font-family: var(--mono);
112
- }
113
-
114
- .hstat.todo { background: rgba(107,112,148,0.15); color: var(--todo-col); }
115
- .hstat.prog { background: rgba(251,191,36,0.12); color: var(--yellow); }
116
- .hstat.done { background: rgba(74,222,128,0.12); color: var(--green); }
117
- .hstat.err { background: rgba(248,113,113,0.12); color: var(--red); }
118
-
119
- .hstat-dot { width: 6px; height: 6px; border-radius: 50%; }
120
- .hstat.todo .hstat-dot { background: var(--todo-col); }
121
- .hstat.prog .hstat-dot { background: var(--yellow); animation: blink 1s ease-in-out infinite; }
122
- .hstat.done .hstat-dot { background: var(--green); }
123
- .hstat.err .hstat-dot { background: var(--red); }
124
-
125
- @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
126
-
127
- .header-right {
128
- margin-left: auto;
129
- display: flex;
130
- align-items: center;
131
- gap: 10px;
132
- }
133
-
134
- .progress-wrap {
135
- width: 120px;
136
- height: 6px;
137
- background: var(--border);
138
- border-radius: 3px;
139
- overflow: hidden;
140
- }
141
-
142
- .progress-fill {
143
- height: 100%;
144
- background: linear-gradient(90deg, var(--accent), var(--green));
145
- border-radius: 3px;
146
- transition: width 0.8s ease;
147
- }
148
-
149
- .progress-pct {
150
- font-family: var(--mono);
151
- font-size: 11px;
152
- color: var(--muted);
153
- min-width: 32px;
154
- }
155
-
156
- .ws-badge {
157
- display: flex;
158
- align-items: center;
159
- gap: 5px;
160
- font-size: 11px;
161
- color: var(--muted);
162
- font-family: var(--mono);
163
- }
164
-
165
- .ws-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--dim); }
166
- .ws-dot.on { background: var(--green); box-shadow: 0 0 6px var(--green); }
167
-
168
- .btn {
169
- display: flex;
170
- align-items: center;
171
- gap: 6px;
172
- padding: 6px 12px;
173
- border-radius: 6px;
174
- font-size: 12px;
175
- font-weight: 600;
176
- cursor: pointer;
177
- border: none;
178
- transition: all 0.15s;
179
- }
180
-
181
- .btn-primary {
182
- background: var(--accent);
183
- color: #fff;
184
- }
185
- .btn-primary:hover { background: #7d9cff; }
186
-
187
- .btn-ghost {
188
- background: rgba(255,255,255,0.06);
189
- color: var(--text);
190
- border: 1px solid var(--border);
191
- }
192
- .btn-ghost:hover { background: rgba(255,255,255,0.1); }
193
-
194
- /* ── CURRENT TASK BAR ── */
195
- .running-bar {
196
- height: 36px;
197
- background: rgba(251,191,36,0.06);
198
- border-bottom: 1px solid rgba(251,191,36,0.15);
199
- display: none;
200
- align-items: center;
201
- padding: 0 20px;
202
- gap: 10px;
203
- flex-shrink: 0;
204
- }
205
-
206
- .running-bar.visible { display: flex; }
207
-
208
- .running-spinner {
209
- width: 14px; height: 14px;
210
- border: 2px solid rgba(251,191,36,0.3);
211
- border-top-color: var(--yellow);
212
- border-radius: 50%;
213
- animation: spin 0.7s linear infinite;
214
- }
215
-
216
- @keyframes spin { to { transform: rotate(360deg); } }
217
-
218
- .running-label { font-family: var(--mono); font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; }
219
- .running-title { font-family: var(--mono); font-size: 11px; color: var(--yellow); }
220
-
221
- /* ── MAIN LAYOUT ── */
222
- .main {
223
- display: flex;
224
- flex: 1;
225
- overflow: hidden;
226
- height: calc(100vh - 56px);
227
- }
228
-
229
- /* ── KANBAN BOARD ── */
230
- .board {
231
- flex: 1;
232
- overflow-x: auto;
233
- overflow-y: hidden;
234
- padding: 20px;
235
- display: flex;
236
- gap: 14px;
237
- align-items: flex-start;
238
- }
239
-
240
- .board::-webkit-scrollbar { height: 6px; }
241
- .board::-webkit-scrollbar-track { background: transparent; }
242
- .board::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
243
- .board::-webkit-scrollbar-thumb:hover { background: var(--dim); }
244
-
245
- /* ── KANBAN COLUMN ── */
246
- .column {
247
- flex-shrink: 0;
248
- width: 300px;
249
- background: var(--col-bg);
250
- border-radius: 12px;
251
- border: 1px solid var(--border);
252
- display: flex;
253
- flex-direction: column;
254
- max-height: calc(100vh - 110px);
255
- }
256
-
257
- .column-header {
258
- padding: 14px 14px 10px;
259
- display: flex;
260
- align-items: center;
261
- gap: 8px;
262
- border-bottom: 1px solid var(--border);
263
- flex-shrink: 0;
264
- background: var(--col-header);
265
- border-radius: 12px 12px 0 0;
266
- }
267
-
268
- .column-dot {
269
- width: 10px; height: 10px;
270
- border-radius: 50%;
271
- flex-shrink: 0;
272
- }
273
-
274
- .column-title {
275
- font-size: 13px;
276
- font-weight: 700;
277
- flex: 1;
278
- letter-spacing: 0.03em;
279
- }
280
-
281
- .column-count {
282
- background: rgba(255,255,255,0.08);
283
- border-radius: 12px;
284
- padding: 2px 8px;
285
- font-size: 11px;
286
- font-weight: 700;
287
- font-family: var(--mono);
288
- color: var(--muted);
289
- }
290
-
291
- .column-body {
292
- flex: 1;
293
- overflow-y: auto;
294
- padding: 10px;
295
- display: flex;
296
- flex-direction: column;
297
- gap: 8px;
298
- min-height: 60px;
299
- }
300
-
301
- .column-body::-webkit-scrollbar { width: 3px; }
302
- .column-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
303
-
304
- /* Column color accents */
305
- .col-todo .column-dot { background: var(--todo-col); }
306
- .col-todo .column-title { color: var(--todo-col); }
307
- .col-prog .column-dot { background: var(--yellow); box-shadow: 0 0 8px rgba(251,191,36,0.4); }
308
- .col-prog .column-title { color: var(--yellow); }
309
- .col-done .column-dot { background: var(--green); }
310
- .col-done .column-title { color: var(--green); }
311
- .col-err .column-dot { background: var(--red); }
312
- .col-err .column-title { color: var(--red); }
313
-
314
- /* ── TASK CARD ── */
315
- .card {
316
- background: var(--card-bg);
317
- border: 1px solid var(--border);
318
- border-radius: 8px;
319
- padding: 12px;
320
- cursor: grab;
321
- transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
322
- user-select: none;
323
- position: relative;
324
- }
325
-
326
- .card:hover {
327
- background: var(--card-hover);
328
- border-color: var(--dim);
329
- box-shadow: 0 4px 20px rgba(0,0,0,0.3);
330
- transform: translateY(-1px);
331
- }
332
-
333
- .card:active { cursor: grabbing; }
334
-
335
- .card.dragging {
336
- opacity: 0.5;
337
- transform: rotate(2deg) scale(0.98);
338
- }
339
-
340
- .card.drag-over {
341
- border-color: var(--accent);
342
- box-shadow: 0 0 0 2px rgba(108,138,255,0.3);
343
- }
344
-
345
- .card-top {
346
- display: flex;
347
- align-items: flex-start;
348
- gap: 8px;
349
- margin-bottom: 8px;
350
- }
351
-
352
- .card-status {
353
- width: 16px; height: 16px;
354
- border-radius: 50%;
355
- flex-shrink: 0;
356
- margin-top: 1px;
357
- display: flex;
358
- align-items: center;
359
- justify-content: center;
360
- font-size: 9px;
361
- }
362
-
363
- .card-status.todo { border: 1.5px solid var(--dim); }
364
- .card-status.in_progress {
365
- border: 1.5px solid var(--yellow);
366
- background: rgba(251,191,36,0.1);
367
- animation: spin-border 1.2s linear infinite;
368
- }
369
- @keyframes spin-border {
370
- from { box-shadow: inset 0 0 0 1px transparent, 0 0 0 1px var(--yellow); }
371
- }
372
- .card-status.done { background: rgba(74,222,128,0.15); border: 1.5px solid var(--green); color: var(--green); }
373
- .card-status.error { background: rgba(248,113,113,0.15); border: 1.5px solid var(--red); color: var(--red); }
374
- .card-status.blocked { border: 1.5px solid var(--orange); background: rgba(251,146,60,0.1); }
375
-
376
- .card-title {
377
- font-size: 13px;
378
- font-weight: 500;
379
- line-height: 1.4;
380
- color: var(--text);
381
- flex: 1;
382
- }
383
-
384
- .card-desc {
385
- font-size: 11px;
386
- color: var(--muted);
387
- line-height: 1.5;
388
- margin-bottom: 10px;
389
- display: -webkit-box;
390
- -webkit-line-clamp: 2;
391
- -webkit-box-orient: vertical;
392
- overflow: hidden;
393
- }
394
-
395
- .card-footer {
396
- display: flex;
397
- align-items: center;
398
- gap: 5px;
399
- flex-wrap: wrap;
400
- }
401
-
402
- .tag {
403
- padding: 2px 7px;
404
- border-radius: 4px;
405
- font-size: 10px;
406
- font-weight: 700;
407
- text-transform: uppercase;
408
- letter-spacing: 0.04em;
409
- font-family: var(--mono);
410
- }
411
-
412
- .tag.high { background: rgba(248,113,113,0.15); color: var(--red); border: 1px solid rgba(248,113,113,0.2); }
413
- .tag.medium { background: rgba(251,191,36,0.12); color: var(--yellow); border: 1px solid rgba(251,191,36,0.2); }
414
- .tag.low { background: rgba(107,112,148,0.15); color: var(--muted); border: 1px solid rgba(107,112,148,0.2); }
415
- .tag.feature { background: rgba(108,138,255,0.12); color: var(--accent); border: 1px solid rgba(108,138,255,0.2); }
416
- .tag.bug { background: rgba(248,113,113,0.12); color: var(--red); border: 1px solid rgba(248,113,113,0.2); }
417
- .tag.config { background: rgba(192,132,252,0.12); color: var(--purple); border: 1px solid rgba(192,132,252,0.2); }
418
- .tag.refactor { background: rgba(251,146,60,0.12); color: var(--orange); border: 1px solid rgba(251,146,60,0.2); }
419
- .tag.test { background: rgba(74,222,128,0.12); color: var(--green); border: 1px solid rgba(74,222,128,0.2); }
420
-
421
- .card-epic {
422
- margin-left: auto;
423
- font-size: 9px;
424
- font-family: var(--mono);
425
- color: var(--dim);
426
- text-transform: uppercase;
427
- letter-spacing: 0.08em;
428
- max-width: 80px;
429
- white-space: nowrap;
430
- overflow: hidden;
431
- text-overflow: ellipsis;
432
- }
433
-
434
- /* Drop placeholder */
435
- .drop-placeholder {
436
- height: 60px;
437
- border: 2px dashed rgba(108,138,255,0.3);
438
- border-radius: 8px;
439
- background: rgba(108,138,255,0.05);
440
- flex-shrink: 0;
441
- }
442
-
443
- /* ── SIDEBAR ── */
444
- .sidebar {
445
- width: 320px;
446
- flex-shrink: 0;
447
- background: var(--bg);
448
- border-left: 1px solid var(--border);
449
- display: flex;
450
- flex-direction: column;
451
- overflow: hidden;
452
- }
453
-
454
- .sidebar-tabs {
455
- display: flex;
456
- border-bottom: 1px solid var(--border);
457
- flex-shrink: 0;
458
- }
459
-
460
- .stab {
461
- flex: 1;
462
- padding: 12px;
463
- font-size: 11px;
464
- font-weight: 600;
465
- text-transform: uppercase;
466
- letter-spacing: 0.1em;
467
- color: var(--muted);
468
- cursor: pointer;
469
- background: none;
470
- border: none;
471
- border-bottom: 2px solid transparent;
472
- transition: all 0.15s;
473
- font-family: var(--mono);
474
- }
475
-
476
- .stab.active { color: var(--accent); border-bottom-color: var(--accent); }
477
- .stab:hover:not(.active) { color: var(--text); }
478
-
479
- .sidebar-body {
480
- flex: 1;
481
- overflow-y: auto;
482
- padding: 12px;
483
- }
484
-
485
- .sidebar-body::-webkit-scrollbar { width: 3px; }
486
- .sidebar-body::-webkit-scrollbar-thumb { background: var(--border); }
487
-
488
- /* ── LOG ENTRIES ── */
489
- .log-entry {
490
- display: flex;
491
- gap: 8px;
492
- padding: 8px 0;
493
- border-bottom: 1px solid rgba(50,54,84,0.5);
494
- font-family: var(--mono);
495
- font-size: 10px;
496
- line-height: 1.5;
497
- }
498
-
499
- .log-entry:last-child { border-bottom: none; }
500
- .log-time { color: var(--dim); flex-shrink: 0; }
501
- .log-icon { flex-shrink: 0; width: 12px; text-align: center; }
502
- .log-msg { color: var(--text); word-break: break-word; }
503
- .log-msg.start { color: var(--accent); }
504
- .log-msg.complete { color: var(--green); }
505
- .log-msg.error { color: var(--red); }
506
-
507
- /* ── DETAIL PANEL ── */
508
- .detail-empty {
509
- text-align: center;
510
- padding: 40px 16px;
511
- font-family: var(--mono);
512
- font-size: 11px;
513
- color: var(--dim);
514
- line-height: 2;
515
- }
516
-
517
- .detail-title {
518
- font-size: 14px;
519
- font-weight: 600;
520
- line-height: 1.4;
521
- margin-bottom: 10px;
522
- }
523
-
524
- .detail-tags { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 12px; }
525
-
526
- .detail-desc {
527
- font-size: 11px;
528
- font-family: var(--mono);
529
- color: var(--muted);
530
- line-height: 1.7;
531
- margin-bottom: 14px;
532
- padding: 10px;
533
- background: var(--col-bg);
534
- border-radius: 6px;
535
- border: 1px solid var(--border);
536
- }
537
-
538
- .detail-logs-title {
539
- font-family: var(--mono);
540
- font-size: 10px;
541
- color: var(--dim);
542
- text-transform: uppercase;
543
- letter-spacing: 0.1em;
544
- margin-bottom: 8px;
545
- }
546
-
547
- /* ── MODAL ── */
548
- .overlay {
549
- position: fixed;
550
- inset: 0;
551
- background: rgba(0,0,0,0.6);
552
- backdrop-filter: blur(4px);
553
- display: none;
554
- align-items: center;
555
- justify-content: center;
556
- z-index: 100;
557
- }
558
-
559
- .overlay.open { display: flex; }
560
-
561
- .modal {
562
- background: var(--bg);
563
- border: 1px solid var(--border);
564
- border-radius: 12px;
565
- width: 480px;
566
- max-width: 90vw;
567
- padding: 24px;
568
- box-shadow: 0 20px 60px rgba(0,0,0,0.5);
569
- }
570
-
571
- .modal-title {
572
- font-size: 14px;
573
- font-weight: 700;
574
- margin-bottom: 20px;
575
- color: var(--text);
576
- }
577
-
578
- .field { margin-bottom: 14px; }
579
-
580
- .field label {
581
- display: block;
582
- font-size: 11px;
583
- font-weight: 600;
584
- color: var(--muted);
585
- text-transform: uppercase;
586
- letter-spacing: 0.08em;
587
- margin-bottom: 5px;
588
- font-family: var(--mono);
589
- }
590
-
591
- .field input, .field textarea, .field select {
592
- width: 100%;
593
- background: var(--col-bg);
594
- border: 1px solid var(--border);
595
- border-radius: 6px;
596
- padding: 8px 12px;
597
- color: var(--text);
598
- font-family: var(--font);
599
- font-size: 13px;
600
- outline: none;
601
- resize: vertical;
602
- transition: border-color 0.15s;
603
- }
604
-
605
- .field input:focus, .field textarea:focus, .field select:focus {
606
- border-color: var(--accent);
607
- box-shadow: 0 0 0 3px rgba(108,138,255,0.1);
608
- }
609
-
610
- .field select option { background: var(--bg); }
611
-
612
- .modal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
613
-
614
- .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; }
615
-
616
- .btn-cancel {
617
- padding: 8px 16px;
618
- background: none;
619
- border: 1px solid var(--border);
620
- border-radius: 6px;
621
- color: var(--muted);
622
- font-size: 13px;
623
- cursor: pointer;
624
- font-family: var(--font);
625
- transition: all 0.15s;
626
- }
627
- .btn-cancel:hover { border-color: var(--text); color: var(--text); }
628
-
629
- .btn-create {
630
- padding: 8px 20px;
631
- background: var(--accent);
632
- border: none;
633
- border-radius: 6px;
634
- color: #fff;
635
- font-size: 13px;
636
- font-weight: 600;
637
- cursor: pointer;
638
- font-family: var(--font);
639
- transition: opacity 0.15s;
640
- }
641
- .btn-create:hover { opacity: 0.85; }
642
-
643
- /* ── EXPO PANEL ── */
644
- .expo-panel {
645
- position: fixed;
646
- bottom: 0; left: 0; right: 320px;
647
- background: var(--bg);
648
- border-top: 1px solid var(--border);
649
- z-index: 20;
650
- transition: transform 0.25s ease;
651
- transform: translateY(100%);
652
- }
653
- .expo-panel.open { transform: translateY(0); }
654
-
655
- .expo-panel-header {
656
- display: flex;
657
- align-items: center;
658
- gap: 10px;
659
- padding: 10px 16px;
660
- border-bottom: 1px solid var(--border);
661
- cursor: pointer;
662
- user-select: none;
663
- }
664
-
665
- .expo-panel-title {
666
- font-family: var(--mono);
667
- font-size: 12px;
668
- font-weight: 600;
669
- color: var(--text);
670
- }
671
-
672
- .expo-logs {
673
- height: 160px;
674
- overflow-y: auto;
675
- padding: 10px 16px;
676
- font-family: var(--mono);
677
- font-size: 11px;
678
- color: var(--muted);
679
- line-height: 1.6;
680
- }
681
-
682
- .expo-logs::-webkit-scrollbar { width: 3px; }
683
- .expo-logs::-webkit-scrollbar-thumb { background: var(--border); }
684
-
685
- .expo-qr-wrap {
686
- padding: 12px 16px;
687
- display: flex;
688
- align-items: center;
689
- gap: 16px;
690
- border-top: 1px solid var(--border);
691
- }
692
-
693
- .expo-url {
694
- font-family: var(--mono);
695
- font-size: 11px;
696
- color: var(--accent);
697
- word-break: break-all;
698
- }
699
-
700
- .expo-status-badge {
701
- padding: 3px 10px;
702
- border-radius: 12px;
703
- font-family: var(--mono);
704
- font-size: 10px;
705
- font-weight: 700;
706
- text-transform: uppercase;
707
- }
708
-
709
- .expo-status-badge.stopped { background: rgba(107,112,148,0.2); color: var(--muted); }
710
- .expo-status-badge.installing { background: rgba(251,191,36,0.15); color: var(--yellow); }
711
- .expo-status-badge.starting { background: rgba(251,191,36,0.15); color: var(--yellow); }
712
- .expo-status-badge.running { background: rgba(74,222,128,0.15); color: var(--green); }
713
- .expo-status-badge.error { background: rgba(248,113,113,0.15); color: var(--red); }
714
-
715
- /* ── TERMINAL PANEL ── */
716
- .term-panel {
717
- position: fixed;
718
- bottom: 0; left: 0; right: 320px;
719
- height: 320px;
720
- background: #0d0f1a;
721
- border-top: 2px solid var(--border);
722
- z-index: 19;
723
- display: flex;
724
- flex-direction: column;
725
- transform: translateY(100%);
726
- transition: transform 0.25s ease;
727
- }
728
-
729
- .term-panel.open { transform: translateY(0); }
730
-
731
- .term-header {
732
- display: flex;
733
- align-items: center;
734
- gap: 10px;
735
- padding: 8px 14px;
736
- background: #111320;
737
- border-bottom: 1px solid var(--border);
738
- flex-shrink: 0;
739
- }
740
-
741
- .term-title {
742
- font-family: var(--mono);
743
- font-size: 11px;
744
- color: var(--muted);
745
- text-transform: uppercase;
746
- letter-spacing: 0.1em;
747
- }
748
-
749
- #terminal {
750
- flex: 1;
751
- overflow: hidden;
752
- padding: 4px;
753
- }
754
-
755
- .btn-term-close {
756
- margin-left: auto;
757
- background: none;
758
- border: none;
759
- color: var(--muted);
760
- cursor: pointer;
761
- font-size: 16px;
762
- line-height: 1;
763
- padding: 2px 6px;
764
- }
765
- .btn-term-close:hover { color: var(--text); }
766
-
767
- /* ── TOOLBAR BOTTOM ── */
768
- .bottom-toolbar {
769
- position: fixed;
770
- bottom: 0; left: 0; right: 320px;
771
- height: 40px;
772
- background: var(--bg);
773
- border-top: 1px solid var(--border);
774
- display: flex;
775
- align-items: center;
776
- padding: 0 16px;
777
- gap: 8px;
778
- z-index: 15;
779
- }
780
-
781
- .toolbar-btn {
782
- display: flex;
783
- align-items: center;
784
- gap: 6px;
785
- padding: 5px 12px;
786
- border-radius: 5px;
787
- font-family: var(--mono);
788
- font-size: 11px;
789
- font-weight: 600;
790
- cursor: pointer;
791
- border: 1px solid var(--border);
792
- background: rgba(255,255,255,0.04);
793
- color: var(--muted);
794
- transition: all 0.15s;
795
- }
796
-
797
- .toolbar-btn:hover { color: var(--text); border-color: var(--dim); background: rgba(255,255,255,0.08); }
798
- .toolbar-btn.active { color: var(--accent); border-color: rgba(108,138,255,0.4); background: rgba(108,138,255,0.08); }
799
- .toolbar-btn.expo-running { color: var(--green); border-color: rgba(74,222,128,0.4); background: rgba(74,222,128,0.08); }
800
-
801
- /* adjust board to not overlap toolbar */
802
- .board-wrap { padding-bottom: 40px; }
803
-
804
- /* ── LANGUAGE SELECTOR ── */
805
- .lang-selector {
806
- display: flex;
807
- gap: 2px;
808
- background: rgba(255,255,255,0.05);
809
- border: 1px solid var(--border);
810
- border-radius: 6px;
811
- padding: 2px;
812
- }
813
- .lang-btn {
814
- background: none;
815
- border: none;
816
- color: var(--muted);
817
- font-family: var(--mono);
818
- font-size: 10px;
819
- font-weight: 700;
820
- padding: 3px 8px;
821
- border-radius: 4px;
822
- cursor: pointer;
823
- letter-spacing: 0.05em;
824
- transition: all 0.15s;
825
- }
826
- .lang-btn:hover { color: var(--text); }
827
- .lang-btn.active { background: var(--accent); color: #fff; }
828
-
829
- /* ── RETRY BUTTON on failed cards ── */
830
- .card-retry-btn {
831
- margin-top: 10px;
832
- width: 100%;
833
- padding: 6px;
834
- background: rgba(248,113,113,0.1);
835
- border: 1px solid rgba(248,113,113,0.25);
836
- border-radius: 6px;
837
- color: var(--red);
838
- font-size: 11px;
839
- font-weight: 600;
840
- font-family: var(--mono);
841
- cursor: pointer;
842
- transition: all 0.15s;
843
- text-align: center;
844
- }
845
- .card-retry-btn:hover {
846
- background: rgba(248,113,113,0.2);
847
- border-color: rgba(248,113,113,0.5);
848
- }
849
-
850
- /* ── CARD ACTION BAR (edit/delete/retry) ── */
851
- .card-actions {
852
- display: flex;
853
- gap: 4px;
854
- margin-top: 8px;
855
- opacity: 0;
856
- transition: opacity 0.15s;
857
- }
858
- .card:hover .card-actions { opacity: 1; }
859
- .card-action-btn {
860
- flex: 1;
861
- padding: 4px 0;
862
- border-radius: 4px;
863
- border: 1px solid var(--border);
864
- background: rgba(255,255,255,0.03);
865
- color: var(--muted);
866
- font-family: var(--mono);
867
- font-size: 10px;
868
- cursor: pointer;
869
- transition: all 0.15s;
870
- text-align: center;
871
- }
872
- .card-action-btn:hover { background: rgba(255,255,255,0.08); color: var(--text); }
873
- .card-action-btn.retry { color: var(--yellow); border-color: rgba(251,191,36,0.3); }
874
- .card-action-btn.retry:hover { background: rgba(251,191,36,0.1); }
875
- .card-action-btn.del { color: var(--red); border-color: rgba(248,113,113,0.2); }
876
- .card-action-btn.del:hover { background: rgba(248,113,113,0.12); }
877
- .card-action-btn.edit { color: var(--accent); border-color: rgba(108,138,255,0.2); }
878
- .card-action-btn.edit:hover { background: rgba(108,138,255,0.1); }
879
-
880
- /* Empty column state */
881
- .col-empty {
882
- text-align: center;
883
- padding: 24px 12px;
884
- font-family: var(--mono);
885
- font-size: 10px;
886
- color: var(--dim);
887
- line-height: 2;
888
- }
889
-
890
- .fade-in { animation: fadeIn 0.2s ease; }
891
- @keyframes fadeIn { from { opacity:0; transform:translateY(4px); } to { opacity:1; transform:translateY(0); } }
892
- </style>
893
- </head>
894
- <body>
895
-
896
- <!-- HEADER -->
897
- <div class="header">
898
- <div class="logo">
899
- <div class="logo-pulse"></div>
900
- ClaudeBoard
901
- </div>
902
- <div class="header-sep"></div>
903
- <div class="project-badge" id="projectName">—</div>
904
-
905
- <div class="header-stats">
906
- <div class="hstat todo"><div class="hstat-dot"></div><span id="statTodo">0</span> <span data-i18n="todo">todo</span></div>
907
- <div class="hstat prog"><div class="hstat-dot"></div><span id="statProg">0</span> <span data-i18n="running">running</span></div>
908
- <div class="hstat done"><div class="hstat-dot"></div><span id="statDone">0</span> <span data-i18n="done">done</span></div>
909
- <div class="hstat err"><div class="hstat-dot"></div><span id="statErr">0</span> <span data-i18n="failed">failed</span></div>
910
- </div>
911
-
912
- <div class="header-right">
913
- <div class="progress-wrap">
914
- <div class="progress-fill" id="progressFill" style="width:0%"></div>
915
- </div>
916
- <div class="progress-pct" id="progressPct">0%</div>
917
- <div class="ws-badge">
918
- <div class="ws-dot" id="wsDot"></div>
919
- <span id="wsLabel" data-i18n="connecting">connecting</span>
920
- </div>
921
- <!-- Language selector -->
922
- <div class="lang-selector">
923
- <button class="lang-btn active" id="lang-en" onclick="setLang('en')">EN</button>
924
- <button class="lang-btn" id="lang-es" onclick="setLang('es')">ES</button>
925
- </div>
926
- <button class="btn btn-primary" onclick="openModal()" data-i18n="addTask">+ Add Task</button>
927
- </div>
928
- </div>
929
-
930
- <!-- RUNNING BAR -->
931
- <div class="running-bar" id="runningBar">
932
- <div class="running-spinner"></div>
933
- <span class="running-label" data-i18n="agentWorking">Agent working →</span>
934
- <span class="running-title" id="runningTitle">—</span>
935
- </div>
936
-
937
- <!-- MAIN -->
938
- <div class="main">
939
-
940
- <!-- KANBAN BOARD -->
941
- <div class="board" id="board">
942
-
943
- <!-- TODO -->
944
- <div class="column col-todo" id="col-todo">
945
- <div class="column-header">
946
- <div class="column-dot"></div>
947
- <span class="column-title" data-i18n="colTodo">To Do</span>
948
- <span class="column-count" id="cnt-todo">0</span>
949
- </div>
950
- <div class="column-body" id="body-todo" ondragover="onDragOver(event,'todo')" ondrop="onDrop(event,'todo')" ondragleave="onDragLeave(event)"></div>
951
- </div>
952
-
953
- <!-- IN PROGRESS -->
954
- <div class="column col-prog" id="col-prog">
955
- <div class="column-header">
956
- <div class="column-dot"></div>
957
- <span class="column-title" data-i18n="colProg">In Progress</span>
958
- <span class="column-count" id="cnt-prog">0</span>
959
- </div>
960
- <div class="column-body" id="body-prog" ondragover="onDragOver(event,'in_progress')" ondrop="onDrop(event,'in_progress')" ondragleave="onDragLeave(event)"></div>
961
- </div>
962
-
963
- <!-- DONE -->
964
- <div class="column col-done" id="col-done">
965
- <div class="column-header">
966
- <div class="column-dot"></div>
967
- <span class="column-title" data-i18n="colDone">Done</span>
968
- <span class="column-count" id="cnt-done">0</span>
969
- </div>
970
- <div class="column-body" id="body-done" ondragover="onDragOver(event,'done')" ondrop="onDrop(event,'done')" ondragleave="onDragLeave(event)"></div>
971
- </div>
972
-
973
- <!-- ERROR -->
974
- <div class="column col-err" id="col-err">
975
- <div class="column-header">
976
- <div class="column-dot"></div>
977
- <span class="column-title" data-i18n="colFailed">Failed</span>
978
- <span class="column-count" id="cnt-err">0</span>
979
- </div>
980
- <div class="column-body" id="body-err" ondragover="onDragOver(event,'error')" ondrop="onDrop(event,'error')" ondragleave="onDragLeave(event)"></div>
981
- </div>
982
-
983
- </div>
984
-
985
- <!-- SIDEBAR -->
986
- <div class="sidebar">
987
- <div class="sidebar-tabs">
988
- <button class="stab active" id="tab-activity" onclick="switchTab('activity')" data-i18n="tabActivity">Activity</button>
989
- <button class="stab" id="tab-detail" onclick="switchTab('detail')" data-i18n="tabDetail">Detail</button>
990
- </div>
991
- <div class="sidebar-body" id="sidebarBody">
992
- <!-- Activity pane — always in DOM, shown/hidden -->
993
- <div id="activityPane">
994
- <div class="detail-empty" data-i18n="activityEmpty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>
995
- </div>
996
- <!-- Detail pane — always in DOM, shown/hidden -->
997
- <div id="detailPane" style="display:none">
998
- <div class="detail-empty" data-i18n="detailEmpty">Click any task card<br>to see its details.</div>
999
- </div>
1000
- </div>
1001
- </div>
1002
-
1003
- </div>
1004
-
1005
- <!-- ADD TASK MODAL -->
1006
- <div class="overlay" id="modal" onclick="if(event.target===this)closeModal()">
1007
- <div class="modal">
1008
- <div class="modal-title" data-i18n="addTaskTitle">Add Task</div>
1009
- <div class="field">
1010
- <label data-i18n="fieldTitle">Title</label>
1011
- <input type="text" id="f-title" data-i18n-placeholder="placeholderTitle">
1012
- </div>
1013
- <div class="field">
1014
- <label data-i18n="fieldDesc">Description</label>
1015
- <textarea id="f-desc" rows="3" data-i18n-placeholder="placeholderDesc"></textarea>
1016
- </div>
1017
- <div class="modal-grid">
1018
- <div class="field">
1019
- <label data-i18n="fieldPriority">Priority</label>
1020
- <select id="f-priority">
1021
- <option value="high" data-i18n="prioHigh">High</option>
1022
- <option value="medium" selected data-i18n="prioMed">Medium</option>
1023
- <option value="low" data-i18n="prioLow">Low</option>
1024
- </select>
1025
- </div>
1026
- <div class="field">
1027
- <label data-i18n="fieldType">Type</label>
1028
- <select id="f-type">
1029
- <option value="feature" selected data-i18n="typeFeature">Feature</option>
1030
- <option value="bug" data-i18n="typeBug">Bug</option>
1031
- <option value="config" data-i18n="typeConfig">Config</option>
1032
- <option value="refactor" data-i18n="typeRefactor">Refactor</option>
1033
- <option value="test" data-i18n="typeTest">Test</option>
1034
- </select>
1035
- </div>
1036
- </div>
1037
- <div class="modal-actions">
1038
- <button class="btn-cancel" onclick="closeModal()" data-i18n="cancel">Cancel</button>
1039
- <button class="btn-create" onclick="submitTask()" data-i18n="createTask">Create Task</button>
1040
- </div>
1041
- </div>
1042
- </div>
1043
-
1044
- <!-- EDIT TASK MODAL -->
1045
- <div class="overlay" id="editModal" onclick="if(event.target===this)closeEditTask()">
1046
- <div class="modal">
1047
- <div class="modal-title" data-i18n="editTaskTitle">Edit Task</div>
1048
- <input type="hidden" id="e-id">
1049
- <div class="field">
1050
- <label data-i18n="fieldTitle">Title</label>
1051
- <input type="text" id="e-title">
1052
- </div>
1053
- <div class="field">
1054
- <label data-i18n="fieldDesc">Description</label>
1055
- <textarea id="e-desc" rows="4"></textarea>
1056
- </div>
1057
- <div class="modal-grid">
1058
- <div class="field">
1059
- <label data-i18n="fieldPriority">Priority</label>
1060
- <select id="e-priority">
1061
- <option value="high" data-i18n="prioHigh">High</option>
1062
- <option value="medium" data-i18n="prioMed">Medium</option>
1063
- <option value="low" data-i18n="prioLow">Low</option>
1064
- </select>
1065
- </div>
1066
- <div class="field">
1067
- <label data-i18n="fieldType">Type</label>
1068
- <select id="e-type">
1069
- <option value="feature" data-i18n="typeFeature">Feature</option>
1070
- <option value="bug" data-i18n="typeBug">Bug</option>
1071
- <option value="config" data-i18n="typeConfig">Config</option>
1072
- <option value="refactor" data-i18n="typeRefactor">Refactor</option>
1073
- <option value="test" data-i18n="typeTest">Test</option>
1074
- </select>
1075
- </div>
1076
- </div>
1077
- <div class="modal-actions">
1078
- <button class="btn-cancel" onclick="closeEditTask()" data-i18n="cancel">Cancel</button>
1079
- <button class="btn-create" onclick="submitEditTask()" data-i18n="saveTask">Save Changes</button>
1080
- </div>
1081
- </div>
1082
- </div>
1083
-
1084
- <!-- RETRY / EDIT MODAL -->
1085
- <div class="overlay" id="retryModal" onclick="if(event.target===this)closeRetry()">
1086
- <div class="modal">
1087
- <div class="modal-title" style="display:flex;align-items:center;gap:10px">
1088
- <span style="color:var(--red)">✕</span>
1089
- <span data-i18n="retryTitle">Edit & Retry Failed Task</span>
1090
- </div>
1091
- <div style="background:rgba(248,113,113,0.06);border:1px solid rgba(248,113,113,0.2);border-radius:8px;padding:10px 12px;margin-bottom:16px;font-family:var(--mono);font-size:11px;color:var(--red)" id="retryErrorLog" data-i18n="noErrorLog">
1092
- No error log found.
1093
- </div>
1094
- <div class="field">
1095
- <label data-i18n="fieldTitle">Title</label>
1096
- <input type="text" id="r-title">
1097
- </div>
1098
- <div class="field">
1099
- <label data-i18n="fieldDesc">Description</label>
1100
- <textarea id="r-desc" rows="4"></textarea>
1101
- </div>
1102
- <div class="field">
1103
- <label style="color:var(--accent)" data-i18n="agentNoteLabel">💬 Note for the agent (hint to fix the issue)</label>
1104
- <textarea id="r-note" rows="3" data-i18n-placeholder="agentNotePlaceholder"></textarea>
1105
- </div>
1106
- <div class="modal-grid">
1107
- <div class="field">
1108
- <label data-i18n="fieldPriority">Priority</label>
1109
- <select id="r-priority">
1110
- <option value="high" data-i18n="prioHigh">High</option>
1111
- <option value="medium" data-i18n="prioMed">Medium</option>
1112
- <option value="low" data-i18n="prioLow">Low</option>
1113
- </select>
1114
- </div>
1115
- <div class="field">
1116
- <label data-i18n="fieldType">Type</label>
1117
- <select id="r-type">
1118
- <option value="feature" data-i18n="typeFeature">Feature</option>
1119
- <option value="bug" data-i18n="typeBug">Bug</option>
1120
- <option value="config" data-i18n="typeConfig">Config</option>
1121
- <option value="refactor" data-i18n="typeRefactor">Refactor</option>
1122
- <option value="test" data-i18n="typeTest">Test</option>
1123
- </select>
1124
- </div>
1125
- </div>
1126
- <div class="modal-actions">
1127
- <button class="btn-cancel" onclick="closeRetry()" data-i18n="cancel">Cancel</button>
1128
- <button class="btn-create" style="background:var(--red)" onclick="submitRetry()" data-i18n="retryBtn">↩ Retry Task</button>
1129
- </div>
1130
- </div>
1131
- </div>
1132
-
1133
- <!-- BOTTOM TOOLBAR -->
1134
- <div class="bottom-toolbar">
1135
- <button class="toolbar-btn" id="expoBtn" onclick="toggleExpoPanel()">
1136
- 📱 Expo
1137
- <span class="expo-status-badge stopped" id="expoBadge" data-i18n="statusStopped">stopped</span>
1138
- </button>
1139
- <button class="toolbar-btn" id="termBtn" onclick="toggleTerminal()" data-i18n="terminal">
1140
- ⌨️ Terminal
1141
- </button>
1142
- </div>
1143
-
1144
- <!-- EXPO PANEL -->
1145
- <div class="expo-panel" id="expoPanel">
1146
- <div class="expo-panel-header" onclick="toggleExpoPanel()">
1147
- <span class="expo-panel-title">📱 Expo Go</span>
1148
- <span class="expo-status-badge stopped" id="expoPanelBadge" data-i18n="statusStopped">stopped</span>
1149
- <div style="margin-left:auto;display:flex;gap:8px">
1150
- <button class="btn btn-primary" id="expoStartBtn" onclick="event.stopPropagation();startExpo()" style="font-size:11px;padding:4px 12px" data-i18n="startExpo">Start Expo</button>
1151
- <button class="btn btn-ghost" id="expoStopBtn" onclick="event.stopPropagation();stopExpo()" style="font-size:11px;padding:4px 12px;display:none" data-i18n="stopExpo">Stop</button>
1152
- </div>
1153
- </div>
1154
- <div style="display:flex;gap:0">
1155
- <div style="flex:1">
1156
- <div class="expo-logs" id="expoLogs" data-i18n="expoIdle">Expo not started. Click "Start Expo" to install dependencies and launch with tunnel.</div>
1157
- <div class="expo-qr-wrap" id="expoUrlWrap" style="display:none">
1158
- <div>
1159
- <div style="font-family:var(--mono);font-size:10px;color:var(--muted);margin-bottom:4px" data-i18n="scanWith">SCAN WITH EXPO GO</div>
1160
- <div class="expo-url" id="expoUrl">—</div>
1161
- </div>
1162
- </div>
1163
- </div>
1164
- <div id="qrWrap" style="padding:12px;display:none">
1165
- <canvas id="qrCanvas" width="120" height="120"></canvas>
1166
- </div>
1167
- </div>
1168
- </div>
1169
-
1170
- <!-- TERMINAL PANEL -->
1171
- <div class="term-panel" id="termPanel">
1172
- <div class="term-header">
1173
- <span style="color:var(--green);font-size:14px">⬤</span>
1174
- <span class="term-title">Terminal — <span style="color:var(--accent)" id="termDir">project</span></span>
1175
- <button class="btn-term-close" onclick="toggleTerminal()">✕</button>
1176
- </div>
1177
- <div id="terminal"></div>
1178
- </div>
1179
-
1180
- <!-- xterm.js -->
1181
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.3.0/xterm.min.css">
1182
- <script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.3.0/xterm.min.js"></script>
1183
- <script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.3.0/addon-fit.min.js"></script>
1184
-
1185
- <!-- QR code via qrcodejs -->
1186
- <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
1187
-
1188
- <script>
1189
- // ── STATE ────────────────────────────────────────────────────────────────────
1190
- let board = { epics: [], logs: [] };
1191
- let activeTab = 'activity';
1192
- let selectedTask = null;
1193
- let draggedId = null;
1194
- let ws;
1195
-
1196
- // ── WEBSOCKET ────────────────────────────────────────────────────────────────
1197
- function connectWS() {
1198
- const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1199
- ws = new WebSocket(`${proto}//${location.host}`);
1200
- ws.onopen = () => setWS(true);
1201
- ws.onclose = () => { setWS(false); setTimeout(connectWS, 2000); };
1202
- ws.onmessage = (e) => {
1203
- const { event, data } = JSON.parse(e.data);
1204
-
1205
- // Task state changes that require a board reload (status flipped, new task, deleted)
1206
- if (['task_added','task_started','task_complete','task_failed','task_deleted'].includes(event)) {
1207
- loadBoard();
1208
- }
1209
- // Reorder: just re-render from current board data — no network call needed
1210
- if (event === 'task_reordered') {
1211
- renderKanban();
1212
- updateStats();
1213
- }
1214
- // Single task field update: patch in-memory and re-render (no full reload)
1215
- if (event === 'task_update') {
1216
- patchTaskInMemory(data);
1217
- }
1218
- if (event === 'log') {
1219
- board.logs.unshift(data);
1220
- if (activeTab === 'activity') renderLogs();
1221
- }
1222
- if (event === 'expo_status') {
1223
- setExpoStatus(data.status, data.url);
1224
- }
1225
- if (event === 'expo_log') {
1226
- appendExpoLog(data.message);
1227
- }
1228
- };
1229
- }
1230
-
1231
- function setWS(on) {
1232
- document.getElementById('wsDot').className = 'ws-dot' + (on ? ' on' : '');
1233
- document.getElementById('wsLabel').textContent = on ? t('live') : t('reconnecting');
1234
- // Poll only when disconnected
1235
- if (on) {
1236
- if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
1237
- } else {
1238
- if (!pollInterval) pollInterval = setInterval(loadBoard, 10000);
1239
- }
1240
- }
1241
-
1242
- // ── DATA ─────────────────────────────────────────────────────────────────────
1243
- async function loadBoard() {
1244
- const res = await fetch('/api/board');
1245
- const data = await res.json();
1246
- board = data;
1247
- document.getElementById('projectName').textContent = data.project || '—';
1248
- renderKanban();
1249
- updateStats();
1250
- if (activeTab === 'activity') renderLogs();
1251
- }
1252
-
1253
- function allTasks() {
1254
- return board.epics?.flatMap(e => (e.cb_tasks || []).map(t => ({ ...t, epicName: e.name }))) || [];
1255
- }
1256
-
1257
- function updateStats() {
1258
- const tasks = allTasks();
1259
- const todo = tasks.filter(t => t.status === 'todo').length;
1260
- const prog = tasks.filter(t => t.status === 'in_progress').length;
1261
- const done = tasks.filter(t => t.status === 'done').length;
1262
- const err = tasks.filter(t => t.status === 'error').length;
1263
- const total = tasks.length;
1264
-
1265
- document.getElementById('statTodo').textContent = todo;
1266
- document.getElementById('statProg').textContent = prog;
1267
- document.getElementById('statDone').textContent = done;
1268
- document.getElementById('statErr').textContent = err;
1269
-
1270
- const pct = total > 0 ? Math.round((done / total) * 100) : 0;
1271
- document.getElementById('progressFill').style.width = pct + '%';
1272
- document.getElementById('progressPct').textContent = pct + '%';
1273
-
1274
- // Running bar
1275
- const running = tasks.find(t => t.status === 'in_progress');
1276
- const bar = document.getElementById('runningBar');
1277
- if (running) {
1278
- bar.className = 'running-bar visible';
1279
- document.getElementById('runningTitle').textContent = running.title;
1280
- } else {
1281
- bar.className = 'running-bar';
1282
- }
1283
- }
1284
-
1285
- // ── KANBAN RENDER ─────────────────────────────────────────────────────────────
1286
- function renderKanban() {
1287
- const tasks = allTasks();
1288
- const groups = {
1289
- todo: tasks.filter(t => t.status === 'todo'),
1290
- in_progress: tasks.filter(t => t.status === 'in_progress'),
1291
- done: tasks.filter(t => t.status === 'done'),
1292
- error: tasks.filter(t => t.status === 'error'),
1293
- };
1294
-
1295
- const map = { todo: 'todo', in_progress: 'prog', done: 'done', error: 'err' };
1296
-
1297
- for (const [status, colKey] of Object.entries(map)) {
1298
- const body = document.getElementById('body-' + colKey);
1299
- const cnt = document.getElementById('cnt-' + colKey);
1300
- const list = groups[status] || [];
1301
- cnt.textContent = list.length;
1302
-
1303
- if (list.length === 0) {
1304
- body.innerHTML = `<div class="col-empty">No tasks here</div>`;
1305
- continue;
1306
- }
1307
-
1308
- // Sort by priority_order before rendering
1309
- const sorted = [...list].sort((a, b) => (a.priority_order ?? 99) - (b.priority_order ?? 99));
1310
- body.innerHTML = sorted.map((t, i) => cardHTML(t, status === 'todo' ? i + 1 : null)).join('');
1311
-
1312
- // Attach drag events
1313
- body.querySelectorAll('.card').forEach(card => {
1314
- card.addEventListener('dragstart', e => onDragStart(e, card.dataset.id));
1315
- card.addEventListener('dragend', () => onDragEnd(card));
1316
- card.addEventListener('click', () => selectCard(card.dataset.id));
1317
- });
1318
- }
1319
- }
1320
-
1321
- function cardHTML(task, position = null) {
1322
- const icons = { todo: '', in_progress: '', done: '✓', error: '✕', blocked: '—' };
1323
- const shortEpic = (task.epicName || '').split(' ').slice(0,2).join(' ');
1324
- const isError = task.status === 'error';
1325
- const posNum = position ? `<span style="font-family:var(--mono);font-size:9px;color:var(--dim);background:rgba(255,255,255,0.05);border-radius:3px;padding:1px 5px;flex-shrink:0">#${position}</span>` : '';
1326
- return `
1327
- <div class="card fade-in" draggable="true" data-id="${task.id}" data-status="${task.status}">
1328
- <div class="card-top">
1329
- <div class="card-status ${task.status}">${icons[task.status] || ''}</div>
1330
- <div class="card-title">${esc(task.title)}</div>
1331
- ${posNum}
1332
- </div>
1333
- ${task.description ? `<div class="card-desc">${esc(task.description.split('\n')[0])}</div>` : ''}
1334
- <div class="card-footer">
1335
- <span class="tag ${task.priority}">${task.priority}</span>
1336
- <span class="tag ${task.type}">${task.type}</span>
1337
- ${shortEpic ? `<span class="card-epic">${esc(shortEpic)}</span>` : ''}
1338
- </div>
1339
- <div class="card-actions" onclick="event.stopPropagation()">
1340
- ${isError ? `<button class="card-action-btn retry" onclick="openRetry('${task.id}')">${t('retryCard')}</button>` : ''}
1341
- <button class="card-action-btn edit" onclick="openEditTask('${task.id}')" title="Edit">✎</button>
1342
- <button class="card-action-btn del" onclick="deleteTask('${task.id}')" title="Delete">✕</button>
1343
- </div>
1344
- </div>`;
1345
- }
1346
-
1347
- // ── IN-MEMORY PATCH (avoid full reload for minor updates) ─────────────────────
1348
- function patchTaskInMemory(patch) {
1349
- if (!board.epics) return;
1350
- for (const epic of board.epics) {
1351
- const idx = (epic.cb_tasks || []).findIndex(t => t.id === patch.id);
1352
- if (idx !== -1) {
1353
- epic.cb_tasks[idx] = { ...epic.cb_tasks[idx], ...patch };
1354
- renderKanban();
1355
- updateStats();
1356
- return;
1357
- }
1358
- }
1359
- // Task not found in memory yet (new orphan etc), do full reload
1360
- loadBoard();
1361
- }
1362
-
1363
- // ── DRAG STATE ────────────────────────────────────────────────────────────────
1364
- // We track order in JS so we don't depend on DOM state during drag
1365
- let dragColumnOrder = {}; // { status: [id, id, ...] } — snapshot at dragstart
1366
- function onDragStart(e, id) {
1367
- // Normalize to string so comparisons with dataset values are consistent
1368
- draggedId = String(id);
1369
-
1370
- // Snapshot current order of all columns from memory (reliable — not DOM-dependent)
1371
- // IDs are stored as strings to match dataset.id comparisons
1372
- dragColumnOrder = {};
1373
- for (const status of ['todo', 'in_progress', 'done', 'error']) {
1374
- dragColumnOrder[status] = allTasks()
1375
- .filter(t => t.status === status)
1376
- .sort((a, b) => (a.priority_order ?? 99) - (b.priority_order ?? 99))
1377
- .map(t => String(t.id));
1378
- }
1379
-
1380
- setTimeout(() => {
1381
- const el = document.querySelector(`.card[data-id="${id}"]`);
1382
- if (el) el.classList.add('dragging');
1383
- }, 0);
1384
- e.dataTransfer.effectAllowed = 'move';
1385
- }
1386
-
1387
- function onDragEnd(card) {
1388
- card.classList.remove('dragging');
1389
- document.querySelectorAll('.drop-placeholder').forEach(p => p.remove());
1390
- }
1391
-
1392
- function onDragOver(e, status) {
1393
- e.preventDefault();
1394
- e.dataTransfer.dropEffect = 'move';
1395
- const col = e.currentTarget;
1396
-
1397
- // Find the card being hovered over to insert placeholder before it
1398
- const draggingCard = document.querySelector('.card.dragging');
1399
- const cards = [...col.querySelectorAll('.card:not(.dragging)')];
1400
-
1401
- // Remove existing placeholder
1402
- col.querySelectorAll('.drop-placeholder').forEach(p => p.remove());
1403
-
1404
- // Find insertion point based on mouse Y position
1405
- let insertBefore = null;
1406
- for (const card of cards) {
1407
- const rect = card.getBoundingClientRect();
1408
- if (e.clientY < rect.top + rect.height / 2) {
1409
- insertBefore = card;
1410
- break;
1411
- }
1412
- }
1413
-
1414
- const ph = document.createElement('div');
1415
- ph.className = 'drop-placeholder';
1416
- if (insertBefore) col.insertBefore(ph, insertBefore);
1417
- else col.appendChild(ph);
1418
-
1419
- // Store insertion target for onDrop
1420
- col.dataset.insertBefore = insertBefore?.dataset?.id || '';
1421
- }
1422
-
1423
- function onDragLeave(e) {
1424
- const col = e.currentTarget;
1425
- const related = e.relatedTarget;
1426
- if (!col.contains(related)) {
1427
- col.querySelectorAll('.drop-placeholder').forEach(p => p.remove());
1428
- }
1429
- }
1430
-
1431
- async function onDrop(e, status) {
1432
- e.preventDefault();
1433
- document.querySelectorAll('.drop-placeholder').forEach(p => p.remove());
1434
- if (!draggedId) return;
1435
-
1436
- const col = e.currentTarget;
1437
- // Read insertBefore from the column body's data attribute (set by onDragOver)
1438
- const insertBeforeId = col.dataset.insertBefore || '';
1439
- delete col.dataset.insertBefore;
1440
-
1441
- // Normalize IDs to strings for consistent comparison (dataset values are always strings)
1442
- const draggedIdStr = String(draggedId);
1443
- const insertBeforeIdStr = insertBeforeId ? String(insertBeforeId) : '';
1444
-
1445
- // Use the JS snapshot (taken at dragstart) — not DOM which has .dragging gaps
1446
- const sourceStatus = allTasks().find(t => String(t.id) === draggedIdStr)?.status || status;
1447
-
1448
- // columnIds is already an array of strings (set in onDragStart)
1449
- const columnIds = (dragColumnOrder[status] || []).filter(id => id !== draggedIdStr);
1450
-
1451
- let newOrderedIds;
1452
- if (!insertBeforeIdStr) {
1453
- // Dropped at the end of the column
1454
- newOrderedIds = [...columnIds, draggedIdStr];
1455
- } else {
1456
- const insertIdx = columnIds.indexOf(insertBeforeIdStr);
1457
- if (insertIdx === -1) {
1458
- // insertBeforeId not found in snapshot — fall back to appending at end
1459
- newOrderedIds = [...columnIds, draggedIdStr];
1460
- } else {
1461
- newOrderedIds = [...columnIds.slice(0, insertIdx), draggedIdStr, ...columnIds.slice(insertIdx)];
1462
- }
1463
- }
1464
-
1465
- // Optimistic update in memory so re-render is instant
1466
- const task = allTasks().find(t => String(t.id) === draggedIdStr);
1467
- if (task) task.status = status;
1468
- newOrderedIds.forEach((idStr, i) => {
1469
- const t = allTasks().find(t => String(t.id) === idStr);
1470
- if (t) t.priority_order = i + 1;
1471
- });
1472
- renderKanban();
1473
- updateStats();
1474
-
1475
- // Update status if column changed
1476
- if (sourceStatus !== status) {
1477
- await fetch(`/api/tasks/${draggedIdStr}`, {
1478
- method: 'PATCH',
1479
- headers: { 'Content-Type': 'application/json' },
1480
- body: JSON.stringify({ status }),
1481
- });
1482
- }
1483
-
1484
- // Persist new order (send as-is — server handles string or numeric IDs)
1485
- await fetch('/api/tasks/reorder', {
1486
- method: 'POST',
1487
- headers: { 'Content-Type': 'application/json' },
1488
- body: JSON.stringify({ taskIds: newOrderedIds }),
1489
- });
1490
-
1491
- draggedId = null;
1492
- }
1493
-
1494
- // ── CARD SELECT ───────────────────────────────────────────────────────────────
1495
- async function selectCard(id) {
1496
- const task = allTasks().find(t => t.id === id);
1497
- if (!task) return;
1498
- selectedTask = task;
1499
-
1500
- const res = await fetch(`/api/tasks/${id}/logs`);
1501
- const { logs } = await res.json();
1502
-
1503
- // Write into the dedicated detail pane (never touches activityPane)
1504
- document.getElementById('detailPane').innerHTML = `
1505
- <div>
1506
- <div class="detail-title">${esc(task.title)}</div>
1507
- <div class="detail-tags">
1508
- <span class="tag ${task.status}">${task.status.replace('_',' ')}</span>
1509
- <span class="tag ${task.priority}">${task.priority}</span>
1510
- <span class="tag ${task.type}">${task.type}</span>
1511
- </div>
1512
- ${task.description ? `<div class="detail-desc">${esc(task.description)}</div>` : ''}
1513
- <div class="detail-logs-title">// agent logs</div>
1514
- ${logs.length === 0
1515
- ? '<div style="font-family:var(--mono);font-size:10px;color:var(--dim);padding:8px 0">No logs yet.</div>'
1516
- : logs.map(l => {
1517
- const t = new Date(l.created_at).toLocaleTimeString('en',{hour12:false});
1518
- return `<div class="log-entry"><span class="log-time">${t}</span><span class="log-msg ${l.type}">${esc(l.message)}</span></div>`;
1519
- }).join('')
1520
- }
1521
- </div>`;
1522
-
1523
- // Switch to detail tab
1524
- activeTab = 'detail';
1525
- document.getElementById('tab-activity').className = 'stab';
1526
- document.getElementById('tab-detail').className = 'stab active';
1527
- document.getElementById('activityPane').style.display = 'none';
1528
- document.getElementById('detailPane').style.display = 'block';
1529
- }
1530
-
1531
- // ── LOGS ──────────────────────────────────────────────────────────────────────
1532
- function renderLogs() {
1533
- // Only update the activity pane — never overwrites detail pane
1534
- const el = document.getElementById('activityPane');
1535
- if (!board.logs?.length) {
1536
- el.innerHTML = `<div class="detail-empty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>`;
1537
- return;
1538
- }
1539
- const icons = { start: '▶', complete: '✓', error: '✕', progress: '·', info: '·' };
1540
- el.innerHTML = board.logs.map(l => {
1541
- const t = new Date(l.created_at).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
1542
- return `<div class="log-entry">
1543
- <span class="log-time">${t}</span>
1544
- <span class="log-icon" style="color:${logColor(l.type)}">${icons[l.type]||'·'}</span>
1545
- <span class="log-msg ${l.type}">${esc(l.message)}</span>
1546
- </div>`;
1547
- }).join('');
1548
- }
1549
-
1550
- function logColor(type) {
1551
- return { start:'#6c8aff', complete:'#4ade80', error:'#f87171', progress:'#6b7094', info:'#6b7094' }[type] || '#6b7094';
1552
- }
1553
-
1554
- // ── TABS ──────────────────────────────────────────────────────────────────────
1555
- function switchTab(tab) {
1556
- activeTab = tab;
1557
- document.getElementById('tab-activity').className = 'stab' + (tab === 'activity' ? ' active' : '');
1558
- document.getElementById('tab-detail').className = 'stab' + (tab === 'detail' ? ' active' : '');
1559
- // Show/hide panes — never overwrite the other pane's content
1560
- document.getElementById('activityPane').style.display = tab === 'activity' ? 'block' : 'none';
1561
- document.getElementById('detailPane').style.display = tab === 'detail' ? 'block' : 'none';
1562
- if (tab === 'activity') renderLogs();
1563
- }
1564
-
1565
- // ── MODAL ─────────────────────────────────────────────────────────────────────
1566
- function openModal() { document.getElementById('modal').className = 'overlay open'; }
1567
- function closeModal() { document.getElementById('modal').className = 'overlay'; }
1568
-
1569
- async function submitTask() {
1570
- const title = document.getElementById('f-title').value.trim();
1571
- if (!title) return;
1572
-
1573
- await fetch('/api/tasks', {
1574
- method: 'POST',
1575
- headers: { 'Content-Type': 'application/json' },
1576
- body: JSON.stringify({
1577
- title,
1578
- description: document.getElementById('f-desc').value.trim(),
1579
- priority: document.getElementById('f-priority').value,
1580
- type: document.getElementById('f-type').value,
1581
- }),
1582
- });
1583
-
1584
- document.getElementById('f-title').value = '';
1585
- document.getElementById('f-desc').value = '';
1586
- closeModal();
1587
- loadBoard();
1588
- }
1589
-
1590
- // ── RETRY MODAL ───────────────────────────────────────────────────────────────
1591
- let retryTaskId = null;
1592
-
1593
- async function openRetry(id) {
1594
- retryTaskId = id;
1595
- const task = allTasks().find(t => t.id === id);
1596
- if (!task) return;
1597
-
1598
- // Fill form with current task data
1599
- document.getElementById('r-title').value = task.title;
1600
- document.getElementById('r-desc').value = task.description || '';
1601
- document.getElementById('r-note').value = '';
1602
- document.getElementById('r-priority').value = task.priority || 'medium';
1603
- document.getElementById('r-type').value = task.type || 'feature';
1604
-
1605
- // Load last error log
1606
- const res = await fetch(`/api/tasks/${id}/logs`);
1607
- const { logs } = await res.json();
1608
- const errorLogs = logs.filter(l => l.type === 'error');
1609
- const lastError = errorLogs[errorLogs.length - 1];
1610
- document.getElementById('retryErrorLog').textContent =
1611
- lastError ? lastError.message : 'No error log found.';
1612
-
1613
- document.getElementById('retryModal').className = 'overlay open';
1614
- }
1615
-
1616
- function closeRetry() {
1617
- document.getElementById('retryModal').className = 'overlay';
1618
- retryTaskId = null;
1619
- }
1620
-
1621
- async function submitRetry() {
1622
- if (!retryTaskId) return;
1623
-
1624
- const title = document.getElementById('r-title').value.trim();
1625
- const desc = document.getElementById('r-desc').value.trim();
1626
- const note = document.getElementById('r-note').value.trim();
1627
- const priority = document.getElementById('r-priority').value;
1628
- const type = document.getElementById('r-type').value;
1629
-
1630
- if (!title) return;
1631
-
1632
- // Build updated description — append agent note if provided
1633
- const updatedDesc = note
1634
- ? `${desc}\n\n⚠️ AGENT NOTE (from human review): ${note}`
1635
- : desc;
1636
-
1637
- // Update task fields + reset status to todo
1638
- await fetch(`/api/tasks/${retryTaskId}`, {
1639
- method: 'PATCH',
1640
- headers: { 'Content-Type': 'application/json' },
1641
- body: JSON.stringify({
1642
- title,
1643
- description: updatedDesc,
1644
- priority,
1645
- type,
1646
- status: 'todo',
1647
- started_at: null,
1648
- }),
1649
- });
1650
-
1651
- // Log the retry
1652
- await fetch(`/api/tasks/${retryTaskId}/log`, {
1653
- method: 'POST',
1654
- headers: { 'Content-Type': 'application/json' },
1655
- body: JSON.stringify({
1656
- message: note
1657
- ? `↩ Retried by human with note: "${note}"`
1658
- : '↩ Retried by human — reset to todo',
1659
- }),
1660
- });
1661
-
1662
- closeRetry();
1663
- loadBoard();
1664
- }
1665
-
1666
- // ── EXPO ─────────────────────────────────────────────────────────────────────
1667
- let expoOpen = false;
1668
- let termOpen = false;
1669
- let term = null;
1670
- let termSocket = null;
1671
- let termFit = null;
1672
- let qrInstance = null;
1673
-
1674
- function toggleExpoPanel() {
1675
- expoOpen = !expoOpen;
1676
- document.getElementById('expoPanel').className = 'expo-panel' + (expoOpen ? ' open' : '');
1677
- document.getElementById('expoBtn').className = 'toolbar-btn' + (expoOpen ? ' active' : '');
1678
- if (termOpen) { termOpen = false; document.getElementById('termPanel').className = 'term-panel'; }
1679
- }
1680
-
1681
- async function startExpo() {
1682
- document.getElementById('expoStartBtn').style.display = 'none';
1683
- document.getElementById('expoStopBtn').style.display = 'inline-flex';
1684
- appendExpoLog('▶ Starting Expo...');
1685
- await fetch('/api/expo/start', { method: 'POST' });
1686
- }
1687
-
1688
- async function stopExpo() {
1689
- await fetch('/api/expo/stop', { method: 'POST' });
1690
- document.getElementById('expoStartBtn').style.display = 'inline-flex';
1691
- document.getElementById('expoStopBtn').style.display = 'none';
1692
- document.getElementById('expoUrlWrap').style.display = 'none';
1693
- document.getElementById('qrWrap').style.display = 'none';
1694
- appendExpoLog('■ Expo stopped.');
1695
- }
1696
-
1697
- function appendExpoLog(msg) {
1698
- const el = document.getElementById('expoLogs');
1699
- const line = document.createElement('div');
1700
- line.textContent = msg;
1701
- el.appendChild(line);
1702
- el.scrollTop = el.scrollHeight;
1703
- }
1704
-
1705
- function setExpoStatus(status, url) {
1706
- const statusKey = 'status' + (status || 'stopped').charAt(0).toUpperCase() + (status || 'stopped').slice(1);
1707
- const label = t(statusKey);
1708
-
1709
- ['expoBadge','expoPanelBadge'].forEach(id => {
1710
- const el = document.getElementById(id);
1711
- el.className = `expo-status-badge ${status}`;
1712
- el.textContent = label;
1713
- });
1714
-
1715
- document.getElementById('expoBtn').className = 'toolbar-btn' + (status === 'running' ? ' expo-running' : (expoOpen ? ' active' : ''));
1716
-
1717
- if (status === 'running' && url) {
1718
- document.getElementById('expoUrl').textContent = url;
1719
- document.getElementById('expoUrlWrap').style.display = 'flex';
1720
-
1721
- // Generate QR code
1722
- document.getElementById('qrWrap').style.display = 'block';
1723
- const canvas = document.getElementById('qrCanvas');
1724
- const ctx = canvas.getContext('2d');
1725
- ctx.clearRect(0, 0, 120, 120);
1726
-
1727
- if (window.QRCode) {
1728
- document.getElementById('qrCanvas').innerHTML = '';
1729
- try {
1730
- new QRCode(document.getElementById('qrCanvas'), {
1731
- text: url,
1732
- width: 120, height: 120,
1733
- colorDark: '#e2e4f0',
1734
- colorLight: '#0d0f1a',
1735
- });
1736
- } catch {}
1737
- }
1738
- }
1739
-
1740
- if (status === 'stopped' || status === 'error') {
1741
- document.getElementById('expoStartBtn').style.display = 'inline-flex';
1742
- document.getElementById('expoStopBtn').style.display = 'none';
1743
- }
1744
- }
1745
-
1746
- // ── TERMINAL ─────────────────────────────────────────────────────────────────
1747
- function toggleTerminal() {
1748
- termOpen = !termOpen;
1749
- document.getElementById('termPanel').className = 'term-panel' + (termOpen ? ' open' : '');
1750
- document.getElementById('termBtn').className = 'toolbar-btn' + (termOpen ? ' active' : '');
1751
-
1752
- if (expoOpen) { expoOpen = false; document.getElementById('expoPanel').className = 'expo-panel'; }
1753
-
1754
- if (termOpen && !term) initTerminal();
1755
- if (termOpen && termFit) setTimeout(() => termFit.fit(), 100);
1756
- }
1757
-
1758
- function initTerminal() {
1759
- term = new Terminal({
1760
- theme: {
1761
- background: '#0d0f1a',
1762
- foreground: '#e2e4f0',
1763
- cursor: '#6c8aff',
1764
- cursorAccent: '#0d0f1a',
1765
- selection: 'rgba(108,138,255,0.3)',
1766
- black: '#1e2130', red: '#f87171', green: '#4ade80', yellow: '#fbbf24',
1767
- blue: '#6c8aff', magenta: '#c084fc', cyan: '#22d3ee', white: '#e2e4f0',
1768
- brightBlack: '#454868', brightRed: '#fca5a5', brightGreen: '#86efac',
1769
- brightYellow: '#fde68a', brightBlue: '#93c5fd', brightMagenta: '#d8b4fe',
1770
- brightCyan: '#67e8f9', brightWhite: '#f1f5f9',
1771
- },
1772
- fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
1773
- fontSize: 13,
1774
- lineHeight: 1.4,
1775
- cursorBlink: true,
1776
- scrollback: 2000,
1777
- });
1778
-
1779
- const fitAddon = new FitAddon.FitAddon();
1780
- termFit = fitAddon;
1781
- term.loadAddon(fitAddon);
1782
- term.open(document.getElementById('terminal'));
1783
- fitAddon.fit();
1784
-
1785
- // Connect WebSocket
1786
- const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1787
- termSocket = new WebSocket(`${proto}//${location.host}/terminal`);
1788
-
1789
- termSocket.onopen = () => {
1790
- term.write('\x1b[32m[ClaudeBoard Terminal]\x1b[0m Connected\r\n\r\n');
1791
- };
1792
-
1793
- termSocket.onmessage = (e) => {
1794
- const msg = JSON.parse(e.data);
1795
- if (msg.type === 'output') term.write(msg.data);
1796
- if (msg.type === 'exit') term.write('\r\n\x1b[31m[process exited]\x1b[0m\r\n');
1797
- };
1798
-
1799
- termSocket.onclose = () => term.write('\r\n\x1b[33m[disconnected]\x1b[0m\r\n');
1800
-
1801
- term.onData((data) => {
1802
- if (termSocket?.readyState === 1) {
1803
- termSocket.send(JSON.stringify({ type: 'input', data }));
1804
- }
1805
- });
1806
-
1807
- // Resize on window resize
1808
- window.addEventListener('resize', () => { if (termFit) termFit.fit(); });
1809
- term.onResize(({ cols, rows }) => {
1810
- if (termSocket?.readyState === 1) termSocket.send(JSON.stringify({ type: 'resize', cols, rows }));
1811
- });
1812
- }
1813
-
1814
- // ── EDIT TASK ─────────────────────────────────────────────────────────────────
1815
- function openEditTask(id) {
1816
- const task = allTasks().find(t => t.id === id);
1817
- if (!task) return;
1818
- document.getElementById('e-id').value = id;
1819
- document.getElementById('e-title').value = task.title || '';
1820
- document.getElementById('e-desc').value = task.description || '';
1821
- document.getElementById('e-priority').value = task.priority || 'medium';
1822
- document.getElementById('e-type').value = task.type || 'feature';
1823
- document.getElementById('editModal').style.display = 'flex';
1824
- applyTranslations();
1825
- }
1826
-
1827
- function closeEditTask() {
1828
- document.getElementById('editModal').style.display = 'none';
1829
- }
1830
-
1831
- async function submitEditTask() {
1832
- const id = document.getElementById('e-id').value;
1833
- const title = document.getElementById('e-title').value.trim();
1834
- const description = document.getElementById('e-desc').value.trim();
1835
- const priority = document.getElementById('e-priority').value;
1836
- const type = document.getElementById('e-type').value;
1837
- if (!title) return;
1838
-
1839
- const priorityOrder = { high: 1, medium: 2, low: 3 };
1840
- await fetch(`/api/tasks/${id}`, {
1841
- method: 'PATCH',
1842
- headers: { 'Content-Type': 'application/json' },
1843
- body: JSON.stringify({ title, description, priority, type, priority_order: priorityOrder[priority] }),
1844
- });
1845
- closeEditTask();
1846
- loadBoard();
1847
- }
1848
-
1849
- // ── DELETE TASK ───────────────────────────────────────────────────────────────
1850
- async function deleteTask(id) {
1851
- if (!confirm(currentLang === 'es' ? '¿Eliminar esta tarea?' : 'Delete this task?')) return;
1852
- await fetch(`/api/tasks/${id}`, { method: 'DELETE' });
1853
- loadBoard();
1854
- }
1855
-
1856
- // ── UTILS ─────────────────────────────────────────────────────────────────────
1857
- function esc(s) {
1858
- if (!s) return '';
1859
- return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1860
- }
1861
-
1862
- document.addEventListener('keydown', e => {
1863
- if (e.key === 'Escape') { closeModal(); closeRetry(); closeEditTask(); }
1864
- if ((e.metaKey || e.ctrlKey) && e.key === 'n') { e.preventDefault(); openModal(); }
1865
- });
1866
-
1867
- // ── I18N ──────────────────────────────────────────────────────────────────────
1868
- const STRINGS = {
1869
- en: {
1870
- todo:'todo', running:'running', done:'done', failed:'failed',
1871
- connecting:'connecting', live:'live', reconnecting:'reconnecting',
1872
- addTask:'+ Add Task',
1873
- agentWorking:'Agent working →',
1874
- colTodo:'To Do', colProg:'In Progress', colDone:'Done', colFailed:'Failed',
1875
- tabActivity:'Activity', tabDetail:'Detail',
1876
- activityEmpty:'Waiting for activity...<br><br>Agents will log<br>their work here.',
1877
- detailEmpty:'Click any task card<br>to see its details.',
1878
- addTaskTitle:'Add Task',
1879
- fieldTitle:'Title', fieldDesc:'Description', fieldPriority:'Priority', fieldType:'Type',
1880
- placeholderTitle:'Implement login screen...',
1881
- placeholderDesc:'Detailed description of what needs to be done...',
1882
- prioHigh:'High', prioMed:'Medium', prioLow:'Low',
1883
- typeFeature:'Feature', typeBug:'Bug', typeConfig:'Config', typeRefactor:'Refactor', typeTest:'Test',
1884
- cancel:'Cancel', createTask:'Create Task',
1885
- retryTitle:'Edit & Retry Failed Task',
1886
- noErrorLog:'No error log found.',
1887
- agentNoteLabel:'💬 Note for the agent (hint to fix the issue)',
1888
- agentNotePlaceholder:'e.g. Use tailwind v3 not v4. The error is about missing module X...',
1889
- retryBtn:'↩ Retry Task',
1890
- terminal:'⌨️ Terminal',
1891
- statusStopped:'stopped', statusInstalling:'installing', statusStarting:'starting',
1892
- statusRunning:'running', statusError:'error',
1893
- startExpo:'Start Expo', stopExpo:'Stop',
1894
- expoIdle:'Expo not started. Click "Start Expo" to install dependencies and launch with tunnel.',
1895
- scanWith:'SCAN WITH EXPO GO',
1896
- retryCard:'↩ Retry',
1897
- editTaskTitle:'Edit Task', saveTask:'Save Changes', deleteConfirm:'Delete this task?',
1898
- },
1899
- es: {
1900
- todo:'pendiente', running:'en curso', done:'listo', failed:'fallido',
1901
- connecting:'conectando', live:'en vivo', reconnecting:'reconectando',
1902
- addTask:'+ Nueva tarea',
1903
- agentWorking:'Agente trabajando →',
1904
- colTodo:'Por hacer', colProg:'En progreso', colDone:'Hecho', colFailed:'Fallido',
1905
- tabActivity:'Actividad', tabDetail:'Detalle',
1906
- activityEmpty:'Esperando actividad...<br><br>Los agentes registrarán<br>su trabajo aquí.',
1907
- detailEmpty:'Hacé clic en una tarea<br>para ver sus detalles.',
1908
- addTaskTitle:'Nueva tarea',
1909
- fieldTitle:'Título', fieldDesc:'Descripción', fieldPriority:'Prioridad', fieldType:'Tipo',
1910
- placeholderTitle:'Implementar pantalla de login...',
1911
- placeholderDesc:'Descripción detallada de lo que hay que hacer...',
1912
- prioHigh:'Alta', prioMed:'Media', prioLow:'Baja',
1913
- typeFeature:'Feature', typeBug:'Bug', typeConfig:'Config', typeRefactor:'Refactor', typeTest:'Test',
1914
- cancel:'Cancelar', createTask:'Crear tarea',
1915
- retryTitle:'Editar y reintentar tarea fallida',
1916
- noErrorLog:'No se encontró log de error.',
1917
- agentNoteLabel:'💬 Nota para el agente (pista para corregir el problema)',
1918
- agentNotePlaceholder:'Ej: Usá tailwind v3 no v4. El error es por el módulo X...',
1919
- retryBtn:'↩ Reintentar',
1920
- terminal:'⌨️ Terminal',
1921
- statusStopped:'detenido', statusInstalling:'instalando', statusStarting:'iniciando',
1922
- statusRunning:'activo', statusError:'error',
1923
- startExpo:'Iniciar Expo', stopExpo:'Detener',
1924
- expoIdle:'Expo no iniciado. Hacé clic en "Iniciar Expo" para instalar dependencias y lanzar con tunnel.',
1925
- scanWith:'ESCANEAR CON EXPO GO',
1926
- retryCard:'↩ Reintentar',
1927
- editTaskTitle:'Editar tarea', saveTask:'Guardar cambios', deleteConfirm:'¿Eliminar esta tarea?',
1928
- }
1929
- };
1930
-
1931
- let currentLang = localStorage.getItem('cb-lang') || 'en';
1932
-
1933
- function t(key) {
1934
- return STRINGS[currentLang][key] ?? STRINGS.en[key] ?? key;
1935
- }
1936
-
1937
- function setLang(lang) {
1938
- currentLang = lang;
1939
- localStorage.setItem('cb-lang', lang);
1940
- document.getElementById('lang-en').classList.toggle('active', lang === 'en');
1941
- document.getElementById('lang-es').classList.toggle('active', lang === 'es');
1942
- applyTranslations();
1943
- }
1944
-
1945
- function applyTranslations() {
1946
- document.querySelectorAll('[data-i18n]').forEach(el => {
1947
- el.innerHTML = t(el.getAttribute('data-i18n'));
1948
- });
1949
- document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
1950
- el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
1951
- });
1952
- // Re-sync expo badge text based on current status class
1953
- ['expoBadge','expoPanelBadge'].forEach(id => {
1954
- const el = document.getElementById(id);
1955
- if (!el) return;
1956
- const status = [...el.classList].find(c =>
1957
- ['stopped','installing','starting','running','error'].includes(c)
1958
- ) || 'stopped';
1959
- el.textContent = t('status' + status.charAt(0).toUpperCase() + status.slice(1));
1960
- });
1961
- }
1962
-
1963
- // ── INIT ──────────────────────────────────────────────────────────────────────
1964
- loadBoard();
1965
- connectWS();
1966
- // Only poll when WS is disconnected (fallback)
1967
- let pollInterval = null;
1968
-
1969
- // Load expo status
1970
- fetch('/api/expo/status').then(r => r.json()).then(d => setExpoStatus(d.status, d.url));
1971
-
1972
- // Apply saved language on load
1973
- (function() {
1974
- document.getElementById('lang-en').classList.toggle('active', currentLang === 'en');
1975
- document.getElementById('lang-es').classList.toggle('active', currentLang === 'es');
1976
- applyTranslations();
1977
- })();
1978
- </script>
1979
- </body>
1980
- </html>