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.
- package/agents/board-client.js +27 -0
- package/agents/claude-api.js +24 -7
- package/agents/orchestrator.js +22 -4
- package/bin/cli.js +7 -3
- package/dashboard/index.html +1189 -572
- package/dashboard/server.js +234 -116
- package/package.json +2 -1
package/dashboard/index.html
CHANGED
|
@@ -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
|
|
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: #
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--red
|
|
25
|
-
--purple: #
|
|
26
|
-
--
|
|
27
|
-
--
|
|
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
|
-
|
|
36
|
+
html, body {
|
|
37
|
+
height: 100%;
|
|
38
|
+
background: var(--bg2);
|
|
34
39
|
color: var(--text);
|
|
35
|
-
font-family: var(--font
|
|
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
|
-
|
|
47
|
-
|
|
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:
|
|
58
|
-
font-
|
|
59
|
-
font-
|
|
60
|
-
|
|
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-
|
|
69
|
+
.logo-pulse {
|
|
66
70
|
width: 8px; height: 8px;
|
|
67
71
|
background: var(--accent);
|
|
68
72
|
border-radius: 50%;
|
|
69
|
-
box-shadow: 0 0
|
|
70
|
-
animation: pulse 2s ease-
|
|
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
|
|
75
|
-
|
|
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-
|
|
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-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
.
|
|
96
|
+
.header-stats {
|
|
93
97
|
display: flex;
|
|
94
98
|
align-items: center;
|
|
95
|
-
gap:
|
|
96
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
.
|
|
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:
|
|
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
|
-
.
|
|
133
|
-
|
|
134
|
-
|
|
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:
|
|
138
|
+
border-radius: 3px;
|
|
145
139
|
overflow: hidden;
|
|
146
140
|
}
|
|
147
141
|
|
|
148
|
-
.progress-
|
|
142
|
+
.progress-fill {
|
|
149
143
|
height: 100%;
|
|
150
|
-
background: var(--green);
|
|
151
|
-
border-radius:
|
|
152
|
-
transition: width 0.
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
letter-spacing: 0.15em;
|
|
162
|
+
font-family: var(--mono);
|
|
186
163
|
}
|
|
187
164
|
|
|
188
|
-
.
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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-
|
|
204
|
-
background:
|
|
205
|
-
|
|
181
|
+
.btn-primary {
|
|
182
|
+
background: var(--accent);
|
|
183
|
+
color: #fff;
|
|
206
184
|
}
|
|
185
|
+
.btn-primary:hover { background: #7d9cff; }
|
|
207
186
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
218
|
-
padding: 0 4px;
|
|
203
|
+
flex-shrink: 0;
|
|
219
204
|
}
|
|
220
205
|
|
|
221
|
-
.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
225
|
+
overflow: hidden;
|
|
226
|
+
height: calc(100vh - 56px);
|
|
234
227
|
}
|
|
235
228
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
291
|
+
.column-body {
|
|
292
|
+
flex: 1;
|
|
293
|
+
overflow-y: auto;
|
|
294
|
+
padding: 10px;
|
|
244
295
|
display: flex;
|
|
245
|
-
|
|
246
|
-
gap:
|
|
247
|
-
|
|
248
|
-
|
|
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:
|
|
251
|
-
|
|
252
|
-
cursor:
|
|
253
|
-
transition:
|
|
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
|
-
.
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
.
|
|
278
|
-
border-color: var(--
|
|
279
|
-
|
|
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
|
-
.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
345
|
+
.card-top {
|
|
346
|
+
display: flex;
|
|
347
|
+
align-items: flex-start;
|
|
348
|
+
gap: 8px;
|
|
349
|
+
margin-bottom: 8px;
|
|
287
350
|
}
|
|
288
351
|
|
|
289
|
-
.
|
|
290
|
-
width:
|
|
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:
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
.task-status-icon.todo {
|
|
301
|
-
border: 1.5px solid var(--border-bright);
|
|
360
|
+
font-size: 9px;
|
|
302
361
|
}
|
|
303
362
|
|
|
304
|
-
.
|
|
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:
|
|
307
|
-
animation: spin-
|
|
366
|
+
background: rgba(251,191,36,0.1);
|
|
367
|
+
animation: spin-border 1.2s linear infinite;
|
|
308
368
|
}
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
334
|
-
line-height: 1.3;
|
|
381
|
+
flex: 1;
|
|
335
382
|
}
|
|
336
383
|
|
|
337
|
-
.
|
|
384
|
+
.card-desc {
|
|
338
385
|
font-size: 11px;
|
|
339
386
|
color: var(--muted);
|
|
340
|
-
line-height: 1.
|
|
341
|
-
|
|
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
|
-
.
|
|
395
|
+
.card-footer {
|
|
347
396
|
display: flex;
|
|
348
397
|
align-items: center;
|
|
349
|
-
gap:
|
|
350
|
-
|
|
398
|
+
gap: 5px;
|
|
399
|
+
flex-wrap: wrap;
|
|
351
400
|
}
|
|
352
401
|
|
|
353
|
-
.
|
|
354
|
-
padding: 2px
|
|
355
|
-
border-radius:
|
|
356
|
-
font-
|
|
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-
|
|
424
|
+
font-family: var(--mono);
|
|
425
|
+
color: var(--dim);
|
|
359
426
|
text-transform: uppercase;
|
|
360
|
-
letter-spacing: 0.
|
|
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
|
-
|
|
364
|
-
.
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
.
|
|
460
|
+
.stab {
|
|
387
461
|
flex: 1;
|
|
388
|
-
padding:
|
|
389
|
-
font-
|
|
390
|
-
font-
|
|
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
|
-
|
|
469
|
+
background: none;
|
|
470
|
+
border: none;
|
|
396
471
|
border-bottom: 2px solid transparent;
|
|
397
472
|
transition: all 0.15s;
|
|
398
|
-
|
|
399
|
-
border-top: none;
|
|
400
|
-
border-left: none;
|
|
401
|
-
border-right: none;
|
|
473
|
+
font-family: var(--mono);
|
|
402
474
|
}
|
|
403
475
|
|
|
404
|
-
.
|
|
405
|
-
|
|
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-
|
|
479
|
+
.sidebar-body {
|
|
410
480
|
flex: 1;
|
|
411
481
|
overflow-y: auto;
|
|
412
482
|
padding: 12px;
|
|
413
483
|
}
|
|
414
484
|
|
|
415
|
-
.sidebar-
|
|
416
|
-
.sidebar-
|
|
485
|
+
.sidebar-body::-webkit-scrollbar { width: 3px; }
|
|
486
|
+
.sidebar-body::-webkit-scrollbar-thumb { background: var(--border); }
|
|
417
487
|
|
|
418
|
-
/* ──
|
|
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
|
|
424
|
-
font-family: var(--
|
|
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.
|
|
496
|
+
line-height: 1.5;
|
|
427
497
|
}
|
|
428
498
|
|
|
429
499
|
.log-entry:last-child { border-bottom: none; }
|
|
430
|
-
|
|
431
|
-
.log-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
.
|
|
559
|
+
.overlay.open { display: flex; }
|
|
449
560
|
|
|
450
|
-
.
|
|
451
|
-
|
|
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-
|
|
457
|
-
font-
|
|
582
|
+
font-size: 11px;
|
|
583
|
+
font-weight: 600;
|
|
458
584
|
color: var(--muted);
|
|
459
585
|
text-transform: uppercase;
|
|
460
|
-
letter-spacing: 0.
|
|
461
|
-
margin-bottom:
|
|
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
|
|
468
|
-
border-radius:
|
|
469
|
-
padding:
|
|
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
|
|
472
|
-
font-size:
|
|
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:
|
|
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(--
|
|
610
|
+
.field select option { background: var(--bg); }
|
|
482
611
|
|
|
483
|
-
.
|
|
484
|
-
|
|
485
|
-
|
|
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:
|
|
489
|
-
color: #
|
|
490
|
-
font-
|
|
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
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
.
|
|
505
|
-
font-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
521
|
-
|
|
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
|
-
.
|
|
525
|
-
font-family: var(--
|
|
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
|
-
|
|
705
|
+
font-weight: 700;
|
|
528
706
|
text-transform: uppercase;
|
|
529
|
-
letter-spacing: 0.1em;
|
|
530
|
-
margin-bottom: 8px;
|
|
531
707
|
}
|
|
532
708
|
|
|
533
|
-
|
|
534
|
-
.
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
display: none;
|
|
724
|
+
flex-direction: column;
|
|
725
|
+
transform: translateY(100%);
|
|
726
|
+
transition: transform 0.25s ease;
|
|
543
727
|
}
|
|
544
728
|
|
|
545
|
-
.
|
|
729
|
+
.term-panel.open { transform: translateY(0); }
|
|
546
730
|
|
|
547
|
-
.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
.
|
|
557
|
-
font-family: var(--
|
|
558
|
-
font-size:
|
|
559
|
-
color: var(--
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
margin-top: 16px;
|
|
749
|
+
#terminal {
|
|
750
|
+
flex: 1;
|
|
751
|
+
overflow: hidden;
|
|
752
|
+
padding: 4px;
|
|
570
753
|
}
|
|
571
754
|
|
|
572
|
-
.btn-
|
|
573
|
-
|
|
755
|
+
.btn-term-close {
|
|
756
|
+
margin-left: auto;
|
|
574
757
|
background: none;
|
|
575
|
-
border:
|
|
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
|
-
/* ──
|
|
603
|
-
.
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
border-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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
|
-
|
|
630
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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-
|
|
645
|
-
|
|
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
|
-
<
|
|
649
|
-
|
|
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
|
-
</
|
|
868
|
+
</div>
|
|
655
869
|
|
|
656
|
-
<!--
|
|
657
|
-
<div class="
|
|
658
|
-
<div class="
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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="
|
|
700
|
-
<button class="tab" onclick="switchTab('detail')"
|
|
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-
|
|
703
|
-
<div class="empty"
|
|
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="
|
|
939
|
+
<div class="overlay" id="modal" onclick="if(event.target===this)closeModal()">
|
|
717
940
|
<div class="modal">
|
|
718
|
-
<div class="modal-title"
|
|
941
|
+
<div class="modal-title">Add Task</div>
|
|
719
942
|
<div class="field">
|
|
720
943
|
<label>Title</label>
|
|
721
|
-
<input type="text" id="
|
|
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="
|
|
948
|
+
<textarea id="f-desc" rows="3" placeholder="Detailed description of what needs to be done..."></textarea>
|
|
726
949
|
</div>
|
|
727
|
-
<div
|
|
950
|
+
<div class="modal-grid">
|
|
728
951
|
<div class="field">
|
|
729
952
|
<label>Priority</label>
|
|
730
|
-
<select id="
|
|
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="
|
|
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-
|
|
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
|
-
|
|
756
|
-
let ws;
|
|
1084
|
+
// ── STATE ────────────────────────────────────────────────────────────────────
|
|
757
1085
|
let board = { epics: [], logs: [] };
|
|
758
|
-
let activeTab = '
|
|
1086
|
+
let activeTab = 'activity';
|
|
759
1087
|
let selectedTask = null;
|
|
1088
|
+
let draggedId = null;
|
|
1089
|
+
let ws;
|
|
760
1090
|
|
|
761
|
-
// ──
|
|
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 (
|
|
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 === '
|
|
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 ? '
|
|
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
|
-
|
|
1126
|
+
renderKanban();
|
|
794
1127
|
updateStats();
|
|
795
|
-
if (activeTab === '
|
|
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
|
|
800
|
-
const todo =
|
|
801
|
-
const
|
|
802
|
-
const done =
|
|
803
|
-
const
|
|
804
|
-
const total =
|
|
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('
|
|
1144
|
+
document.getElementById('statProg').textContent = prog;
|
|
808
1145
|
document.getElementById('statDone').textContent = done;
|
|
809
|
-
document.getElementById('
|
|
1146
|
+
document.getElementById('statErr').textContent = err;
|
|
810
1147
|
|
|
811
1148
|
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
812
|
-
document.getElementById('
|
|
1149
|
+
document.getElementById('progressFill').style.width = pct + '%';
|
|
1150
|
+
document.getElementById('progressPct').textContent = pct + '%';
|
|
813
1151
|
|
|
814
|
-
//
|
|
815
|
-
const running =
|
|
816
|
-
const bar = document.getElementById('
|
|
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.
|
|
819
|
-
document.getElementById('
|
|
1156
|
+
bar.className = 'running-bar visible';
|
|
1157
|
+
document.getElementById('runningTitle').textContent = running.title;
|
|
820
1158
|
} else {
|
|
821
|
-
bar.
|
|
1159
|
+
bar.className = 'running-bar';
|
|
822
1160
|
}
|
|
823
1161
|
}
|
|
824
1162
|
|
|
825
|
-
// ── RENDER
|
|
826
|
-
function
|
|
827
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
835
|
-
const
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
|
851
|
-
const icons = { todo: '', in_progress: '
|
|
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="
|
|
854
|
-
<div class="
|
|
855
|
-
|
|
856
|
-
<div class="
|
|
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
|
-
|
|
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
|
-
// ──
|
|
868
|
-
function
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
el
|
|
872
|
-
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
// ──
|
|
894
|
-
async function
|
|
1266
|
+
// ── CARD SELECT ───────────────────────────────────────────────────────────────
|
|
1267
|
+
async function selectCard(id) {
|
|
895
1268
|
activeTab = 'detail';
|
|
896
|
-
document.getElementById('
|
|
897
|
-
document.getElementById('
|
|
1269
|
+
document.getElementById('tab-activity').className = 'stab';
|
|
1270
|
+
document.getElementById('tab-detail').className = 'stab active';
|
|
898
1271
|
|
|
899
|
-
const
|
|
900
|
-
|
|
1272
|
+
const task = allTasks().find(t => t.id === id);
|
|
1273
|
+
if (!task) return;
|
|
1274
|
+
selectedTask = task;
|
|
901
1275
|
|
|
902
|
-
const
|
|
903
|
-
const { logs } = await
|
|
1276
|
+
const res = await fetch(`/api/tasks/${id}/logs`);
|
|
1277
|
+
const { logs } = await res.json();
|
|
904
1278
|
|
|
905
|
-
const el = document.getElementById('
|
|
1279
|
+
const el = document.getElementById('sidebarBody');
|
|
906
1280
|
el.innerHTML = `
|
|
907
|
-
<div
|
|
908
|
-
<div class="detail-title">${esc(
|
|
909
|
-
<div
|
|
910
|
-
<span class="
|
|
911
|
-
<span class="
|
|
912
|
-
<span class="
|
|
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
|
-
|
|
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('
|
|
931
|
-
document.getElementById('
|
|
932
|
-
if (tab === '
|
|
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
|
-
// ──
|
|
936
|
-
function openModal() { document.getElementById('modal').className = '
|
|
937
|
-
function closeModal() { document.getElementById('modal').className = '
|
|
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('
|
|
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('
|
|
949
|
-
priority: document.getElementById('
|
|
950
|
-
type: document.getElementById('
|
|
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('
|
|
955
|
-
document.getElementById('
|
|
1350
|
+
document.getElementById('f-title').value = '';
|
|
1351
|
+
document.getElementById('f-desc').value = '';
|
|
956
1352
|
closeModal();
|
|
957
1353
|
loadBoard();
|
|
958
1354
|
}
|
|
959
1355
|
|
|
960
|
-
// ──
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
967
|
-
document.getElementById('
|
|
968
|
-
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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,
|
|
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>
|