claudeboard 1.1.0 → 1.5.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.
@@ -4,373 +4,447 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
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">
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">
9
8
  <style>
10
9
  :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;
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;
28
32
  }
29
33
 
30
34
  * { margin: 0; padding: 0; box-sizing: border-box; }
31
35
 
32
- body {
33
- background: var(--bg);
36
+ html, body {
37
+ height: 100%;
38
+ background: var(--bg2);
34
39
  color: var(--text);
35
- font-family: var(--font-sans);
36
- min-height: 100vh;
37
- display: flex;
38
- flex-direction: column;
40
+ font-family: var(--font);
39
41
  overflow: hidden;
40
42
  }
41
43
 
42
44
  /* ── HEADER ── */
43
- header {
45
+ .header {
46
+ height: 56px;
47
+ background: var(--bg);
48
+ border-bottom: 1px solid var(--border);
44
49
  display: flex;
45
50
  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
+ padding: 0 20px;
52
+ gap: 16px;
51
53
  flex-shrink: 0;
54
+ position: relative;
55
+ z-index: 10;
52
56
  }
53
57
 
54
58
  .logo {
55
59
  display: flex;
56
60
  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;
61
+ gap: 8px;
62
+ font-size: 14px;
63
+ font-weight: 700;
64
+ letter-spacing: 0.08em;
62
65
  color: var(--accent);
66
+ text-transform: uppercase;
63
67
  }
64
68
 
65
- .logo-dot {
69
+ .logo-pulse {
66
70
  width: 8px; height: 8px;
67
71
  background: var(--accent);
68
72
  border-radius: 50%;
69
- box-shadow: 0 0 8px var(--accent);
70
- animation: pulse 2s ease-in-out infinite;
73
+ box-shadow: 0 0 0 0 rgba(108,138,255,0.4);
74
+ animation: pulse-ring 2s ease-out infinite;
71
75
  }
72
76
 
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); }
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); }
76
81
  }
77
82
 
78
- .header-right {
79
- display: flex;
80
- align-items: center;
81
- gap: 20px;
82
- }
83
+ .header-sep { width: 1px; height: 24px; background: var(--border); }
83
84
 
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;
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;
90
94
  }
91
95
 
92
- .ws-status {
96
+ .header-stats {
93
97
  display: flex;
94
98
  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);
99
+ gap: 4px;
100
+ margin-left: 8px;
105
101
  }
106
102
 
107
- .ws-dot.connected { background: var(--green); box-shadow: 0 0 6px var(--green); }
108
-
109
- /* ── STATS BAR ── */
110
- .stats-bar {
103
+ .hstat {
111
104
  display: flex;
112
- gap: 1px;
113
- padding: 0 24px;
114
- height: 44px;
115
105
  align-items: center;
116
- border-bottom: 1px solid var(--border);
117
- background: var(--surface);
118
- flex-shrink: 0;
106
+ gap: 5px;
107
+ padding: 4px 10px;
108
+ border-radius: 6px;
109
+ font-size: 12px;
110
+ font-weight: 600;
111
+ font-family: var(--mono);
119
112
  }
120
113
 
121
- .stat {
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;
122
129
  display: flex;
123
130
  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;
131
+ gap: 10px;
130
132
  }
131
133
 
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;
134
+ .progress-wrap {
135
+ width: 120px;
136
+ height: 6px;
143
137
  background: var(--border);
144
- border-radius: 2px;
138
+ border-radius: 3px;
145
139
  overflow: hidden;
146
140
  }
147
141
 
148
- .progress-bar-fill {
142
+ .progress-fill {
149
143
  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;
144
+ background: linear-gradient(90deg, var(--accent), var(--green));
145
+ border-radius: 3px;
146
+ transition: width 0.8s ease;
162
147
  }
163
148
 
164
- /* ── BOARD ── */
165
- .board {
166
- overflow-y: auto;
167
- padding: 20px 24px;
149
+ .progress-pct {
150
+ font-family: var(--mono);
151
+ font-size: 11px;
152
+ color: var(--muted);
153
+ min-width: 32px;
168
154
  }
169
155
 
170
- .board::-webkit-scrollbar { width: 4px; }
171
- .board::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; }
172
-
173
- .board-header {
156
+ .ws-badge {
174
157
  display: flex;
175
158
  align-items: center;
176
- justify-content: space-between;
177
- margin-bottom: 20px;
178
- }
179
-
180
- .board-title {
181
- font-family: var(--font-mono);
159
+ gap: 5px;
182
160
  font-size: 11px;
183
161
  color: var(--muted);
184
- text-transform: uppercase;
185
- letter-spacing: 0.15em;
162
+ font-family: var(--mono);
186
163
  }
187
164
 
188
- .btn-add {
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 {
189
169
  display: flex;
190
170
  align-items: center;
191
171
  gap: 6px;
192
172
  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;
173
+ border-radius: 6px;
174
+ font-size: 12px;
175
+ font-weight: 600;
199
176
  cursor: pointer;
177
+ border: none;
200
178
  transition: all 0.15s;
201
179
  }
202
180
 
203
- .btn-add:hover {
204
- background: rgba(0,212,255,0.15);
205
- border-color: rgba(0,212,255,0.4);
181
+ .btn-primary {
182
+ background: var(--accent);
183
+ color: #fff;
206
184
  }
185
+ .btn-primary:hover { background: #7d9cff; }
207
186
 
208
- /* ── EPIC ── */
209
- .epic {
210
- margin-bottom: 28px;
187
+ .btn-ghost {
188
+ background: rgba(255,255,255,0.06);
189
+ color: var(--text);
190
+ border: 1px solid var(--border);
211
191
  }
192
+ .btn-ghost:hover { background: rgba(255,255,255,0.1); }
212
193
 
213
- .epic-header {
214
- display: flex;
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;
215
200
  align-items: center;
201
+ padding: 0 20px;
216
202
  gap: 10px;
217
- margin-bottom: 8px;
218
- padding: 0 4px;
203
+ flex-shrink: 0;
219
204
  }
220
205
 
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;
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;
228
214
  }
229
215
 
230
- .epic-line {
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;
231
224
  flex: 1;
232
- height: 1px;
233
- background: var(--border);
225
+ overflow: hidden;
226
+ height: calc(100vh - 56px);
234
227
  }
235
228
 
236
- .epic-count {
237
- font-family: var(--font-mono);
238
- font-size: 10px;
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);
239
288
  color: var(--muted);
240
289
  }
241
290
 
242
- /* ── TASK CARD ── */
243
- .task {
291
+ .column-body {
292
+ flex: 1;
293
+ overflow-y: auto;
294
+ padding: 10px;
244
295
  display: flex;
245
- align-items: flex-start;
246
- gap: 12px;
247
- padding: 12px 14px;
248
- background: var(--surface);
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);
249
317
  border: 1px solid var(--border);
250
- border-radius: 6px;
251
- margin-bottom: 4px;
252
- cursor: pointer;
253
- transition: all 0.15s;
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;
254
323
  position: relative;
255
324
  }
256
325
 
257
- .task:hover {
258
- border-color: var(--border-bright);
259
- background: #14141a;
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);
260
331
  }
261
332
 
262
- .task.in_progress {
263
- border-color: rgba(255,204,0,0.3);
264
- background: rgba(255,204,0,0.03);
265
- }
333
+ .card:active { cursor: grabbing; }
266
334
 
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);
335
+ .card.dragging {
336
+ opacity: 0.5;
337
+ transform: rotate(2deg) scale(0.98);
275
338
  }
276
339
 
277
- .task.done {
278
- border-color: var(--border);
279
- opacity: 0.45;
340
+ .card.drag-over {
341
+ border-color: var(--accent);
342
+ box-shadow: 0 0 0 2px rgba(108,138,255,0.3);
280
343
  }
281
344
 
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);
345
+ .card-top {
346
+ display: flex;
347
+ align-items: flex-start;
348
+ gap: 8px;
349
+ margin-bottom: 8px;
287
350
  }
288
351
 
289
- .task-status-icon {
290
- width: 18px; height: 18px;
352
+ .card-status {
353
+ width: 16px; height: 16px;
291
354
  border-radius: 50%;
292
355
  flex-shrink: 0;
293
356
  margin-top: 1px;
294
357
  display: flex;
295
358
  align-items: center;
296
359
  justify-content: center;
297
- font-size: 10px;
298
- }
299
-
300
- .task-status-icon.todo {
301
- border: 1.5px solid var(--border-bright);
360
+ font-size: 9px;
302
361
  }
303
362
 
304
- .task-status-icon.in_progress {
363
+ .card-status.todo { border: 1.5px solid var(--dim); }
364
+ .card-status.in_progress {
305
365
  border: 1.5px solid var(--yellow);
306
- background: var(--yellow-dim);
307
- animation: spin-ring 1.5s linear infinite;
366
+ background: rgba(251,191,36,0.1);
367
+ animation: spin-border 1.2s linear infinite;
308
368
  }
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);
369
+ @keyframes spin-border {
370
+ from { box-shadow: inset 0 0 0 1px transparent, 0 0 0 1px var(--yellow); }
325
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); }
326
375
 
327
- .task-body { flex: 1; min-width: 0; }
328
-
329
- .task-title {
376
+ .card-title {
330
377
  font-size: 13px;
331
378
  font-weight: 500;
379
+ line-height: 1.4;
332
380
  color: var(--text);
333
- margin-bottom: 4px;
334
- line-height: 1.3;
381
+ flex: 1;
335
382
  }
336
383
 
337
- .task-desc {
384
+ .card-desc {
338
385
  font-size: 11px;
339
386
  color: var(--muted);
340
- line-height: 1.4;
341
- white-space: nowrap;
387
+ line-height: 1.5;
388
+ margin-bottom: 10px;
389
+ display: -webkit-box;
390
+ -webkit-line-clamp: 2;
391
+ -webkit-box-orient: vertical;
342
392
  overflow: hidden;
343
- text-overflow: ellipsis;
344
393
  }
345
394
 
346
- .task-meta {
395
+ .card-footer {
347
396
  display: flex;
348
397
  align-items: center;
349
- gap: 6px;
350
- margin-top: 6px;
398
+ gap: 5px;
399
+ flex-wrap: wrap;
351
400
  }
352
401
 
353
- .badge {
354
- padding: 2px 6px;
355
- border-radius: 3px;
356
- font-family: var(--font-mono);
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;
357
423
  font-size: 9px;
358
- font-weight: 600;
424
+ font-family: var(--mono);
425
+ color: var(--dim);
359
426
  text-transform: uppercase;
360
- letter-spacing: 0.05em;
427
+ letter-spacing: 0.08em;
428
+ max-width: 80px;
429
+ white-space: nowrap;
430
+ overflow: hidden;
431
+ text-overflow: ellipsis;
361
432
  }
362
433
 
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); }
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
+ }
371
442
 
372
443
  /* ── SIDEBAR ── */
373
444
  .sidebar {
445
+ width: 320px;
446
+ flex-shrink: 0;
447
+ background: var(--bg);
374
448
  border-left: 1px solid var(--border);
375
449
  display: flex;
376
450
  flex-direction: column;
@@ -383,351 +457,500 @@
383
457
  flex-shrink: 0;
384
458
  }
385
459
 
386
- .tab {
460
+ .stab {
387
461
  flex: 1;
388
- padding: 10px;
389
- font-family: var(--font-mono);
390
- font-size: 10px;
462
+ padding: 12px;
463
+ font-size: 11px;
464
+ font-weight: 600;
391
465
  text-transform: uppercase;
392
466
  letter-spacing: 0.1em;
393
467
  color: var(--muted);
394
468
  cursor: pointer;
395
- text-align: center;
469
+ background: none;
470
+ border: none;
396
471
  border-bottom: 2px solid transparent;
397
472
  transition: all 0.15s;
398
- background: none;
399
- border-top: none;
400
- border-left: none;
401
- border-right: none;
473
+ font-family: var(--mono);
402
474
  }
403
475
 
404
- .tab.active {
405
- color: var(--accent);
406
- border-bottom-color: var(--accent);
407
- }
476
+ .stab.active { color: var(--accent); border-bottom-color: var(--accent); }
477
+ .stab:hover:not(.active) { color: var(--text); }
408
478
 
409
- .sidebar-content {
479
+ .sidebar-body {
410
480
  flex: 1;
411
481
  overflow-y: auto;
412
482
  padding: 12px;
413
483
  }
414
484
 
415
- .sidebar-content::-webkit-scrollbar { width: 3px; }
416
- .sidebar-content::-webkit-scrollbar-thumb { background: var(--border-bright); }
485
+ .sidebar-body::-webkit-scrollbar { width: 3px; }
486
+ .sidebar-body::-webkit-scrollbar-thumb { background: var(--border); }
417
487
 
418
- /* ── ACTIVITY LOG ── */
488
+ /* ── LOG ENTRIES ── */
419
489
  .log-entry {
420
490
  display: flex;
421
491
  gap: 8px;
422
492
  padding: 8px 0;
423
- border-bottom: 1px solid var(--border);
424
- font-family: var(--font-mono);
493
+ border-bottom: 1px solid rgba(50,54,84,0.5);
494
+ font-family: var(--mono);
425
495
  font-size: 10px;
426
- line-height: 1.4;
496
+ line-height: 1.5;
427
497
  }
428
498
 
429
499
  .log-entry:last-child { border-bottom: none; }
430
-
431
- .log-time { color: var(--muted); flex-shrink: 0; }
432
-
433
- .log-msg { color: var(--text); }
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; }
434
503
  .log-msg.start { color: var(--accent); }
435
504
  .log-msg.complete { color: var(--green); }
436
505
  .log-msg.error { color: var(--red); }
437
- .log-msg.progress { color: var(--text); }
438
506
 
439
- .log-type-icon { flex-shrink: 0; }
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
+ }
440
516
 
441
- /* ── ADD TASK PANEL ── */
442
- .add-panel {
443
- padding: 12px;
444
- border-bottom: 1px solid var(--border);
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);
445
553
  display: none;
554
+ align-items: center;
555
+ justify-content: center;
556
+ z-index: 100;
446
557
  }
447
558
 
448
- .add-panel.open { display: block; }
559
+ .overlay.open { display: flex; }
449
560
 
450
- .field {
451
- margin-bottom: 10px;
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);
452
569
  }
453
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
+
454
580
  .field label {
455
581
  display: block;
456
- font-family: var(--font-mono);
457
- font-size: 10px;
582
+ font-size: 11px;
583
+ font-weight: 600;
458
584
  color: var(--muted);
459
585
  text-transform: uppercase;
460
- letter-spacing: 0.1em;
461
- margin-bottom: 4px;
586
+ letter-spacing: 0.08em;
587
+ margin-bottom: 5px;
588
+ font-family: var(--mono);
462
589
  }
463
590
 
464
591
  .field input, .field textarea, .field select {
465
592
  width: 100%;
466
- background: var(--bg);
467
- border: 1px solid var(--border-bright);
468
- border-radius: 4px;
469
- padding: 7px 10px;
593
+ background: var(--col-bg);
594
+ border: 1px solid var(--border);
595
+ border-radius: 6px;
596
+ padding: 8px 12px;
470
597
  color: var(--text);
471
- font-family: var(--font-mono);
472
- font-size: 11px;
598
+ font-family: var(--font);
599
+ font-size: 13px;
473
600
  outline: none;
474
601
  resize: vertical;
602
+ transition: border-color 0.15s;
475
603
  }
476
604
 
477
605
  .field input:focus, .field textarea:focus, .field select:focus {
478
- border-color: rgba(0,212,255,0.4);
606
+ border-color: var(--accent);
607
+ box-shadow: 0 0 0 3px rgba(108,138,255,0.1);
479
608
  }
480
609
 
481
- .field select option { background: var(--surface); }
610
+ .field select option { background: var(--bg); }
482
611
 
483
- .btn-submit {
484
- width: 100%;
485
- padding: 8px;
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;
486
631
  background: var(--accent);
487
632
  border: none;
488
- border-radius: 4px;
489
- color: #000;
490
- font-family: var(--font-mono);
491
- font-size: 11px;
633
+ border-radius: 6px;
634
+ color: #fff;
635
+ font-size: 13px;
492
636
  font-weight: 600;
493
637
  cursor: pointer;
638
+ font-family: var(--font);
494
639
  transition: opacity 0.15s;
495
640
  }
641
+ .btn-create:hover { opacity: 0.85; }
496
642
 
497
- .btn-submit:hover { opacity: 0.85; }
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); }
498
654
 
499
- /* ── TASK DETAIL ── */
500
- .task-detail {
501
- padding: 0;
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;
502
663
  }
503
664
 
504
- .detail-title {
505
- font-size: 13px;
665
+ .expo-panel-title {
666
+ font-family: var(--mono);
667
+ font-size: 12px;
506
668
  font-weight: 600;
507
669
  color: var(--text);
508
- margin-bottom: 8px;
509
- line-height: 1.4;
510
670
  }
511
671
 
512
- .detail-desc {
672
+ .expo-logs {
673
+ height: 160px;
674
+ overflow-y: auto;
675
+ padding: 10px 16px;
676
+ font-family: var(--mono);
513
677
  font-size: 11px;
514
678
  color: var(--muted);
515
679
  line-height: 1.6;
516
- margin-bottom: 12px;
517
- font-family: var(--font-mono);
518
680
  }
519
681
 
520
- .detail-logs {
521
- margin-top: 12px;
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);
522
691
  }
523
692
 
524
- .detail-logs-title {
525
- font-family: var(--font-mono);
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);
526
704
  font-size: 10px;
527
- color: var(--muted);
705
+ font-weight: 700;
528
706
  text-transform: uppercase;
529
- letter-spacing: 0.1em;
530
- margin-bottom: 8px;
531
707
  }
532
708
 
533
- /* ── MODAL ── */
534
- .modal-backdrop {
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 {
535
717
  position: fixed;
536
- inset: 0;
537
- background: rgba(0,0,0,0.7);
718
+ bottom: 0; left: 0; right: 320px;
719
+ height: 320px;
720
+ background: #0d0f1a;
721
+ border-top: 2px solid var(--border);
722
+ z-index: 19;
538
723
  display: flex;
539
- align-items: center;
540
- justify-content: center;
541
- z-index: 100;
542
- display: none;
724
+ flex-direction: column;
725
+ transform: translateY(100%);
726
+ transition: transform 0.25s ease;
543
727
  }
544
728
 
545
- .modal-backdrop.open { display: flex; }
729
+ .term-panel.open { transform: translateY(0); }
546
730
 
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;
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;
554
739
  }
555
740
 
556
- .modal-title {
557
- font-family: var(--font-mono);
558
- font-size: 12px;
559
- color: var(--accent);
741
+ .term-title {
742
+ font-family: var(--mono);
743
+ font-size: 11px;
744
+ color: var(--muted);
560
745
  text-transform: uppercase;
561
746
  letter-spacing: 0.1em;
562
- margin-bottom: 20px;
563
747
  }
564
748
 
565
- .modal-actions {
566
- display: flex;
567
- gap: 8px;
568
- justify-content: flex-end;
569
- margin-top: 16px;
749
+ #terminal {
750
+ flex: 1;
751
+ overflow: hidden;
752
+ padding: 4px;
570
753
  }
571
754
 
572
- .btn-cancel {
573
- padding: 7px 14px;
755
+ .btn-term-close {
756
+ margin-left: auto;
574
757
  background: none;
575
- border: 1px solid var(--border-bright);
576
- border-radius: 4px;
758
+ border: none;
577
759
  color: var(--muted);
578
- font-family: var(--font-mono);
579
- font-size: 11px;
580
760
  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);
761
+ font-size: 16px;
762
+ line-height: 1;
598
763
  padding: 2px 6px;
599
- border-radius: 3px;
600
764
  }
765
+ .btn-term-close:hover { color: var(--text); }
601
766
 
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;
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);
607
774
  display: flex;
608
775
  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;
776
+ padding: 0 16px;
777
+ gap: 8px;
778
+ z-index: 15;
615
779
  }
616
780
 
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;
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;
625
795
  }
626
796
 
627
- @keyframes rotate { to { transform: rotate(360deg); } }
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); }
628
800
 
629
- .fade-in {
630
- animation: fadeIn 0.2s ease;
801
+ /* adjust board to not overlap toolbar */
802
+ .board-wrap { padding-bottom: 40px; }
803
+
804
+ /* ── RETRY BUTTON on failed cards ── */
805
+ .card-retry-btn {
806
+ margin-top: 10px;
807
+ width: 100%;
808
+ padding: 6px;
809
+ background: rgba(248,113,113,0.1);
810
+ border: 1px solid rgba(248,113,113,0.25);
811
+ border-radius: 6px;
812
+ color: var(--red);
813
+ font-size: 11px;
814
+ font-weight: 600;
815
+ font-family: var(--mono);
816
+ cursor: pointer;
817
+ transition: all 0.15s;
818
+ text-align: center;
819
+ }
820
+ .card-retry-btn:hover {
821
+ background: rgba(248,113,113,0.2);
822
+ border-color: rgba(248,113,113,0.5);
631
823
  }
632
824
 
633
- @keyframes fadeIn {
634
- from { opacity: 0; transform: translateY(4px); }
635
- to { opacity: 1; transform: translateY(0); }
825
+ /* Empty column state */
826
+ .col-empty {
827
+ text-align: center;
828
+ padding: 24px 12px;
829
+ font-family: var(--mono);
830
+ font-size: 10px;
831
+ color: var(--dim);
832
+ line-height: 2;
636
833
  }
834
+
835
+ .fade-in { animation: fadeIn 0.2s ease; }
836
+ @keyframes fadeIn { from { opacity:0; transform:translateY(4px); } to { opacity:1; transform:translateY(0); } }
637
837
  </style>
638
838
  </head>
639
839
  <body>
640
840
 
641
841
  <!-- HEADER -->
642
- <header>
842
+ <div class="header">
643
843
  <div class="logo">
644
- <div class="logo-dot"></div>
645
- CLAUDEBOARD
844
+ <div class="logo-pulse"></div>
845
+ ClaudeBoard
646
846
  </div>
847
+ <div class="header-sep"></div>
848
+ <div class="project-badge" id="projectName">—</div>
849
+
850
+ <div class="header-stats">
851
+ <div class="hstat todo"><div class="hstat-dot"></div><span id="statTodo">0</span> todo</div>
852
+ <div class="hstat prog"><div class="hstat-dot"></div><span id="statProg">0</span> running</div>
853
+ <div class="hstat done"><div class="hstat-dot"></div><span id="statDone">0</span> done</div>
854
+ <div class="hstat err"><div class="hstat-dot"></div><span id="statErr">0</span> failed</div>
855
+ </div>
856
+
647
857
  <div class="header-right">
648
- <span class="project-name" id="projectName">—</span>
649
- <div class="ws-status">
858
+ <div class="progress-wrap">
859
+ <div class="progress-fill" id="progressFill" style="width:0%"></div>
860
+ </div>
861
+ <div class="progress-pct" id="progressPct">0%</div>
862
+ <div class="ws-badge">
650
863
  <div class="ws-dot" id="wsDot"></div>
651
864
  <span id="wsLabel">connecting</span>
652
865
  </div>
866
+ <button class="btn btn-primary" onclick="openModal()">+ Add Task</button>
653
867
  </div>
654
- </header>
868
+ </div>
655
869
 
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>
870
+ <!-- RUNNING BAR -->
871
+ <div class="running-bar" id="runningBar">
872
+ <div class="running-spinner"></div>
873
+ <span class="running-label">Agent working →</span>
874
+ <span class="running-title" id="runningTitle">—</span>
677
875
  </div>
678
876
 
679
877
  <!-- MAIN -->
680
878
  <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>
879
+
880
+ <!-- KANBAN BOARD -->
881
+ <div class="board" id="board">
882
+
883
+ <!-- TODO -->
884
+ <div class="column col-todo" id="col-todo">
885
+ <div class="column-header">
886
+ <div class="column-dot"></div>
887
+ <span class="column-title">To Do</span>
888
+ <span class="column-count" id="cnt-todo">0</span>
889
+ </div>
890
+ <div class="column-body" id="body-todo" ondragover="onDragOver(event,'todo')" ondrop="onDrop(event,'todo')" ondragleave="onDragLeave(event)"></div>
891
+ </div>
892
+
893
+ <!-- IN PROGRESS -->
894
+ <div class="column col-prog" id="col-prog">
895
+ <div class="column-header">
896
+ <div class="column-dot"></div>
897
+ <span class="column-title">In Progress</span>
898
+ <span class="column-count" id="cnt-prog">0</span>
899
+ </div>
900
+ <div class="column-body" id="body-prog" ondragover="onDragOver(event,'in_progress')" ondrop="onDrop(event,'in_progress')" ondragleave="onDragLeave(event)"></div>
686
901
  </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.
902
+
903
+ <!-- DONE -->
904
+ <div class="column col-done" id="col-done">
905
+ <div class="column-header">
906
+ <div class="column-dot"></div>
907
+ <span class="column-title">Done</span>
908
+ <span class="column-count" id="cnt-done">0</span>
909
+ </div>
910
+ <div class="column-body" id="body-done" ondragover="onDragOver(event,'done')" ondrop="onDrop(event,'done')" ondragleave="onDragLeave(event)"></div>
911
+ </div>
912
+
913
+ <!-- ERROR -->
914
+ <div class="column col-err" id="col-err">
915
+ <div class="column-header">
916
+ <div class="column-dot"></div>
917
+ <span class="column-title">Failed</span>
918
+ <span class="column-count" id="cnt-err">0</span>
692
919
  </div>
920
+ <div class="column-body" id="body-err" ondragover="onDragOver(event,'error')" ondrop="onDrop(event,'error')" ondragleave="onDragLeave(event)"></div>
693
921
  </div>
922
+
694
923
  </div>
695
924
 
696
925
  <!-- SIDEBAR -->
697
926
  <div class="sidebar">
698
927
  <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>
928
+ <button class="stab active" id="tab-activity" onclick="switchTab('activity')">Activity</button>
929
+ <button class="stab" id="tab-detail" onclick="switchTab('detail')">Detail</button>
701
930
  </div>
702
- <div class="sidebar-content" id="sidebarContent">
703
- <div class="empty" style="padding:32px 12px">Waiting for activity...</div>
931
+ <div class="sidebar-body" id="sidebarBody">
932
+ <div class="detail-empty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>
704
933
  </div>
705
934
  </div>
706
- </div>
707
935
 
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
936
  </div>
714
937
 
715
938
  <!-- ADD TASK MODAL -->
716
- <div class="modal-backdrop" id="modal">
939
+ <div class="overlay" id="modal" onclick="if(event.target===this)closeModal()">
717
940
  <div class="modal">
718
- <div class="modal-title">// Add Task</div>
941
+ <div class="modal-title">Add Task</div>
719
942
  <div class="field">
720
943
  <label>Title</label>
721
- <input type="text" id="newTitle" placeholder="Implement authentication screen">
944
+ <input type="text" id="f-title" placeholder="Implement login screen...">
722
945
  </div>
723
946
  <div class="field">
724
947
  <label>Description</label>
725
- <textarea id="newDesc" rows="3" placeholder="What needs to be done..."></textarea>
948
+ <textarea id="f-desc" rows="3" placeholder="Detailed description of what needs to be done..."></textarea>
726
949
  </div>
727
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
950
+ <div class="modal-grid">
728
951
  <div class="field">
729
952
  <label>Priority</label>
730
- <select id="newPriority">
953
+ <select id="f-priority">
731
954
  <option value="high">High</option>
732
955
  <option value="medium" selected>Medium</option>
733
956
  <option value="low">Low</option>
@@ -735,209 +958,382 @@
735
958
  </div>
736
959
  <div class="field">
737
960
  <label>Type</label>
738
- <select id="newType">
961
+ <select id="f-type">
739
962
  <option value="feature" selected>Feature</option>
740
963
  <option value="bug">Bug</option>
964
+ <option value="config">Config</option>
741
965
  <option value="refactor">Refactor</option>
742
966
  <option value="test">Test</option>
743
- <option value="config">Config</option>
744
967
  </select>
745
968
  </div>
746
969
  </div>
747
970
  <div class="modal-actions">
748
971
  <button class="btn-cancel" onclick="closeModal()">Cancel</button>
749
- <button class="btn-submit" onclick="submitTask()">Add Task</button>
972
+ <button class="btn-create" onclick="submitTask()">Create Task</button>
750
973
  </div>
751
974
  </div>
752
975
  </div>
753
976
 
977
+ <!-- RETRY / EDIT MODAL -->
978
+ <div class="overlay" id="retryModal" onclick="if(event.target===this)closeRetry()">
979
+ <div class="modal">
980
+ <div class="modal-title" style="display:flex;align-items:center;gap:10px">
981
+ <span style="color:var(--red)">✕</span>
982
+ <span>Edit & Retry Failed Task</span>
983
+ </div>
984
+
985
+ <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">
986
+ No error log found.
987
+ </div>
988
+
989
+ <div class="field">
990
+ <label>Title</label>
991
+ <input type="text" id="r-title">
992
+ </div>
993
+ <div class="field">
994
+ <label>Description</label>
995
+ <textarea id="r-desc" rows="4"></textarea>
996
+ </div>
997
+ <div class="field">
998
+ <label style="color:var(--accent)">💬 Note for the agent (hint to fix the issue)</label>
999
+ <textarea id="r-note" rows="3" placeholder="e.g. Use tailwind v3 not v4. The error is about missing module X. Try a simpler approach without..."></textarea>
1000
+ </div>
1001
+ <div class="modal-grid">
1002
+ <div class="field">
1003
+ <label>Priority</label>
1004
+ <select id="r-priority">
1005
+ <option value="high">High</option>
1006
+ <option value="medium">Medium</option>
1007
+ <option value="low">Low</option>
1008
+ </select>
1009
+ </div>
1010
+ <div class="field">
1011
+ <label>Type</label>
1012
+ <select id="r-type">
1013
+ <option value="feature">Feature</option>
1014
+ <option value="bug">Bug</option>
1015
+ <option value="config">Config</option>
1016
+ <option value="refactor">Refactor</option>
1017
+ <option value="test">Test</option>
1018
+ </select>
1019
+ </div>
1020
+ </div>
1021
+ <div class="modal-actions">
1022
+ <button class="btn-cancel" onclick="closeRetry()">Cancel</button>
1023
+ <button class="btn-create" style="background:var(--red)" onclick="submitRetry()">↩ Retry Task</button>
1024
+ </div>
1025
+ </div>
1026
+ </div>
1027
+
1028
+ <!-- BOTTOM TOOLBAR -->
1029
+ <div class="bottom-toolbar">
1030
+ <button class="toolbar-btn" id="expoBtn" onclick="toggleExpoPanel()">
1031
+ 📱 Expo
1032
+ <span class="expo-status-badge stopped" id="expoBadge">stopped</span>
1033
+ </button>
1034
+ <button class="toolbar-btn" id="termBtn" onclick="toggleTerminal()">
1035
+ ⌨️ Terminal
1036
+ </button>
1037
+ </div>
1038
+
1039
+ <!-- EXPO PANEL -->
1040
+ <div class="expo-panel" id="expoPanel">
1041
+ <div class="expo-panel-header" onclick="toggleExpoPanel()">
1042
+ <span class="expo-panel-title">📱 Expo Go</span>
1043
+ <span class="expo-status-badge stopped" id="expoPanelBadge">stopped</span>
1044
+ <div style="margin-left:auto;display:flex;gap:8px">
1045
+ <button class="btn btn-primary" id="expoStartBtn" onclick="event.stopPropagation();startExpo()" style="font-size:11px;padding:4px 12px">Start Expo</button>
1046
+ <button class="btn btn-ghost" id="expoStopBtn" onclick="event.stopPropagation();stopExpo()" style="font-size:11px;padding:4px 12px;display:none">Stop</button>
1047
+ </div>
1048
+ </div>
1049
+ <div style="display:flex;gap:0">
1050
+ <div style="flex:1">
1051
+ <div class="expo-logs" id="expoLogs">Expo not started. Click "Start Expo" to install dependencies and launch with tunnel.</div>
1052
+ <div class="expo-qr-wrap" id="expoUrlWrap" style="display:none">
1053
+ <div>
1054
+ <div style="font-family:var(--mono);font-size:10px;color:var(--muted);margin-bottom:4px">SCAN WITH EXPO GO</div>
1055
+ <div class="expo-url" id="expoUrl">—</div>
1056
+ </div>
1057
+ </div>
1058
+ </div>
1059
+ <div id="qrWrap" style="padding:12px;display:none">
1060
+ <canvas id="qrCanvas" width="120" height="120"></canvas>
1061
+ </div>
1062
+ </div>
1063
+ </div>
1064
+
1065
+ <!-- TERMINAL PANEL -->
1066
+ <div class="term-panel" id="termPanel">
1067
+ <div class="term-header">
1068
+ <span style="color:var(--green);font-size:14px">⬤</span>
1069
+ <span class="term-title">Terminal — <span style="color:var(--accent)" id="termDir">project</span></span>
1070
+ <button class="btn-term-close" onclick="toggleTerminal()">✕</button>
1071
+ </div>
1072
+ <div id="terminal"></div>
1073
+ </div>
1074
+
1075
+ <!-- xterm.js -->
1076
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.3.0/xterm.min.css">
1077
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.3.0/xterm.min.js"></script>
1078
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.3.0/addon-fit.min.js"></script>
1079
+
1080
+ <!-- QR code via qrcodejs -->
1081
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
1082
+
754
1083
  <script>
755
- const API = '';
756
- let ws;
1084
+ // ── STATE ────────────────────────────────────────────────────────────────────
757
1085
  let board = { epics: [], logs: [] };
758
- let activeTab = 'log';
1086
+ let activeTab = 'activity';
759
1087
  let selectedTask = null;
1088
+ let draggedId = null;
1089
+ let ws;
760
1090
 
761
- // ── WS ────────────────────────────────────────────────────────────────────────
1091
+ // ── WEBSOCKET ────────────────────────────────────────────────────────────────
762
1092
  function connectWS() {
763
1093
  const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
764
1094
  ws = new WebSocket(`${proto}//${location.host}`);
765
-
766
1095
  ws.onopen = () => setWS(true);
767
1096
  ws.onclose = () => { setWS(false); setTimeout(connectWS, 2000); };
768
-
769
1097
  ws.onmessage = (e) => {
770
1098
  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') {
1099
+ if (['task_update','task_added','task_started','task_complete','task_failed'].includes(event)) {
773
1100
  loadBoard();
774
1101
  }
775
1102
  if (event === 'log') {
776
1103
  board.logs.unshift(data);
777
- if (activeTab === 'log') renderLogs();
1104
+ if (activeTab === 'activity') renderLogs();
1105
+ }
1106
+ if (event === 'expo_status') {
1107
+ setExpoStatus(data.status, data.url);
1108
+ }
1109
+ if (event === 'expo_log') {
1110
+ appendExpoLog(data.message);
778
1111
  }
779
1112
  };
780
1113
  }
781
1114
 
782
1115
  function setWS(on) {
783
- document.getElementById('wsDot').className = 'ws-dot' + (on ? ' connected' : '');
1116
+ document.getElementById('wsDot').className = 'ws-dot' + (on ? ' on' : '');
784
1117
  document.getElementById('wsLabel').textContent = on ? 'live' : 'reconnecting';
785
1118
  }
786
1119
 
787
- // ── DATA ──────────────────────────────────────────────────────────────────────
1120
+ // ── DATA ─────────────────────────────────────────────────────────────────────
788
1121
  async function loadBoard() {
789
1122
  const res = await fetch('/api/board');
790
1123
  const data = await res.json();
791
1124
  board = data;
792
1125
  document.getElementById('projectName').textContent = data.project || '—';
793
- renderBoard();
1126
+ renderKanban();
794
1127
  updateStats();
795
- if (activeTab === 'log') renderLogs();
1128
+ if (activeTab === 'activity') renderLogs();
1129
+ }
1130
+
1131
+ function allTasks() {
1132
+ return board.epics?.flatMap(e => (e.cb_tasks || []).map(t => ({ ...t, epicName: e.name }))) || [];
796
1133
  }
797
1134
 
798
1135
  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;
1136
+ const tasks = allTasks();
1137
+ const todo = tasks.filter(t => t.status === 'todo').length;
1138
+ const prog = tasks.filter(t => t.status === 'in_progress').length;
1139
+ const done = tasks.filter(t => t.status === 'done').length;
1140
+ const err = tasks.filter(t => t.status === 'error').length;
1141
+ const total = tasks.length;
805
1142
 
806
1143
  document.getElementById('statTodo').textContent = todo;
807
- document.getElementById('statProgress').textContent = progress;
1144
+ document.getElementById('statProg').textContent = prog;
808
1145
  document.getElementById('statDone').textContent = done;
809
- document.getElementById('statError').textContent = error;
1146
+ document.getElementById('statErr').textContent = err;
810
1147
 
811
1148
  const pct = total > 0 ? Math.round((done / total) * 100) : 0;
812
- document.getElementById('progressBar').style.width = pct + '%';
1149
+ document.getElementById('progressFill').style.width = pct + '%';
1150
+ document.getElementById('progressPct').textContent = pct + '%';
813
1151
 
814
- // Current task bar
815
- const running = allTasks.find(t => t.status === 'in_progress');
816
- const bar = document.getElementById('currentTaskBar');
1152
+ // Running bar
1153
+ const running = tasks.find(t => t.status === 'in_progress');
1154
+ const bar = document.getElementById('runningBar');
817
1155
  if (running) {
818
- bar.style.display = 'flex';
819
- document.getElementById('currentTaskTitle').textContent = running.title;
1156
+ bar.className = 'running-bar visible';
1157
+ document.getElementById('runningTitle').textContent = running.title;
820
1158
  } else {
821
- bar.style.display = 'none';
1159
+ bar.className = 'running-bar';
822
1160
  }
823
1161
  }
824
1162
 
825
- // ── RENDER BOARD ──────────────────────────────────────────────────────────────
826
- function renderBoard() {
827
- const el = document.getElementById('boardContent');
1163
+ // ── KANBAN RENDER ─────────────────────────────────────────────────────────────
1164
+ function renderKanban() {
1165
+ const tasks = allTasks();
1166
+ const groups = {
1167
+ todo: tasks.filter(t => t.status === 'todo'),
1168
+ in_progress: tasks.filter(t => t.status === 'in_progress'),
1169
+ done: tasks.filter(t => t.status === 'done'),
1170
+ error: tasks.filter(t => t.status === 'error'),
1171
+ };
828
1172
 
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
- }
1173
+ const map = { todo: 'todo', in_progress: 'prog', done: 'done', error: 'err' };
833
1174
 
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('');
1175
+ for (const [status, colKey] of Object.entries(map)) {
1176
+ const body = document.getElementById('body-' + colKey);
1177
+ const cnt = document.getElementById('cnt-' + colKey);
1178
+ const list = groups[status] || [];
1179
+ cnt.textContent = list.length;
1180
+
1181
+ if (list.length === 0) {
1182
+ body.innerHTML = `<div class="col-empty">No tasks here</div>`;
1183
+ continue;
1184
+ }
1185
+
1186
+ body.innerHTML = list.map(t => cardHTML(t)).join('');
1187
+
1188
+ // Attach drag events
1189
+ body.querySelectorAll('.card').forEach(card => {
1190
+ card.addEventListener('dragstart', e => onDragStart(e, card.dataset.id));
1191
+ card.addEventListener('dragend', () => onDragEnd(card));
1192
+ card.addEventListener('click', () => selectCard(card.dataset.id));
1193
+ });
1194
+ }
848
1195
  }
849
1196
 
850
- function renderTask(task) {
851
- const icons = { todo: '', in_progress: '', done: '✓', error: '✕', blocked: '—' };
1197
+ function cardHTML(task) {
1198
+ const icons = { todo: '', in_progress: '', done: '✓', error: '✕', blocked: '—' };
1199
+ const shortEpic = (task.epicName || '').split(' ').slice(0,2).join(' ');
1200
+ const isError = task.status === 'error';
852
1201
  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>
1202
+ <div class="card fade-in" draggable="true" data-id="${task.id}" data-status="${task.status}">
1203
+ <div class="card-top">
1204
+ <div class="card-status ${task.status}">${icons[task.status] || ''}</div>
1205
+ <div class="card-title">${esc(task.title)}</div>
862
1206
  </div>
863
- </div>
864
- `;
1207
+ ${task.description ? `<div class="card-desc">${esc(task.description.split('\n')[0])}</div>` : ''}
1208
+ <div class="card-footer">
1209
+ <span class="tag ${task.priority}">${task.priority}</span>
1210
+ <span class="tag ${task.type}">${task.type}</span>
1211
+ ${shortEpic ? `<span class="card-epic">${esc(shortEpic)}</span>` : ''}
1212
+ </div>
1213
+ ${isError ? `<button class="card-retry-btn" onclick="event.stopPropagation();openRetry('${task.id}')">↩ Edit & Retry</button>` : ''}
1214
+ </div>`;
865
1215
  }
866
1216
 
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;
1217
+ // ── DRAG & DROP ───────────────────────────────────────────────────────────────
1218
+ function onDragStart(e, id) {
1219
+ draggedId = id;
1220
+ setTimeout(() => {
1221
+ const el = document.querySelector(`.card[data-id="${id}"]`);
1222
+ if (el) el.classList.add('dragging');
1223
+ }, 0);
1224
+ e.dataTransfer.effectAllowed = 'move';
1225
+ }
1226
+
1227
+ function onDragEnd(card) {
1228
+ card.classList.remove('dragging');
1229
+ document.querySelectorAll('.drop-placeholder').forEach(p => p.remove());
1230
+ }
1231
+
1232
+ function onDragOver(e, status) {
1233
+ e.preventDefault();
1234
+ e.dataTransfer.dropEffect = 'move';
1235
+ const col = e.currentTarget;
1236
+ if (!col.querySelector('.drop-placeholder')) {
1237
+ const ph = document.createElement('div');
1238
+ ph.className = 'drop-placeholder';
1239
+ col.appendChild(ph);
873
1240
  }
1241
+ }
874
1242
 
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('');
1243
+ function onDragLeave(e) {
1244
+ const col = e.currentTarget;
1245
+ const related = e.relatedTarget;
1246
+ if (!col.contains(related)) {
1247
+ col.querySelectorAll('.drop-placeholder').forEach(p => p.remove());
1248
+ }
886
1249
  }
887
1250
 
888
- function logColor(type) {
889
- const c = { start: '#00d4ff', complete: '#00ff88', error: '#ff4466', progress: '#5a5a70', info: '#5a5a70' };
890
- return c[type] || '#5a5a70';
1251
+ async function onDrop(e, status) {
1252
+ e.preventDefault();
1253
+ document.querySelectorAll('.drop-placeholder').forEach(p => p.remove());
1254
+ if (!draggedId) return;
1255
+
1256
+ await fetch(`/api/tasks/${draggedId}`, {
1257
+ method: 'PATCH',
1258
+ headers: { 'Content-Type': 'application/json' },
1259
+ body: JSON.stringify({ status }),
1260
+ });
1261
+
1262
+ draggedId = null;
1263
+ loadBoard();
891
1264
  }
892
1265
 
893
- // ── TASK DETAIL ───────────────────────────────────────────────────────────────
894
- async function selectTask(id) {
1266
+ // ── CARD SELECT ───────────────────────────────────────────────────────────────
1267
+ async function selectCard(id) {
895
1268
  activeTab = 'detail';
896
- document.getElementById('tabLog').className = 'tab';
897
- document.getElementById('tabDetail').className = 'tab active';
1269
+ document.getElementById('tab-activity').className = 'stab';
1270
+ document.getElementById('tab-detail').className = 'stab active';
898
1271
 
899
- const allTasks = board.epics.flatMap(e => e.cb_tasks || []);
900
- selectedTask = allTasks.find(t => t.id === id);
1272
+ const task = allTasks().find(t => t.id === id);
1273
+ if (!task) return;
1274
+ selectedTask = task;
901
1275
 
902
- const logsRes = await fetch(`/api/tasks/${id}/logs`);
903
- const { logs } = await logsRes.json();
1276
+ const res = await fetch(`/api/tasks/${id}/logs`);
1277
+ const { logs } = await res.json();
904
1278
 
905
- const el = document.getElementById('sidebarContent');
1279
+ const el = document.getElementById('sidebarBody');
906
1280
  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('')}
1281
+ <div>
1282
+ <div class="detail-title">${esc(task.title)}</div>
1283
+ <div class="detail-tags">
1284
+ <span class="tag ${task.status}">${task.status.replace('_',' ')}</span>
1285
+ <span class="tag ${task.priority}">${task.priority}</span>
1286
+ <span class="tag ${task.type}">${task.type}</span>
922
1287
  </div>
923
- </div>
924
- `;
1288
+ ${task.description ? `<div class="detail-desc">${esc(task.description)}</div>` : ''}
1289
+ <div class="detail-logs-title">// agent logs</div>
1290
+ ${logs.length === 0
1291
+ ? '<div style="font-family:var(--mono);font-size:10px;color:var(--dim);padding:8px 0">No logs yet.</div>'
1292
+ : logs.map(l => {
1293
+ const t = new Date(l.created_at).toLocaleTimeString('en',{hour12:false});
1294
+ return `<div class="log-entry"><span class="log-time">${t}</span><span class="log-msg ${l.type}">${esc(l.message)}</span></div>`;
1295
+ }).join('')
1296
+ }
1297
+ </div>`;
1298
+ }
1299
+
1300
+ // ── LOGS ──────────────────────────────────────────────────────────────────────
1301
+ function renderLogs() {
1302
+ const el = document.getElementById('sidebarBody');
1303
+ if (!board.logs?.length) {
1304
+ el.innerHTML = `<div class="detail-empty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>`;
1305
+ return;
1306
+ }
1307
+
1308
+ const icons = { start: '▶', complete: '✓', error: '✕', progress: '·', info: '·' };
1309
+ el.innerHTML = board.logs.map(l => {
1310
+ const t = new Date(l.created_at).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
1311
+ return `<div class="log-entry">
1312
+ <span class="log-time">${t}</span>
1313
+ <span class="log-icon" style="color:${logColor(l.type)}">${icons[l.type]||'·'}</span>
1314
+ <span class="log-msg ${l.type}">${esc(l.message)}</span>
1315
+ </div>`;
1316
+ }).join('');
1317
+ }
1318
+
1319
+ function logColor(type) {
1320
+ return { start:'#6c8aff', complete:'#4ade80', error:'#f87171', progress:'#6b7094', info:'#6b7094' }[type] || '#6b7094';
925
1321
  }
926
1322
 
927
1323
  // ── TABS ──────────────────────────────────────────────────────────────────────
928
1324
  function switchTab(tab) {
929
1325
  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();
1326
+ document.getElementById('tab-activity').className = 'stab' + (tab === 'activity' ? ' active' : '');
1327
+ document.getElementById('tab-detail').className = 'stab' + (tab === 'detail' ? ' active' : '');
1328
+ if (tab === 'activity') renderLogs();
933
1329
  }
934
1330
 
935
- // ── ADD TASK ──────────────────────────────────────────────────────────────────
936
- function openModal() { document.getElementById('modal').className = 'modal-backdrop open'; }
937
- function closeModal() { document.getElementById('modal').className = 'modal-backdrop'; }
1331
+ // ── MODAL ─────────────────────────────────────────────────────────────────────
1332
+ function openModal() { document.getElementById('modal').className = 'overlay open'; }
1333
+ function closeModal() { document.getElementById('modal').className = 'overlay'; }
938
1334
 
939
1335
  async function submitTask() {
940
- const title = document.getElementById('newTitle').value.trim();
1336
+ const title = document.getElementById('f-title').value.trim();
941
1337
  if (!title) return;
942
1338
 
943
1339
  await fetch('/api/tasks', {
@@ -945,39 +1341,260 @@ async function submitTask() {
945
1341
  headers: { 'Content-Type': 'application/json' },
946
1342
  body: JSON.stringify({
947
1343
  title,
948
- description: document.getElementById('newDesc').value.trim(),
949
- priority: document.getElementById('newPriority').value,
950
- type: document.getElementById('newType').value,
951
- })
1344
+ description: document.getElementById('f-desc').value.trim(),
1345
+ priority: document.getElementById('f-priority').value,
1346
+ type: document.getElementById('f-type').value,
1347
+ }),
952
1348
  });
953
1349
 
954
- document.getElementById('newTitle').value = '';
955
- document.getElementById('newDesc').value = '';
1350
+ document.getElementById('f-title').value = '';
1351
+ document.getElementById('f-desc').value = '';
956
1352
  closeModal();
957
1353
  loadBoard();
958
1354
  }
959
1355
 
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;');
1356
+ // ── RETRY MODAL ───────────────────────────────────────────────────────────────
1357
+ let retryTaskId = null;
1358
+
1359
+ async function openRetry(id) {
1360
+ retryTaskId = id;
1361
+ const task = allTasks().find(t => t.id === id);
1362
+ if (!task) return;
1363
+
1364
+ // Fill form with current task data
1365
+ document.getElementById('r-title').value = task.title;
1366
+ document.getElementById('r-desc').value = task.description || '';
1367
+ document.getElementById('r-note').value = '';
1368
+ document.getElementById('r-priority').value = task.priority || 'medium';
1369
+ document.getElementById('r-type').value = task.type || 'feature';
1370
+
1371
+ // Load last error log
1372
+ const res = await fetch(`/api/tasks/${id}/logs`);
1373
+ const { logs } = await res.json();
1374
+ const errorLogs = logs.filter(l => l.type === 'error');
1375
+ const lastError = errorLogs[errorLogs.length - 1];
1376
+ document.getElementById('retryErrorLog').textContent =
1377
+ lastError ? lastError.message : 'No error log found.';
1378
+
1379
+ document.getElementById('retryModal').className = 'overlay open';
964
1380
  }
965
1381
 
966
- // Close modal on backdrop click
967
- document.getElementById('modal').addEventListener('click', (e) => {
968
- if (e.target === e.currentTarget) closeModal();
969
- });
1382
+ function closeRetry() {
1383
+ document.getElementById('retryModal').className = 'overlay';
1384
+ retryTaskId = null;
1385
+ }
1386
+
1387
+ async function submitRetry() {
1388
+ if (!retryTaskId) return;
1389
+
1390
+ const title = document.getElementById('r-title').value.trim();
1391
+ const desc = document.getElementById('r-desc').value.trim();
1392
+ const note = document.getElementById('r-note').value.trim();
1393
+ const priority = document.getElementById('r-priority').value;
1394
+ const type = document.getElementById('r-type').value;
1395
+
1396
+ if (!title) return;
1397
+
1398
+ // Build updated description — append agent note if provided
1399
+ const updatedDesc = note
1400
+ ? `${desc}\n\n⚠️ AGENT NOTE (from human review): ${note}`
1401
+ : desc;
1402
+
1403
+ // Update task fields + reset status to todo
1404
+ await fetch(`/api/tasks/${retryTaskId}`, {
1405
+ method: 'PATCH',
1406
+ headers: { 'Content-Type': 'application/json' },
1407
+ body: JSON.stringify({
1408
+ title,
1409
+ description: updatedDesc,
1410
+ priority,
1411
+ type,
1412
+ status: 'todo',
1413
+ started_at: null,
1414
+ }),
1415
+ });
1416
+
1417
+ // Log the retry
1418
+ await fetch(`/api/tasks/${retryTaskId}/log`, {
1419
+ method: 'POST',
1420
+ headers: { 'Content-Type': 'application/json' },
1421
+ body: JSON.stringify({
1422
+ message: note
1423
+ ? `↩ Retried by human with note: "${note}"`
1424
+ : '↩ Retried by human — reset to todo',
1425
+ }),
1426
+ });
1427
+
1428
+ closeRetry();
1429
+ loadBoard();
1430
+ }
1431
+
1432
+ // ── EXPO ─────────────────────────────────────────────────────────────────────
1433
+ let expoOpen = false;
1434
+ let termOpen = false;
1435
+ let term = null;
1436
+ let termSocket = null;
1437
+ let termFit = null;
1438
+ let qrInstance = null;
1439
+
1440
+ function toggleExpoPanel() {
1441
+ expoOpen = !expoOpen;
1442
+ document.getElementById('expoPanel').className = 'expo-panel' + (expoOpen ? ' open' : '');
1443
+ document.getElementById('expoBtn').className = 'toolbar-btn' + (expoOpen ? ' active' : '');
1444
+ if (termOpen) { termOpen = false; document.getElementById('termPanel').className = 'term-panel'; }
1445
+ }
1446
+
1447
+ async function startExpo() {
1448
+ document.getElementById('expoStartBtn').style.display = 'none';
1449
+ document.getElementById('expoStopBtn').style.display = 'inline-flex';
1450
+ appendExpoLog('▶ Starting Expo...');
1451
+ await fetch('/api/expo/start', { method: 'POST' });
1452
+ }
1453
+
1454
+ async function stopExpo() {
1455
+ await fetch('/api/expo/stop', { method: 'POST' });
1456
+ document.getElementById('expoStartBtn').style.display = 'inline-flex';
1457
+ document.getElementById('expoStopBtn').style.display = 'none';
1458
+ document.getElementById('expoUrlWrap').style.display = 'none';
1459
+ document.getElementById('qrWrap').style.display = 'none';
1460
+ appendExpoLog('■ Expo stopped.');
1461
+ }
1462
+
1463
+ function appendExpoLog(msg) {
1464
+ const el = document.getElementById('expoLogs');
1465
+ const line = document.createElement('div');
1466
+ line.textContent = msg;
1467
+ el.appendChild(line);
1468
+ el.scrollTop = el.scrollHeight;
1469
+ }
1470
+
1471
+ function setExpoStatus(status, url) {
1472
+ const labels = { stopped:'stopped', installing:'installing...', starting:'starting...', running:'running', error:'error' };
1473
+ const label = labels[status] || status;
1474
+
1475
+ ['expoBadge','expoPanelBadge'].forEach(id => {
1476
+ const el = document.getElementById(id);
1477
+ el.className = `expo-status-badge ${status}`;
1478
+ el.textContent = label;
1479
+ });
1480
+
1481
+ document.getElementById('expoBtn').className = 'toolbar-btn' + (status === 'running' ? ' expo-running' : (expoOpen ? ' active' : ''));
1482
+
1483
+ if (status === 'running' && url) {
1484
+ document.getElementById('expoUrl').textContent = url;
1485
+ document.getElementById('expoUrlWrap').style.display = 'flex';
1486
+
1487
+ // Generate QR code
1488
+ document.getElementById('qrWrap').style.display = 'block';
1489
+ const canvas = document.getElementById('qrCanvas');
1490
+ const ctx = canvas.getContext('2d');
1491
+ ctx.clearRect(0, 0, 120, 120);
1492
+
1493
+ if (window.QRCode) {
1494
+ document.getElementById('qrCanvas').innerHTML = '';
1495
+ try {
1496
+ new QRCode(document.getElementById('qrCanvas'), {
1497
+ text: url,
1498
+ width: 120, height: 120,
1499
+ colorDark: '#e2e4f0',
1500
+ colorLight: '#0d0f1a',
1501
+ });
1502
+ } catch {}
1503
+ }
1504
+ }
1505
+
1506
+ if (status === 'stopped' || status === 'error') {
1507
+ document.getElementById('expoStartBtn').style.display = 'inline-flex';
1508
+ document.getElementById('expoStopBtn').style.display = 'none';
1509
+ }
1510
+ }
1511
+
1512
+ // ── TERMINAL ─────────────────────────────────────────────────────────────────
1513
+ function toggleTerminal() {
1514
+ termOpen = !termOpen;
1515
+ document.getElementById('termPanel').className = 'term-panel' + (termOpen ? ' open' : '');
1516
+ document.getElementById('termBtn').className = 'toolbar-btn' + (termOpen ? ' active' : '');
1517
+
1518
+ if (expoOpen) { expoOpen = false; document.getElementById('expoPanel').className = 'expo-panel'; }
1519
+
1520
+ if (termOpen && !term) initTerminal();
1521
+ if (termOpen && termFit) setTimeout(() => termFit.fit(), 100);
1522
+ }
1523
+
1524
+ function initTerminal() {
1525
+ term = new Terminal({
1526
+ theme: {
1527
+ background: '#0d0f1a',
1528
+ foreground: '#e2e4f0',
1529
+ cursor: '#6c8aff',
1530
+ cursorAccent: '#0d0f1a',
1531
+ selection: 'rgba(108,138,255,0.3)',
1532
+ black: '#1e2130', red: '#f87171', green: '#4ade80', yellow: '#fbbf24',
1533
+ blue: '#6c8aff', magenta: '#c084fc', cyan: '#22d3ee', white: '#e2e4f0',
1534
+ brightBlack: '#454868', brightRed: '#fca5a5', brightGreen: '#86efac',
1535
+ brightYellow: '#fde68a', brightBlue: '#93c5fd', brightMagenta: '#d8b4fe',
1536
+ brightCyan: '#67e8f9', brightWhite: '#f1f5f9',
1537
+ },
1538
+ fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
1539
+ fontSize: 13,
1540
+ lineHeight: 1.4,
1541
+ cursorBlink: true,
1542
+ scrollback: 2000,
1543
+ });
1544
+
1545
+ const fitAddon = new FitAddon.FitAddon();
1546
+ termFit = fitAddon;
1547
+ term.loadAddon(fitAddon);
1548
+ term.open(document.getElementById('terminal'));
1549
+ fitAddon.fit();
1550
+
1551
+ // Connect WebSocket
1552
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1553
+ termSocket = new WebSocket(`${proto}//${location.host}/terminal`);
1554
+
1555
+ termSocket.onopen = () => {
1556
+ term.write('\x1b[32m[ClaudeBoard Terminal]\x1b[0m Connected\r\n\r\n');
1557
+ };
1558
+
1559
+ termSocket.onmessage = (e) => {
1560
+ const msg = JSON.parse(e.data);
1561
+ if (msg.type === 'output') term.write(msg.data);
1562
+ if (msg.type === 'exit') term.write('\r\n\x1b[31m[process exited]\x1b[0m\r\n');
1563
+ };
970
1564
 
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(); }
1565
+ termSocket.onclose = () => term.write('\r\n\x1b[33m[disconnected]\x1b[0m\r\n');
1566
+
1567
+ term.onData((data) => {
1568
+ if (termSocket?.readyState === 1) {
1569
+ termSocket.send(JSON.stringify({ type: 'input', data }));
1570
+ }
1571
+ });
1572
+
1573
+ // Resize on window resize
1574
+ window.addEventListener('resize', () => { if (termFit) termFit.fit(); });
1575
+ term.onResize(({ cols, rows }) => {
1576
+ if (termSocket?.readyState === 1) termSocket.send(JSON.stringify({ type: 'resize', cols, rows }));
1577
+ });
1578
+ }
1579
+
1580
+ // ── UTILS ─────────────────────────────────────────────────────────────────────
1581
+ function esc(s) {
1582
+ if (!s) return '';
1583
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1584
+ }
1585
+
1586
+ document.addEventListener('keydown', e => {
1587
+ if (e.key === 'Escape') { closeModal(); closeRetry(); }
1588
+ if ((e.metaKey || e.ctrlKey) && e.key === 'n') { e.preventDefault(); openModal(); }
975
1589
  });
976
1590
 
977
1591
  // ── INIT ──────────────────────────────────────────────────────────────────────
978
1592
  loadBoard();
979
- setInterval(loadBoard, 10000); // Fallback poll every 10s
1593
+ setInterval(loadBoard, 8000);
980
1594
  connectWS();
1595
+
1596
+ // Load expo status
1597
+ fetch('/api/expo/status').then(r => r.json()).then(d => setExpoStatus(d.status, d.url));
981
1598
  </script>
982
1599
  </body>
983
1600
  </html>