claudeboard 1.0.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.
@@ -0,0 +1,983 @@
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 rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
9
+ <style>
10
+ :root {
11
+ --bg: #0a0a0b;
12
+ --surface: #111114;
13
+ --border: #1e1e24;
14
+ --border-bright: #2e2e38;
15
+ --text: #e8e8f0;
16
+ --muted: #5a5a70;
17
+ --accent: #00d4ff;
18
+ --accent-dim: rgba(0,212,255,0.08);
19
+ --green: #00ff88;
20
+ --green-dim: rgba(0,255,136,0.08);
21
+ --yellow: #ffcc00;
22
+ --yellow-dim: rgba(255,204,0,0.08);
23
+ --red: #ff4466;
24
+ --red-dim: rgba(255,68,102,0.08);
25
+ --purple: #9b6dff;
26
+ --font-mono: 'IBM Plex Mono', monospace;
27
+ --font-sans: 'IBM Plex Sans', sans-serif;
28
+ }
29
+
30
+ * { margin: 0; padding: 0; box-sizing: border-box; }
31
+
32
+ body {
33
+ background: var(--bg);
34
+ color: var(--text);
35
+ font-family: var(--font-sans);
36
+ min-height: 100vh;
37
+ display: flex;
38
+ flex-direction: column;
39
+ overflow: hidden;
40
+ }
41
+
42
+ /* ── HEADER ── */
43
+ header {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ padding: 0 24px;
48
+ height: 52px;
49
+ border-bottom: 1px solid var(--border);
50
+ background: var(--surface);
51
+ flex-shrink: 0;
52
+ }
53
+
54
+ .logo {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 10px;
58
+ font-family: var(--font-mono);
59
+ font-size: 13px;
60
+ font-weight: 600;
61
+ letter-spacing: 0.1em;
62
+ color: var(--accent);
63
+ }
64
+
65
+ .logo-dot {
66
+ width: 8px; height: 8px;
67
+ background: var(--accent);
68
+ border-radius: 50%;
69
+ box-shadow: 0 0 8px var(--accent);
70
+ animation: pulse 2s ease-in-out infinite;
71
+ }
72
+
73
+ @keyframes pulse {
74
+ 0%, 100% { opacity: 1; box-shadow: 0 0 8px var(--accent); }
75
+ 50% { opacity: 0.5; box-shadow: 0 0 3px var(--accent); }
76
+ }
77
+
78
+ .header-right {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 20px;
82
+ }
83
+
84
+ .project-name {
85
+ font-family: var(--font-mono);
86
+ font-size: 11px;
87
+ color: var(--muted);
88
+ text-transform: uppercase;
89
+ letter-spacing: 0.15em;
90
+ }
91
+
92
+ .ws-status {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 6px;
96
+ font-family: var(--font-mono);
97
+ font-size: 10px;
98
+ color: var(--muted);
99
+ }
100
+
101
+ .ws-dot {
102
+ width: 6px; height: 6px;
103
+ border-radius: 50%;
104
+ background: var(--muted);
105
+ }
106
+
107
+ .ws-dot.connected { background: var(--green); box-shadow: 0 0 6px var(--green); }
108
+
109
+ /* ── STATS BAR ── */
110
+ .stats-bar {
111
+ display: flex;
112
+ gap: 1px;
113
+ padding: 0 24px;
114
+ height: 44px;
115
+ align-items: center;
116
+ border-bottom: 1px solid var(--border);
117
+ background: var(--surface);
118
+ flex-shrink: 0;
119
+ }
120
+
121
+ .stat {
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ padding: 0 16px;
126
+ height: 28px;
127
+ border-radius: 4px;
128
+ font-family: var(--font-mono);
129
+ font-size: 11px;
130
+ }
131
+
132
+ .stat-value { font-weight: 600; font-size: 13px; }
133
+ .stat-label { color: var(--muted); }
134
+ .stat.todo .stat-value { color: var(--muted); }
135
+ .stat.progress .stat-value { color: var(--yellow); }
136
+ .stat.done .stat-value { color: var(--green); }
137
+ .stat.error .stat-value { color: var(--red); }
138
+
139
+ .progress-bar-wrap {
140
+ flex: 1;
141
+ margin-left: 16px;
142
+ height: 4px;
143
+ background: var(--border);
144
+ border-radius: 2px;
145
+ overflow: hidden;
146
+ }
147
+
148
+ .progress-bar-fill {
149
+ height: 100%;
150
+ background: var(--green);
151
+ border-radius: 2px;
152
+ transition: width 0.6s ease;
153
+ box-shadow: 0 0 8px var(--green);
154
+ }
155
+
156
+ /* ── MAIN LAYOUT ── */
157
+ .main {
158
+ flex: 1;
159
+ display: grid;
160
+ grid-template-columns: 1fr 320px;
161
+ overflow: hidden;
162
+ }
163
+
164
+ /* ── BOARD ── */
165
+ .board {
166
+ overflow-y: auto;
167
+ padding: 20px 24px;
168
+ }
169
+
170
+ .board::-webkit-scrollbar { width: 4px; }
171
+ .board::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; }
172
+
173
+ .board-header {
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: space-between;
177
+ margin-bottom: 20px;
178
+ }
179
+
180
+ .board-title {
181
+ font-family: var(--font-mono);
182
+ font-size: 11px;
183
+ color: var(--muted);
184
+ text-transform: uppercase;
185
+ letter-spacing: 0.15em;
186
+ }
187
+
188
+ .btn-add {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 6px;
192
+ padding: 6px 12px;
193
+ background: var(--accent-dim);
194
+ border: 1px solid rgba(0,212,255,0.2);
195
+ border-radius: 4px;
196
+ color: var(--accent);
197
+ font-family: var(--font-mono);
198
+ font-size: 11px;
199
+ cursor: pointer;
200
+ transition: all 0.15s;
201
+ }
202
+
203
+ .btn-add:hover {
204
+ background: rgba(0,212,255,0.15);
205
+ border-color: rgba(0,212,255,0.4);
206
+ }
207
+
208
+ /* ── EPIC ── */
209
+ .epic {
210
+ margin-bottom: 28px;
211
+ }
212
+
213
+ .epic-header {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 10px;
217
+ margin-bottom: 8px;
218
+ padding: 0 4px;
219
+ }
220
+
221
+ .epic-name {
222
+ font-size: 12px;
223
+ font-weight: 600;
224
+ color: var(--purple);
225
+ font-family: var(--font-mono);
226
+ text-transform: uppercase;
227
+ letter-spacing: 0.1em;
228
+ }
229
+
230
+ .epic-line {
231
+ flex: 1;
232
+ height: 1px;
233
+ background: var(--border);
234
+ }
235
+
236
+ .epic-count {
237
+ font-family: var(--font-mono);
238
+ font-size: 10px;
239
+ color: var(--muted);
240
+ }
241
+
242
+ /* ── TASK CARD ── */
243
+ .task {
244
+ display: flex;
245
+ align-items: flex-start;
246
+ gap: 12px;
247
+ padding: 12px 14px;
248
+ background: var(--surface);
249
+ border: 1px solid var(--border);
250
+ border-radius: 6px;
251
+ margin-bottom: 4px;
252
+ cursor: pointer;
253
+ transition: all 0.15s;
254
+ position: relative;
255
+ }
256
+
257
+ .task:hover {
258
+ border-color: var(--border-bright);
259
+ background: #14141a;
260
+ }
261
+
262
+ .task.in_progress {
263
+ border-color: rgba(255,204,0,0.3);
264
+ background: rgba(255,204,0,0.03);
265
+ }
266
+
267
+ .task.in_progress::before {
268
+ content: '';
269
+ position: absolute;
270
+ left: 0; top: 0; bottom: 0;
271
+ width: 2px;
272
+ background: var(--yellow);
273
+ border-radius: 6px 0 0 6px;
274
+ box-shadow: 0 0 8px var(--yellow);
275
+ }
276
+
277
+ .task.done {
278
+ border-color: var(--border);
279
+ opacity: 0.45;
280
+ }
281
+
282
+ .task.done .task-title { text-decoration: line-through; }
283
+
284
+ .task.error {
285
+ border-color: rgba(255,68,102,0.3);
286
+ background: rgba(255,68,102,0.03);
287
+ }
288
+
289
+ .task-status-icon {
290
+ width: 18px; height: 18px;
291
+ border-radius: 50%;
292
+ flex-shrink: 0;
293
+ margin-top: 1px;
294
+ display: flex;
295
+ align-items: center;
296
+ justify-content: center;
297
+ font-size: 10px;
298
+ }
299
+
300
+ .task-status-icon.todo {
301
+ border: 1.5px solid var(--border-bright);
302
+ }
303
+
304
+ .task-status-icon.in_progress {
305
+ border: 1.5px solid var(--yellow);
306
+ background: var(--yellow-dim);
307
+ animation: spin-ring 1.5s linear infinite;
308
+ }
309
+
310
+ @keyframes spin-ring {
311
+ from { box-shadow: 2px 0 0 var(--yellow); }
312
+ to { box-shadow: 2px 0 0 var(--yellow); }
313
+ }
314
+
315
+ .task-status-icon.done {
316
+ background: var(--green-dim);
317
+ border: 1.5px solid var(--green);
318
+ color: var(--green);
319
+ }
320
+
321
+ .task-status-icon.error {
322
+ background: var(--red-dim);
323
+ border: 1.5px solid var(--red);
324
+ color: var(--red);
325
+ }
326
+
327
+ .task-body { flex: 1; min-width: 0; }
328
+
329
+ .task-title {
330
+ font-size: 13px;
331
+ font-weight: 500;
332
+ color: var(--text);
333
+ margin-bottom: 4px;
334
+ line-height: 1.3;
335
+ }
336
+
337
+ .task-desc {
338
+ font-size: 11px;
339
+ color: var(--muted);
340
+ line-height: 1.4;
341
+ white-space: nowrap;
342
+ overflow: hidden;
343
+ text-overflow: ellipsis;
344
+ }
345
+
346
+ .task-meta {
347
+ display: flex;
348
+ align-items: center;
349
+ gap: 6px;
350
+ margin-top: 6px;
351
+ }
352
+
353
+ .badge {
354
+ padding: 2px 6px;
355
+ border-radius: 3px;
356
+ font-family: var(--font-mono);
357
+ font-size: 9px;
358
+ font-weight: 600;
359
+ text-transform: uppercase;
360
+ letter-spacing: 0.05em;
361
+ }
362
+
363
+ .badge.high { background: rgba(255,68,102,0.15); color: var(--red); }
364
+ .badge.medium { background: rgba(255,204,0,0.12); color: var(--yellow); }
365
+ .badge.low { background: rgba(90,90,112,0.2); color: var(--muted); }
366
+ .badge.feature { background: rgba(0,212,255,0.1); color: var(--accent); }
367
+ .badge.bug { background: rgba(255,68,102,0.1); color: var(--red); }
368
+ .badge.refactor { background: rgba(155,109,255,0.1); color: var(--purple); }
369
+ .badge.test { background: rgba(0,255,136,0.1); color: var(--green); }
370
+ .badge.config { background: rgba(90,90,112,0.15); color: var(--muted); }
371
+
372
+ /* ── SIDEBAR ── */
373
+ .sidebar {
374
+ border-left: 1px solid var(--border);
375
+ display: flex;
376
+ flex-direction: column;
377
+ overflow: hidden;
378
+ }
379
+
380
+ .sidebar-tabs {
381
+ display: flex;
382
+ border-bottom: 1px solid var(--border);
383
+ flex-shrink: 0;
384
+ }
385
+
386
+ .tab {
387
+ flex: 1;
388
+ padding: 10px;
389
+ font-family: var(--font-mono);
390
+ font-size: 10px;
391
+ text-transform: uppercase;
392
+ letter-spacing: 0.1em;
393
+ color: var(--muted);
394
+ cursor: pointer;
395
+ text-align: center;
396
+ border-bottom: 2px solid transparent;
397
+ transition: all 0.15s;
398
+ background: none;
399
+ border-top: none;
400
+ border-left: none;
401
+ border-right: none;
402
+ }
403
+
404
+ .tab.active {
405
+ color: var(--accent);
406
+ border-bottom-color: var(--accent);
407
+ }
408
+
409
+ .sidebar-content {
410
+ flex: 1;
411
+ overflow-y: auto;
412
+ padding: 12px;
413
+ }
414
+
415
+ .sidebar-content::-webkit-scrollbar { width: 3px; }
416
+ .sidebar-content::-webkit-scrollbar-thumb { background: var(--border-bright); }
417
+
418
+ /* ── ACTIVITY LOG ── */
419
+ .log-entry {
420
+ display: flex;
421
+ gap: 8px;
422
+ padding: 8px 0;
423
+ border-bottom: 1px solid var(--border);
424
+ font-family: var(--font-mono);
425
+ font-size: 10px;
426
+ line-height: 1.4;
427
+ }
428
+
429
+ .log-entry:last-child { border-bottom: none; }
430
+
431
+ .log-time { color: var(--muted); flex-shrink: 0; }
432
+
433
+ .log-msg { color: var(--text); }
434
+ .log-msg.start { color: var(--accent); }
435
+ .log-msg.complete { color: var(--green); }
436
+ .log-msg.error { color: var(--red); }
437
+ .log-msg.progress { color: var(--text); }
438
+
439
+ .log-type-icon { flex-shrink: 0; }
440
+
441
+ /* ── ADD TASK PANEL ── */
442
+ .add-panel {
443
+ padding: 12px;
444
+ border-bottom: 1px solid var(--border);
445
+ display: none;
446
+ }
447
+
448
+ .add-panel.open { display: block; }
449
+
450
+ .field {
451
+ margin-bottom: 10px;
452
+ }
453
+
454
+ .field label {
455
+ display: block;
456
+ font-family: var(--font-mono);
457
+ font-size: 10px;
458
+ color: var(--muted);
459
+ text-transform: uppercase;
460
+ letter-spacing: 0.1em;
461
+ margin-bottom: 4px;
462
+ }
463
+
464
+ .field input, .field textarea, .field select {
465
+ width: 100%;
466
+ background: var(--bg);
467
+ border: 1px solid var(--border-bright);
468
+ border-radius: 4px;
469
+ padding: 7px 10px;
470
+ color: var(--text);
471
+ font-family: var(--font-mono);
472
+ font-size: 11px;
473
+ outline: none;
474
+ resize: vertical;
475
+ }
476
+
477
+ .field input:focus, .field textarea:focus, .field select:focus {
478
+ border-color: rgba(0,212,255,0.4);
479
+ }
480
+
481
+ .field select option { background: var(--surface); }
482
+
483
+ .btn-submit {
484
+ width: 100%;
485
+ padding: 8px;
486
+ background: var(--accent);
487
+ border: none;
488
+ border-radius: 4px;
489
+ color: #000;
490
+ font-family: var(--font-mono);
491
+ font-size: 11px;
492
+ font-weight: 600;
493
+ cursor: pointer;
494
+ transition: opacity 0.15s;
495
+ }
496
+
497
+ .btn-submit:hover { opacity: 0.85; }
498
+
499
+ /* ── TASK DETAIL ── */
500
+ .task-detail {
501
+ padding: 0;
502
+ }
503
+
504
+ .detail-title {
505
+ font-size: 13px;
506
+ font-weight: 600;
507
+ color: var(--text);
508
+ margin-bottom: 8px;
509
+ line-height: 1.4;
510
+ }
511
+
512
+ .detail-desc {
513
+ font-size: 11px;
514
+ color: var(--muted);
515
+ line-height: 1.6;
516
+ margin-bottom: 12px;
517
+ font-family: var(--font-mono);
518
+ }
519
+
520
+ .detail-logs {
521
+ margin-top: 12px;
522
+ }
523
+
524
+ .detail-logs-title {
525
+ font-family: var(--font-mono);
526
+ font-size: 10px;
527
+ color: var(--muted);
528
+ text-transform: uppercase;
529
+ letter-spacing: 0.1em;
530
+ margin-bottom: 8px;
531
+ }
532
+
533
+ /* ── MODAL ── */
534
+ .modal-backdrop {
535
+ position: fixed;
536
+ inset: 0;
537
+ background: rgba(0,0,0,0.7);
538
+ display: flex;
539
+ align-items: center;
540
+ justify-content: center;
541
+ z-index: 100;
542
+ display: none;
543
+ }
544
+
545
+ .modal-backdrop.open { display: flex; }
546
+
547
+ .modal {
548
+ background: var(--surface);
549
+ border: 1px solid var(--border-bright);
550
+ border-radius: 8px;
551
+ width: 480px;
552
+ max-width: 90vw;
553
+ padding: 24px;
554
+ }
555
+
556
+ .modal-title {
557
+ font-family: var(--font-mono);
558
+ font-size: 12px;
559
+ color: var(--accent);
560
+ text-transform: uppercase;
561
+ letter-spacing: 0.1em;
562
+ margin-bottom: 20px;
563
+ }
564
+
565
+ .modal-actions {
566
+ display: flex;
567
+ gap: 8px;
568
+ justify-content: flex-end;
569
+ margin-top: 16px;
570
+ }
571
+
572
+ .btn-cancel {
573
+ padding: 7px 14px;
574
+ background: none;
575
+ border: 1px solid var(--border-bright);
576
+ border-radius: 4px;
577
+ color: var(--muted);
578
+ font-family: var(--font-mono);
579
+ font-size: 11px;
580
+ cursor: pointer;
581
+ }
582
+
583
+ .btn-cancel:hover { border-color: var(--text); color: var(--text); }
584
+
585
+ /* ── EMPTY STATE ── */
586
+ .empty {
587
+ text-align: center;
588
+ padding: 48px 24px;
589
+ font-family: var(--font-mono);
590
+ font-size: 11px;
591
+ color: var(--muted);
592
+ line-height: 2;
593
+ }
594
+
595
+ .empty code {
596
+ color: var(--accent);
597
+ background: var(--accent-dim);
598
+ padding: 2px 6px;
599
+ border-radius: 3px;
600
+ }
601
+
602
+ /* ── CURRENT TASK INDICATOR ── */
603
+ .current-task-bar {
604
+ background: rgba(255,204,0,0.05);
605
+ border-top: 1px solid rgba(255,204,0,0.15);
606
+ padding: 8px 24px;
607
+ display: flex;
608
+ align-items: center;
609
+ gap: 10px;
610
+ flex-shrink: 0;
611
+ font-family: var(--font-mono);
612
+ font-size: 10px;
613
+ color: var(--yellow);
614
+ min-height: 36px;
615
+ }
616
+
617
+ .current-task-label { color: var(--muted); }
618
+
619
+ .spinner {
620
+ width: 12px; height: 12px;
621
+ border: 1.5px solid rgba(255,204,0,0.3);
622
+ border-top-color: var(--yellow);
623
+ border-radius: 50%;
624
+ animation: rotate 0.8s linear infinite;
625
+ }
626
+
627
+ @keyframes rotate { to { transform: rotate(360deg); } }
628
+
629
+ .fade-in {
630
+ animation: fadeIn 0.2s ease;
631
+ }
632
+
633
+ @keyframes fadeIn {
634
+ from { opacity: 0; transform: translateY(4px); }
635
+ to { opacity: 1; transform: translateY(0); }
636
+ }
637
+ </style>
638
+ </head>
639
+ <body>
640
+
641
+ <!-- HEADER -->
642
+ <header>
643
+ <div class="logo">
644
+ <div class="logo-dot"></div>
645
+ CLAUDEBOARD
646
+ </div>
647
+ <div class="header-right">
648
+ <span class="project-name" id="projectName">—</span>
649
+ <div class="ws-status">
650
+ <div class="ws-dot" id="wsDot"></div>
651
+ <span id="wsLabel">connecting</span>
652
+ </div>
653
+ </div>
654
+ </header>
655
+
656
+ <!-- STATS BAR -->
657
+ <div class="stats-bar">
658
+ <div class="stat todo">
659
+ <span class="stat-value" id="statTodo">0</span>
660
+ <span class="stat-label">todo</span>
661
+ </div>
662
+ <div class="stat progress">
663
+ <span class="stat-value" id="statProgress">0</span>
664
+ <span class="stat-label">running</span>
665
+ </div>
666
+ <div class="stat done">
667
+ <span class="stat-value" id="statDone">0</span>
668
+ <span class="stat-label">done</span>
669
+ </div>
670
+ <div class="stat error">
671
+ <span class="stat-value" id="statError">0</span>
672
+ <span class="stat-label">failed</span>
673
+ </div>
674
+ <div class="progress-bar-wrap">
675
+ <div class="progress-bar-fill" id="progressBar" style="width:0%"></div>
676
+ </div>
677
+ </div>
678
+
679
+ <!-- MAIN -->
680
+ <div class="main">
681
+ <!-- BOARD -->
682
+ <div class="board">
683
+ <div class="board-header">
684
+ <span class="board-title">Tasks</span>
685
+ <button class="btn-add" onclick="openModal()">+ Add Task</button>
686
+ </div>
687
+ <div id="boardContent">
688
+ <div class="empty">
689
+ No tasks yet.<br>
690
+ Run <code>claudeboard import-prd ./PRD.md</code><br>
691
+ or add tasks manually.
692
+ </div>
693
+ </div>
694
+ </div>
695
+
696
+ <!-- SIDEBAR -->
697
+ <div class="sidebar">
698
+ <div class="sidebar-tabs">
699
+ <button class="tab active" onclick="switchTab('log')" id="tabLog">Activity</button>
700
+ <button class="tab" onclick="switchTab('detail')" id="tabDetail">Detail</button>
701
+ </div>
702
+ <div class="sidebar-content" id="sidebarContent">
703
+ <div class="empty" style="padding:32px 12px">Waiting for activity...</div>
704
+ </div>
705
+ </div>
706
+ </div>
707
+
708
+ <!-- CURRENT TASK BAR -->
709
+ <div class="current-task-bar" id="currentTaskBar" style="display:none">
710
+ <div class="spinner"></div>
711
+ <span class="current-task-label">RUNNING →</span>
712
+ <span id="currentTaskTitle">—</span>
713
+ </div>
714
+
715
+ <!-- ADD TASK MODAL -->
716
+ <div class="modal-backdrop" id="modal">
717
+ <div class="modal">
718
+ <div class="modal-title">// Add Task</div>
719
+ <div class="field">
720
+ <label>Title</label>
721
+ <input type="text" id="newTitle" placeholder="Implement authentication screen">
722
+ </div>
723
+ <div class="field">
724
+ <label>Description</label>
725
+ <textarea id="newDesc" rows="3" placeholder="What needs to be done..."></textarea>
726
+ </div>
727
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
728
+ <div class="field">
729
+ <label>Priority</label>
730
+ <select id="newPriority">
731
+ <option value="high">High</option>
732
+ <option value="medium" selected>Medium</option>
733
+ <option value="low">Low</option>
734
+ </select>
735
+ </div>
736
+ <div class="field">
737
+ <label>Type</label>
738
+ <select id="newType">
739
+ <option value="feature" selected>Feature</option>
740
+ <option value="bug">Bug</option>
741
+ <option value="refactor">Refactor</option>
742
+ <option value="test">Test</option>
743
+ <option value="config">Config</option>
744
+ </select>
745
+ </div>
746
+ </div>
747
+ <div class="modal-actions">
748
+ <button class="btn-cancel" onclick="closeModal()">Cancel</button>
749
+ <button class="btn-submit" onclick="submitTask()">Add Task</button>
750
+ </div>
751
+ </div>
752
+ </div>
753
+
754
+ <script>
755
+ const API = '';
756
+ let ws;
757
+ let board = { epics: [], logs: [] };
758
+ let activeTab = 'log';
759
+ let selectedTask = null;
760
+
761
+ // ── WS ────────────────────────────────────────────────────────────────────────
762
+ function connectWS() {
763
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
764
+ ws = new WebSocket(`${proto}//${location.host}`);
765
+
766
+ ws.onopen = () => setWS(true);
767
+ ws.onclose = () => { setWS(false); setTimeout(connectWS, 2000); };
768
+
769
+ ws.onmessage = (e) => {
770
+ const { event, data } = JSON.parse(e.data);
771
+ if (event === 'task_update' || event === 'task_added' || event === 'task_started' ||
772
+ event === 'task_complete' || event === 'task_failed') {
773
+ loadBoard();
774
+ }
775
+ if (event === 'log') {
776
+ board.logs.unshift(data);
777
+ if (activeTab === 'log') renderLogs();
778
+ }
779
+ };
780
+ }
781
+
782
+ function setWS(on) {
783
+ document.getElementById('wsDot').className = 'ws-dot' + (on ? ' connected' : '');
784
+ document.getElementById('wsLabel').textContent = on ? 'live' : 'reconnecting';
785
+ }
786
+
787
+ // ── DATA ──────────────────────────────────────────────────────────────────────
788
+ async function loadBoard() {
789
+ const res = await fetch('/api/board');
790
+ const data = await res.json();
791
+ board = data;
792
+ document.getElementById('projectName').textContent = data.project || '—';
793
+ renderBoard();
794
+ updateStats();
795
+ if (activeTab === 'log') renderLogs();
796
+ }
797
+
798
+ function updateStats() {
799
+ const allTasks = board.epics.flatMap(e => e.cb_tasks || []);
800
+ const todo = allTasks.filter(t => t.status === 'todo').length;
801
+ const progress = allTasks.filter(t => t.status === 'in_progress').length;
802
+ const done = allTasks.filter(t => t.status === 'done').length;
803
+ const error = allTasks.filter(t => t.status === 'error').length;
804
+ const total = allTasks.length;
805
+
806
+ document.getElementById('statTodo').textContent = todo;
807
+ document.getElementById('statProgress').textContent = progress;
808
+ document.getElementById('statDone').textContent = done;
809
+ document.getElementById('statError').textContent = error;
810
+
811
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
812
+ document.getElementById('progressBar').style.width = pct + '%';
813
+
814
+ // Current task bar
815
+ const running = allTasks.find(t => t.status === 'in_progress');
816
+ const bar = document.getElementById('currentTaskBar');
817
+ if (running) {
818
+ bar.style.display = 'flex';
819
+ document.getElementById('currentTaskTitle').textContent = running.title;
820
+ } else {
821
+ bar.style.display = 'none';
822
+ }
823
+ }
824
+
825
+ // ── RENDER BOARD ──────────────────────────────────────────────────────────────
826
+ function renderBoard() {
827
+ const el = document.getElementById('boardContent');
828
+
829
+ if (!board.epics || board.epics.length === 0) {
830
+ el.innerHTML = `<div class="empty">No tasks yet.<br>Run <code>claudeboard import-prd ./PRD.md</code><br>or add tasks manually.</div>`;
831
+ return;
832
+ }
833
+
834
+ el.innerHTML = board.epics.map(epic => {
835
+ const tasks = epic.cb_tasks || [];
836
+ const done = tasks.filter(t => t.status === 'done').length;
837
+ return `
838
+ <div class="epic">
839
+ <div class="epic-header">
840
+ <span class="epic-name">${esc(epic.name)}</span>
841
+ <div class="epic-line"></div>
842
+ <span class="epic-count">${done}/${tasks.length}</span>
843
+ </div>
844
+ ${tasks.map(task => renderTask(task)).join('')}
845
+ </div>
846
+ `;
847
+ }).join('');
848
+ }
849
+
850
+ function renderTask(task) {
851
+ const icons = { todo: '', in_progress: '◌', done: '✓', error: '✕', blocked: '—' };
852
+ return `
853
+ <div class="task ${task.status} fade-in" onclick="selectTask('${task.id}')">
854
+ <div class="task-status-icon ${task.status}">${icons[task.status] || ''}</div>
855
+ <div class="task-body">
856
+ <div class="task-title">${esc(task.title)}</div>
857
+ ${task.description ? `<div class="task-desc">${esc(task.description)}</div>` : ''}
858
+ <div class="task-meta">
859
+ <span class="badge ${task.priority}">${task.priority}</span>
860
+ <span class="badge ${task.type}">${task.type}</span>
861
+ </div>
862
+ </div>
863
+ </div>
864
+ `;
865
+ }
866
+
867
+ // ── LOGS ──────────────────────────────────────────────────────────────────────
868
+ function renderLogs() {
869
+ const el = document.getElementById('sidebarContent');
870
+ if (!board.logs || board.logs.length === 0) {
871
+ el.innerHTML = `<div class="empty" style="padding:32px 12px">Waiting for activity...</div>`;
872
+ return;
873
+ }
874
+
875
+ el.innerHTML = board.logs.map(log => {
876
+ const time = new Date(log.created_at).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
877
+ const typeIcons = { start: '▶', complete: '✓', error: '✕', progress: '·', info: '·' };
878
+ return `
879
+ <div class="log-entry">
880
+ <span class="log-time">${time}</span>
881
+ <span class="log-type-icon" style="color:${logColor(log.type)}">${typeIcons[log.type] || '·'}</span>
882
+ <span class="log-msg ${log.type}">${esc(log.message)}</span>
883
+ </div>
884
+ `;
885
+ }).join('');
886
+ }
887
+
888
+ function logColor(type) {
889
+ const c = { start: '#00d4ff', complete: '#00ff88', error: '#ff4466', progress: '#5a5a70', info: '#5a5a70' };
890
+ return c[type] || '#5a5a70';
891
+ }
892
+
893
+ // ── TASK DETAIL ───────────────────────────────────────────────────────────────
894
+ async function selectTask(id) {
895
+ activeTab = 'detail';
896
+ document.getElementById('tabLog').className = 'tab';
897
+ document.getElementById('tabDetail').className = 'tab active';
898
+
899
+ const allTasks = board.epics.flatMap(e => e.cb_tasks || []);
900
+ selectedTask = allTasks.find(t => t.id === id);
901
+
902
+ const logsRes = await fetch(`/api/tasks/${id}/logs`);
903
+ const { logs } = await logsRes.json();
904
+
905
+ const el = document.getElementById('sidebarContent');
906
+ el.innerHTML = `
907
+ <div class="task-detail">
908
+ <div class="detail-title">${esc(selectedTask.title)}</div>
909
+ <div style="display:flex;gap:6px;margin-bottom:10px">
910
+ <span class="badge ${selectedTask.status}">${selectedTask.status}</span>
911
+ <span class="badge ${selectedTask.priority}">${selectedTask.priority}</span>
912
+ <span class="badge ${selectedTask.type}">${selectedTask.type}</span>
913
+ </div>
914
+ ${selectedTask.description ? `<div class="detail-desc">${esc(selectedTask.description)}</div>` : ''}
915
+ <div class="detail-logs">
916
+ <div class="detail-logs-title">// logs</div>
917
+ ${logs.length === 0 ? '<div style="font-family:var(--font-mono);font-size:10px;color:var(--muted)">No logs yet.</div>' :
918
+ logs.map(log => {
919
+ const time = new Date(log.created_at).toLocaleTimeString('en', { hour12: false });
920
+ return `<div class="log-entry"><span class="log-time">${time}</span><span class="log-msg ${log.type}">${esc(log.message)}</span></div>`;
921
+ }).join('')}
922
+ </div>
923
+ </div>
924
+ `;
925
+ }
926
+
927
+ // ── TABS ──────────────────────────────────────────────────────────────────────
928
+ function switchTab(tab) {
929
+ activeTab = tab;
930
+ document.getElementById('tabLog').className = 'tab' + (tab === 'log' ? ' active' : '');
931
+ document.getElementById('tabDetail').className = 'tab' + (tab === 'detail' ? ' active' : '');
932
+ if (tab === 'log') renderLogs();
933
+ }
934
+
935
+ // ── ADD TASK ──────────────────────────────────────────────────────────────────
936
+ function openModal() { document.getElementById('modal').className = 'modal-backdrop open'; }
937
+ function closeModal() { document.getElementById('modal').className = 'modal-backdrop'; }
938
+
939
+ async function submitTask() {
940
+ const title = document.getElementById('newTitle').value.trim();
941
+ if (!title) return;
942
+
943
+ await fetch('/api/tasks', {
944
+ method: 'POST',
945
+ headers: { 'Content-Type': 'application/json' },
946
+ body: JSON.stringify({
947
+ title,
948
+ description: document.getElementById('newDesc').value.trim(),
949
+ priority: document.getElementById('newPriority').value,
950
+ type: document.getElementById('newType').value,
951
+ })
952
+ });
953
+
954
+ document.getElementById('newTitle').value = '';
955
+ document.getElementById('newDesc').value = '';
956
+ closeModal();
957
+ loadBoard();
958
+ }
959
+
960
+ // ── UTILS ─────────────────────────────────────────────────────────────────────
961
+ function esc(str) {
962
+ if (!str) return '';
963
+ return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
964
+ }
965
+
966
+ // Close modal on backdrop click
967
+ document.getElementById('modal').addEventListener('click', (e) => {
968
+ if (e.target === e.currentTarget) closeModal();
969
+ });
970
+
971
+ // Keyboard shortcuts
972
+ document.addEventListener('keydown', (e) => {
973
+ if (e.key === 'Escape') closeModal();
974
+ if (e.key === 'n' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); openModal(); }
975
+ });
976
+
977
+ // ── INIT ──────────────────────────────────────────────────────────────────────
978
+ loadBoard();
979
+ setInterval(loadBoard, 10000); // Fallback poll every 10s
980
+ connectWS();
981
+ </script>
982
+ </body>
983
+ </html>