tlc-claude-code 0.8.0 → 0.8.1

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/README.md CHANGED
@@ -101,7 +101,6 @@ TLC knows where you are and what's next.
101
101
  | `/tlc:claim` | Reserve a task |
102
102
  | `/tlc:who` | See who's working on what |
103
103
  | `/tlc:bug` | Log a bug |
104
- | `/tlc:server` | Start dev server with dashboard |
105
104
  | `npx tlc-claude-code init` | Add Docker dev launcher to project |
106
105
 
107
106
  ### Integration Commands
@@ -128,9 +127,10 @@ TLC supports distributed teams with git-based coordination.
128
127
  ```
129
128
 
130
129
  ```bash
131
- /tlc:claim 2 # Reserve task 2
132
- /tlc:who # See team status
133
- /tlc:server # Start dashboard for QA
130
+ /tlc:claim 2 # Reserve task 2
131
+ /tlc:who # See team status
132
+ npx tlc-claude-code init # Add dev server launcher
133
+ # Then double-click tlc-start.bat
134
134
  ```
135
135
 
136
136
  **📄 [Full Team Workflow Guide →](docs/team-workflow.md)**
@@ -139,44 +139,34 @@ TLC supports distributed teams with git-based coordination.
139
139
 
140
140
  ## Dev Server
141
141
 
142
- Launch a mini-Replit for your team. Two options:
143
-
144
- ### Option 1: Docker (Recommended)
145
-
146
- One-click launcher that runs your app, database, and dashboard in containers:
142
+ Launch a mini-Replit for your team with Docker:
147
143
 
148
144
  ```bash
149
- # Add launcher to your project
145
+ # Add launcher to your project (one-time)
150
146
  npx tlc-claude-code init
151
147
 
152
148
  # Then double-click tlc-start.bat (Windows)
153
149
  ```
154
150
 
155
151
  **What you get:**
156
- - **Dashboard**: http://localhost:3147 — Live preview, logs, tasks
157
- - **App**: http://localhost:5000 — Your running application
158
- - **Database**: localhost:5433 — PostgreSQL auto-provisioned
159
-
160
- Containers are named `tlc-{project}-*` so you can run multiple projects simultaneously.
161
-
162
- **Requirements:** [Docker Desktop](https://www.docker.com/products/docker-desktop)
163
-
164
- > **Note:** Windows only for now. macOS/Linux support coming soon.
165
-
166
- ### Option 2: Direct (No Docker)
167
152
 
168
- ```bash
169
- /tlc:server
170
- ```
171
-
172
- Runs the app directly on your machine with auto-detected start command.
173
-
174
- ### Features
153
+ | URL | Service |
154
+ |-----|---------|
155
+ | http://localhost:3147 | Dashboard — Live preview, logs, tasks |
156
+ | http://localhost:5000 | App — Your running application |
157
+ | http://localhost:8080 | DB Admin Database GUI (Adminer) |
158
+ | localhost:5433 | Database — PostgreSQL |
175
159
 
160
+ **Features:**
176
161
  - **Live preview** — Your app embedded in dashboard
177
162
  - **Real-time logs** — App, tests, git activity
178
163
  - **Bug submission** — Web form for QA
179
164
  - **Task board** — Who's working on what
165
+ - **Multi-project** — Containers named `tlc-{project}-*` for simultaneous projects
166
+
167
+ **Requirements:** [Docker Desktop](https://www.docker.com/products/docker-desktop)
168
+
169
+ > **Note:** Windows only for now. macOS/Linux support coming soon.
180
170
 
181
171
  ---
182
172
 
@@ -289,7 +279,6 @@ Commands install to `.claude/commands/tlc/`
289
279
 
290
280
  - **[Help / All Commands](help.md)** — Complete command reference
291
281
  - **[Team Workflow](docs/team-workflow.md)** — Guide for teams (engineers + PO + QA)
292
- - **[Server Spec](server.md)** — Dev server documentation
293
282
 
294
283
  ---
295
284
 
package/bin/install.js CHANGED
@@ -134,6 +134,12 @@ function install(targetDir, installType) {
134
134
  async function main() {
135
135
  const args = process.argv.slice(2);
136
136
 
137
+ // Handle 'init' subcommand - delegate to init.js
138
+ if (args[0] === 'init') {
139
+ require('./init.js');
140
+ return;
141
+ }
142
+
137
143
  printBanner();
138
144
 
139
145
  if (args.includes('--global') || args.includes('-g')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc-claude-code": "./bin/install.js",
@@ -23,7 +23,7 @@
23
23
  border-bottom: 1px solid #30363d;
24
24
  }
25
25
  .header h1 {
26
- font-size: 16px;
26
+ font-size: 18px;
27
27
  color: #58a6ff;
28
28
  display: flex;
29
29
  align-items: center;
@@ -31,8 +31,9 @@
31
31
  }
32
32
  .header h1 .logo {
33
33
  font-family: monospace;
34
- font-size: 14px;
34
+ font-size: 16px;
35
35
  color: #7ee787;
36
+ font-weight: bold;
36
37
  }
37
38
  .header .status {
38
39
  display: flex;
@@ -46,274 +47,310 @@
46
47
  }
47
48
  .header .status .dot.running { background: #3fb950; }
48
49
  .header .status .dot.stopped { background: #f85149; }
49
- .header button {
50
- padding: 6px 14px;
51
- background: #21262d;
52
- border: 1px solid #30363d;
53
- border-radius: 6px;
54
- color: #e6edf3;
55
- cursor: pointer;
56
- font-size: 13px;
57
- }
58
- .header button:hover {
59
- background: #30363d;
50
+ .header .phase-badge {
51
+ padding: 4px 10px;
52
+ background: #238636;
53
+ border-radius: 12px;
54
+ font-size: 12px;
55
+ font-weight: 500;
60
56
  }
61
57
 
62
- .main {
58
+ .container {
63
59
  display: grid;
64
- grid-template-columns: 1fr 380px;
65
- grid-template-rows: 1fr 1fr;
66
- height: calc(100vh - 50px);
60
+ grid-template-columns: 1fr 300px;
61
+ height: calc(100vh - 54px);
67
62
  }
68
63
 
69
- .preview {
70
- grid-row: 1 / 3;
71
- border-right: 1px solid #30363d;
64
+ .main-content {
72
65
  display: flex;
73
66
  flex-direction: column;
67
+ overflow: hidden;
74
68
  }
75
- .preview-header {
76
- padding: 8px 12px;
77
- background: #161b22;
69
+
70
+ .tabs {
78
71
  display: flex;
79
- justify-content: space-between;
80
- align-items: center;
72
+ gap: 0;
73
+ background: #161b22;
81
74
  border-bottom: 1px solid #30363d;
82
- gap: 10px;
83
75
  }
84
- .preview-header .label {
85
- font-size: 12px;
86
- color: #8b949e;
87
- text-transform: uppercase;
88
- }
89
- .preview-header input {
90
- flex: 1;
91
- padding: 6px 10px;
92
- background: #0d1117;
93
- border: 1px solid #30363d;
94
- border-radius: 6px;
95
- color: #e6edf3;
96
- font-size: 13px;
97
- }
98
- .preview-header button {
99
- padding: 6px 12px;
76
+ .tab {
77
+ padding: 12px 20px;
100
78
  background: transparent;
101
- border: 1px solid #30363d;
102
- border-radius: 6px;
79
+ border: none;
103
80
  color: #8b949e;
104
81
  cursor: pointer;
105
- font-size: 12px;
82
+ font-size: 14px;
83
+ border-bottom: 2px solid transparent;
84
+ transition: all 0.2s;
106
85
  }
107
- .preview-header button:hover {
86
+ .tab:hover {
108
87
  color: #e6edf3;
109
- border-color: #58a6ff;
88
+ background: #21262d;
110
89
  }
111
- .preview iframe {
112
- flex: 1;
113
- border: none;
114
- background: white;
90
+ .tab.active {
91
+ color: #58a6ff;
92
+ border-bottom-color: #58a6ff;
93
+ }
94
+ .tab .badge {
95
+ background: #f85149;
96
+ color: white;
97
+ padding: 2px 6px;
98
+ border-radius: 10px;
99
+ font-size: 11px;
100
+ margin-left: 6px;
115
101
  }
116
- .preview .loading {
102
+
103
+ .tab-content {
117
104
  flex: 1;
118
- display: flex;
119
- align-items: center;
120
- justify-content: center;
121
- background: #1a1a2e;
122
- color: #8b949e;
105
+ overflow: hidden;
106
+ }
107
+ .tab-panel {
108
+ display: none;
109
+ height: 100%;
110
+ overflow-y: auto;
111
+ padding: 20px;
112
+ }
113
+ .tab-panel.active {
114
+ display: block;
115
+ }
116
+
117
+ /* Plan Panel */
118
+ .plan-content {
119
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
120
+ font-size: 13px;
121
+ line-height: 1.6;
122
+ white-space: pre-wrap;
123
+ }
124
+ .plan-content h1, .plan-content h2, .plan-content h3 {
125
+ font-family: system-ui, sans-serif;
126
+ color: #58a6ff;
127
+ margin: 20px 0 10px 0;
123
128
  }
129
+ .plan-content h1 { font-size: 20px; }
130
+ .plan-content h2 { font-size: 16px; }
131
+ .plan-content h3 { font-size: 14px; }
132
+ .plan-content .task-done { color: #3fb950; }
133
+ .plan-content .task-working { color: #58a6ff; }
134
+ .plan-content .task-todo { color: #8b949e; }
124
135
 
125
- .logs {
136
+ /* Tests Panel */
137
+ .test-section {
138
+ margin-bottom: 30px;
139
+ }
140
+ .test-section h3 {
141
+ font-size: 14px;
142
+ color: #58a6ff;
143
+ margin-bottom: 15px;
144
+ padding-bottom: 8px;
126
145
  border-bottom: 1px solid #30363d;
127
- display: flex;
128
- flex-direction: column;
129
146
  }
130
- .logs-header {
131
- padding: 8px 12px;
147
+ .test-item {
148
+ padding: 12px 15px;
132
149
  background: #161b22;
150
+ border-radius: 6px;
151
+ margin-bottom: 8px;
133
152
  display: flex;
134
- gap: 8px;
135
- border-bottom: 1px solid #30363d;
153
+ align-items: center;
154
+ gap: 12px;
136
155
  }
137
- .logs-header button {
138
- padding: 4px 12px;
139
- background: transparent;
140
- border: 1px solid #30363d;
156
+ .test-item .checkbox {
157
+ width: 18px;
158
+ height: 18px;
159
+ border: 2px solid #30363d;
141
160
  border-radius: 4px;
142
- color: #8b949e;
143
161
  cursor: pointer;
144
- font-size: 12px;
145
- }
146
- .logs-header button.active {
147
- background: #21262d;
148
- color: #e6edf3;
149
- border-color: #58a6ff;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
150
165
  }
151
- .logs-header button:hover:not(.active) {
152
- background: #21262d;
166
+ .test-item .checkbox.checked {
167
+ background: #238636;
168
+ border-color: #238636;
153
169
  }
154
- .logs-content {
155
- flex: 1;
156
- overflow-y: auto;
157
- padding: 10px;
158
- font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
170
+ .test-item .checkbox.checked::after {
171
+ content: '✓';
172
+ color: white;
159
173
  font-size: 12px;
160
- background: #010409;
161
- line-height: 1.5;
162
- }
163
- .log-line { padding: 1px 0; white-space: pre-wrap; word-break: break-word; }
164
- .log-line.error { color: #f85149; }
165
- .log-line.success { color: #3fb950; }
166
- .log-line.info { color: #58a6ff; }
167
- .log-line.warn { color: #d29922; }
168
-
169
- .sidebar {
170
- display: flex;
171
- flex-direction: column;
172
174
  }
173
-
174
- .tasks {
175
+ .test-item .text {
175
176
  flex: 1;
176
- overflow-y: auto;
177
- padding: 12px;
178
177
  }
179
- .tasks-header {
178
+ .test-actions {
180
179
  display: flex;
181
- justify-content: space-between;
182
- align-items: center;
183
- margin-bottom: 12px;
184
- }
185
- .tasks h2 {
186
- font-size: 11px;
187
- text-transform: uppercase;
188
- color: #8b949e;
189
- letter-spacing: 0.5px;
190
- }
191
- .tasks .phase-name {
192
- font-size: 13px;
193
- color: #e6edf3;
194
- font-weight: 500;
180
+ gap: 10px;
181
+ margin-bottom: 20px;
195
182
  }
196
- .task {
197
- padding: 10px 12px;
183
+
184
+ /* Bugs Panel */
185
+ .bug-item {
186
+ padding: 15px;
198
187
  background: #161b22;
199
188
  border-radius: 6px;
200
- margin-bottom: 8px;
201
- border-left: 3px solid transparent;
202
- cursor: default;
189
+ margin-bottom: 10px;
190
+ border-left: 3px solid #f85149;
203
191
  }
204
- .task.done {
192
+ .bug-item.closed {
205
193
  border-left-color: #3fb950;
206
194
  opacity: 0.6;
207
195
  }
208
- .task.working {
209
- border-left-color: #58a6ff;
210
- background: #1c2128;
211
- }
212
- .task.available {
213
- border-left-color: #484f58;
214
- }
215
- .task .title {
216
- font-weight: 500;
217
- font-size: 13px;
196
+ .bug-item .bug-header {
218
197
  display: flex;
219
- align-items: center;
220
- gap: 8px;
198
+ justify-content: space-between;
199
+ margin-bottom: 8px;
221
200
  }
222
- .task .title .icon {
223
- font-size: 12px;
201
+ .bug-item .bug-id {
202
+ font-weight: bold;
203
+ color: #f85149;
224
204
  }
225
- .task.done .icon { color: #3fb950; }
226
- .task.working .icon { color: #58a6ff; }
227
- .task.available .icon { color: #484f58; }
228
- .task .meta {
229
- font-size: 11px;
205
+ .bug-item.closed .bug-id { color: #3fb950; }
206
+ .bug-item .bug-date {
230
207
  color: #8b949e;
231
- margin-top: 4px;
232
- margin-left: 20px;
208
+ font-size: 12px;
209
+ }
210
+ .bug-item .bug-desc {
211
+ font-size: 14px;
212
+ line-height: 1.5;
233
213
  }
234
214
 
235
215
  .bug-form {
236
- padding: 12px;
237
216
  background: #161b22;
238
- border-top: 1px solid #30363d;
217
+ padding: 20px;
218
+ border-radius: 8px;
219
+ margin-bottom: 20px;
239
220
  }
240
- .bug-form h2 {
241
- font-size: 11px;
242
- text-transform: uppercase;
243
- color: #8b949e;
244
- margin-bottom: 10px;
245
- letter-spacing: 0.5px;
221
+ .bug-form h3 {
222
+ font-size: 14px;
223
+ margin-bottom: 15px;
224
+ color: #e6edf3;
246
225
  }
247
226
  .bug-form textarea {
248
227
  width: 100%;
249
- height: 60px;
250
- padding: 8px;
228
+ height: 80px;
229
+ padding: 10px;
251
230
  background: #0d1117;
252
231
  border: 1px solid #30363d;
253
232
  border-radius: 6px;
254
233
  color: #e6edf3;
255
- resize: none;
256
- margin-bottom: 8px;
234
+ resize: vertical;
235
+ margin-bottom: 10px;
257
236
  font-family: inherit;
258
- font-size: 13px;
237
+ font-size: 14px;
259
238
  }
260
239
  .bug-form textarea:focus {
261
240
  outline: none;
262
241
  border-color: #58a6ff;
263
242
  }
264
- .bug-form .actions {
243
+ .bug-form select {
244
+ padding: 8px 12px;
245
+ background: #21262d;
246
+ border: 1px solid #30363d;
247
+ border-radius: 6px;
248
+ color: #e6edf3;
249
+ margin-right: 10px;
250
+ }
251
+
252
+ /* Logs Panel */
253
+ .logs-tabs {
265
254
  display: flex;
266
255
  gap: 8px;
256
+ margin-bottom: 15px;
267
257
  }
268
- .bug-form button {
269
- padding: 8px 14px;
270
- border-radius: 6px;
271
- border: none;
258
+ .logs-tabs button {
259
+ padding: 6px 14px;
260
+ background: #21262d;
261
+ border: 1px solid #30363d;
262
+ border-radius: 4px;
263
+ color: #8b949e;
272
264
  cursor: pointer;
273
265
  font-size: 12px;
274
- font-weight: 500;
275
266
  }
276
- .bug-form .screenshot {
277
- background: #21262d;
267
+ .logs-tabs button.active {
268
+ background: #30363d;
278
269
  color: #e6edf3;
270
+ border-color: #58a6ff;
271
+ }
272
+ .logs-content {
273
+ background: #010409;
274
+ border-radius: 6px;
275
+ padding: 15px;
276
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
277
+ font-size: 12px;
278
+ line-height: 1.6;
279
+ height: calc(100% - 60px);
280
+ overflow-y: auto;
281
+ }
282
+ .log-line { padding: 2px 0; white-space: pre-wrap; word-break: break-word; }
283
+ .log-line.error { color: #f85149; }
284
+ .log-line.success { color: #3fb950; }
285
+ .log-line.info { color: #58a6ff; }
286
+ .log-line.warn { color: #d29922; }
287
+
288
+ /* Changelog Panel */
289
+ .commit {
290
+ padding: 15px;
291
+ background: #161b22;
292
+ border-radius: 6px;
293
+ margin-bottom: 10px;
294
+ display: flex;
295
+ gap: 15px;
296
+ }
297
+ .commit .hash {
298
+ font-family: monospace;
299
+ color: #58a6ff;
300
+ font-size: 12px;
301
+ }
302
+ .commit .message {
279
303
  flex: 1;
280
- border: 1px solid #30363d;
281
304
  }
282
- .bug-form .screenshot:hover {
283
- background: #30363d;
305
+ .commit .message .title {
306
+ font-weight: 500;
307
+ margin-bottom: 4px;
284
308
  }
285
- .bug-form .submit {
286
- background: #238636;
287
- color: white;
288
- padding-left: 20px;
289
- padding-right: 20px;
309
+ .commit .message .meta {
310
+ font-size: 12px;
311
+ color: #8b949e;
312
+ }
313
+
314
+ /* Sidebar */
315
+ .sidebar {
316
+ background: #161b22;
317
+ border-left: 1px solid #30363d;
318
+ display: flex;
319
+ flex-direction: column;
320
+ }
321
+
322
+ .sidebar-section {
323
+ padding: 20px;
324
+ border-bottom: 1px solid #30363d;
290
325
  }
291
- .bug-form .submit:hover {
292
- background: #2ea043;
326
+ .sidebar-section h3 {
327
+ font-size: 11px;
328
+ text-transform: uppercase;
329
+ color: #8b949e;
330
+ margin-bottom: 15px;
331
+ letter-spacing: 0.5px;
293
332
  }
294
333
 
295
- .stats {
334
+ .stats-grid {
296
335
  display: grid;
297
- grid-template-columns: repeat(3, 1fr);
298
- gap: 8px;
299
- padding: 12px;
300
- background: #161b22;
301
- border-top: 1px solid #30363d;
336
+ grid-template-columns: 1fr 1fr;
337
+ gap: 10px;
302
338
  }
303
339
  .stat {
304
340
  text-align: center;
305
- padding: 10px;
341
+ padding: 15px 10px;
306
342
  background: #0d1117;
307
343
  border-radius: 6px;
308
344
  }
309
345
  .stat-value {
310
- font-size: 22px;
346
+ font-size: 24px;
311
347
  font-weight: bold;
312
348
  font-variant-numeric: tabular-nums;
313
349
  }
314
350
  .stat-value.green { color: #3fb950; }
315
351
  .stat-value.red { color: #f85149; }
316
352
  .stat-value.blue { color: #58a6ff; }
353
+ .stat-value.yellow { color: #d29922; }
317
354
  .stat-label {
318
355
  font-size: 10px;
319
356
  color: #8b949e;
@@ -321,20 +358,92 @@
321
358
  margin-top: 4px;
322
359
  }
323
360
 
324
- /* Responsive */
325
- @media (max-width: 900px) {
326
- .main {
327
- grid-template-columns: 1fr;
328
- grid-template-rows: 1fr auto auto;
329
- }
330
- .preview {
331
- grid-row: auto;
332
- min-height: 300px;
333
- }
334
- .logs {
335
- max-height: 200px;
336
- }
361
+ .quick-actions {
362
+ display: flex;
363
+ flex-direction: column;
364
+ gap: 8px;
365
+ }
366
+ .action-btn {
367
+ padding: 10px 15px;
368
+ border-radius: 6px;
369
+ border: none;
370
+ cursor: pointer;
371
+ font-size: 13px;
372
+ font-weight: 500;
373
+ display: flex;
374
+ align-items: center;
375
+ gap: 8px;
376
+ transition: all 0.2s;
377
+ }
378
+ .action-btn.primary {
379
+ background: #238636;
380
+ color: white;
381
+ }
382
+ .action-btn.primary:hover { background: #2ea043; }
383
+ .action-btn.secondary {
384
+ background: #21262d;
385
+ color: #e6edf3;
386
+ border: 1px solid #30363d;
387
+ }
388
+ .action-btn.secondary:hover { background: #30363d; }
389
+ .action-btn .icon { font-size: 16px; }
390
+
391
+ .links {
392
+ display: flex;
393
+ flex-direction: column;
394
+ gap: 8px;
395
+ }
396
+ .link {
397
+ display: flex;
398
+ justify-content: space-between;
399
+ align-items: center;
400
+ padding: 10px;
401
+ background: #0d1117;
402
+ border-radius: 6px;
403
+ text-decoration: none;
404
+ color: #e6edf3;
405
+ font-size: 13px;
406
+ }
407
+ .link:hover {
408
+ background: #21262d;
409
+ }
410
+ .link .url {
411
+ color: #8b949e;
412
+ font-family: monospace;
413
+ font-size: 11px;
414
+ }
415
+
416
+ .empty-state {
417
+ text-align: center;
418
+ padding: 40px;
419
+ color: #8b949e;
420
+ }
421
+ .empty-state .icon {
422
+ font-size: 48px;
423
+ margin-bottom: 15px;
424
+ opacity: 0.5;
425
+ }
426
+
427
+ /* Buttons */
428
+ .btn {
429
+ padding: 8px 16px;
430
+ border-radius: 6px;
431
+ border: none;
432
+ cursor: pointer;
433
+ font-size: 13px;
434
+ font-weight: 500;
435
+ }
436
+ .btn-primary {
437
+ background: #238636;
438
+ color: white;
439
+ }
440
+ .btn-primary:hover { background: #2ea043; }
441
+ .btn-secondary {
442
+ background: #21262d;
443
+ color: #e6edf3;
444
+ border: 1px solid #30363d;
337
445
  }
446
+ .btn-secondary:hover { background: #30363d; }
338
447
  </style>
339
448
  </head>
340
449
  <body>
@@ -344,62 +453,154 @@
344
453
  Dev Server
345
454
  </h1>
346
455
  <div class="status">
456
+ <span class="phase-badge" id="phase-badge">Phase 1</span>
347
457
  <span class="dot" id="status-dot"></span>
348
458
  <span id="status-text">Connecting...</span>
349
- <button onclick="runTests()">Run Tests</button>
350
- <button onclick="restartApp()">Restart App</button>
351
459
  </div>
352
460
  </div>
353
461
 
354
- <div class="main">
355
- <div class="preview">
356
- <div class="preview-header">
357
- <span class="label">Preview</span>
358
- <input type="text" id="url-bar" value="http://localhost:3000/" onkeypress="if(event.key==='Enter')navigateTo(this.value)">
359
- <button onclick="refreshPreview()">Refresh</button>
360
- <button onclick="openInTab()">Open in Tab</button>
462
+ <div class="container">
463
+ <div class="main-content">
464
+ <div class="tabs">
465
+ <button class="tab active" data-tab="plan">Plan</button>
466
+ <button class="tab" data-tab="tests">Tests</button>
467
+ <button class="tab" data-tab="bugs">Bugs <span class="badge" id="bugs-badge" style="display:none">0</span></button>
468
+ <button class="tab" data-tab="logs">Logs</button>
469
+ <button class="tab" data-tab="changelog">Changelog</button>
361
470
  </div>
362
- <iframe id="app-frame" src="/app/"></iframe>
363
- </div>
364
471
 
365
- <div class="logs">
366
- <div class="logs-header">
367
- <button class="active" data-type="app" onclick="showLogs('app')">App</button>
368
- <button data-type="test" onclick="showLogs('test')">Tests</button>
369
- <button data-type="git" onclick="showLogs('git')">Git</button>
472
+ <div class="tab-content">
473
+ <!-- Plan Panel -->
474
+ <div class="tab-panel active" id="panel-plan">
475
+ <div class="plan-content" id="plan-content">
476
+ <div class="empty-state">
477
+ <div class="icon">📋</div>
478
+ <p>Loading plan...</p>
479
+ </div>
480
+ </div>
481
+ </div>
482
+
483
+ <!-- Tests Panel -->
484
+ <div class="tab-panel" id="panel-tests">
485
+ <div class="test-actions">
486
+ <button class="btn btn-primary" onclick="runTests()">▶ Run Unit Tests</button>
487
+ <button class="btn btn-secondary" onclick="runPlaywright()">🎭 Run Playwright</button>
488
+ </div>
489
+ <div class="test-section">
490
+ <h3>QA Test Checklist</h3>
491
+ <div id="test-checklist">
492
+ <div class="empty-state">
493
+ <div class="icon">✓</div>
494
+ <p>No test plan found. Create .planning/phases/{N}-TESTS.md</p>
495
+ </div>
496
+ </div>
497
+ </div>
498
+ <div class="test-section">
499
+ <h3>Test Results</h3>
500
+ <div id="test-results" class="logs-content" style="height: 300px;">
501
+ <div class="log-line info">Run tests to see results...</div>
502
+ </div>
503
+ </div>
504
+ </div>
505
+
506
+ <!-- Bugs Panel -->
507
+ <div class="tab-panel" id="panel-bugs">
508
+ <div class="bug-form">
509
+ <h3>Report New Bug</h3>
510
+ <textarea id="bug-desc" placeholder="Describe the bug..."></textarea>
511
+ <div>
512
+ <select id="bug-severity">
513
+ <option value="low">Low</option>
514
+ <option value="medium" selected>Medium</option>
515
+ <option value="high">High</option>
516
+ <option value="critical">Critical</option>
517
+ </select>
518
+ <button class="btn btn-primary" onclick="submitBug()">Submit Bug</button>
519
+ </div>
520
+ </div>
521
+ <div id="bugs-list">
522
+ <div class="empty-state">
523
+ <div class="icon">🐛</div>
524
+ <p>No bugs reported</p>
525
+ </div>
526
+ </div>
527
+ </div>
528
+
529
+ <!-- Logs Panel -->
530
+ <div class="tab-panel" id="panel-logs">
531
+ <div class="logs-tabs">
532
+ <button class="active" data-logtype="app" onclick="showLogs('app')">App</button>
533
+ <button data-logtype="test" onclick="showLogs('test')">Tests</button>
534
+ <button data-logtype="git" onclick="showLogs('git')">Git</button>
535
+ </div>
536
+ <div class="logs-content" id="logs-output"></div>
537
+ </div>
538
+
539
+ <!-- Changelog Panel -->
540
+ <div class="tab-panel" id="panel-changelog">
541
+ <div id="changelog-content">
542
+ <div class="empty-state">
543
+ <div class="icon">📝</div>
544
+ <p>Loading changelog...</p>
545
+ </div>
546
+ </div>
547
+ </div>
370
548
  </div>
371
- <div class="logs-content" id="logs"></div>
372
549
  </div>
373
550
 
374
551
  <div class="sidebar">
375
- <div class="tasks" id="tasks">
376
- <div class="tasks-header">
377
- <h2>Tasks</h2>
378
- <span class="phase-name" id="phase-name">Loading...</span>
552
+ <div class="sidebar-section">
553
+ <h3>Stats</h3>
554
+ <div class="stats-grid">
555
+ <div class="stat">
556
+ <div class="stat-value green" id="stat-pass">-</div>
557
+ <div class="stat-label">Passing</div>
558
+ </div>
559
+ <div class="stat">
560
+ <div class="stat-value red" id="stat-fail">-</div>
561
+ <div class="stat-label">Failing</div>
562
+ </div>
563
+ <div class="stat">
564
+ <div class="stat-value blue" id="stat-tasks">-</div>
565
+ <div class="stat-label">Tasks</div>
566
+ </div>
567
+ <div class="stat">
568
+ <div class="stat-value yellow" id="stat-bugs">-</div>
569
+ <div class="stat-label">Open Bugs</div>
570
+ </div>
379
571
  </div>
380
572
  </div>
381
573
 
382
- <div class="bug-form">
383
- <h2>Report Bug</h2>
384
- <textarea id="bug-desc" placeholder="Describe the issue..."></textarea>
385
- <div class="actions">
386
- <button class="screenshot" onclick="takeScreenshot()">Screenshot</button>
387
- <button class="submit" onclick="submitBug()">Submit Bug</button>
574
+ <div class="sidebar-section">
575
+ <h3>Quick Actions</h3>
576
+ <div class="quick-actions">
577
+ <button class="action-btn primary" onclick="runTests()">
578
+ <span class="icon">▶</span> Run Tests
579
+ </button>
580
+ <button class="action-btn secondary" onclick="runPlaywright()">
581
+ <span class="icon">🎭</span> Run Playwright
582
+ </button>
583
+ <button class="action-btn secondary" onclick="refreshAll()">
584
+ <span class="icon">🔄</span> Refresh All
585
+ </button>
388
586
  </div>
389
587
  </div>
390
588
 
391
- <div class="stats">
392
- <div class="stat">
393
- <div class="stat-value green" id="tests-pass">-</div>
394
- <div class="stat-label">Passing</div>
395
- </div>
396
- <div class="stat">
397
- <div class="stat-value red" id="tests-fail">-</div>
398
- <div class="stat-label">Failing</div>
399
- </div>
400
- <div class="stat">
401
- <div class="stat-value blue" id="bugs-open">-</div>
402
- <div class="stat-label">Open Bugs</div>
589
+ <div class="sidebar-section">
590
+ <h3>Links</h3>
591
+ <div class="links">
592
+ <a class="link" href="http://localhost:5000" target="_blank">
593
+ <span>App</span>
594
+ <span class="url">:5000</span>
595
+ </a>
596
+ <a class="link" href="http://localhost:8080" target="_blank">
597
+ <span>Database Admin</span>
598
+ <span class="url">:8080</span>
599
+ </a>
600
+ <a class="link" href="http://localhost:5433" target="_blank" onclick="event.preventDefault(); alert('Connect with: postgres://postgres:postgres@localhost:5433/app')">
601
+ <span>PostgreSQL</span>
602
+ <span class="url">:5433</span>
603
+ </a>
403
604
  </div>
404
605
  </div>
405
606
  </div>
@@ -409,9 +610,18 @@
409
610
  let ws;
410
611
  const logs = { app: [], test: [], git: [] };
411
612
  let currentLogType = 'app';
412
- let appPort = 3000;
413
613
  let reconnectAttempts = 0;
414
614
 
615
+ // Tab switching
616
+ document.querySelectorAll('.tab').forEach(tab => {
617
+ tab.addEventListener('click', () => {
618
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
619
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
620
+ tab.classList.add('active');
621
+ document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
622
+ });
623
+ });
624
+
415
625
  function connect() {
416
626
  ws = new WebSocket(`ws://${location.host}`);
417
627
 
@@ -423,22 +633,14 @@
423
633
  };
424
634
 
425
635
  ws.onclose = () => {
426
- console.log('Disconnected from TLC server');
636
+ console.log('Disconnected');
427
637
  updateStatus(false);
428
- addLog('app', 'Disconnected from server', 'error');
429
-
430
- // Reconnect with backoff
431
638
  if (reconnectAttempts < 10) {
432
- const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
433
- setTimeout(connect, delay);
639
+ setTimeout(connect, Math.min(1000 * Math.pow(2, reconnectAttempts), 30000));
434
640
  reconnectAttempts++;
435
641
  }
436
642
  };
437
643
 
438
- ws.onerror = (err) => {
439
- console.error('WebSocket error:', err);
440
- };
441
-
442
644
  ws.onmessage = (event) => {
443
645
  const msg = JSON.parse(event.data);
444
646
  handleMessage(msg);
@@ -448,56 +650,29 @@
448
650
  function handleMessage(msg) {
449
651
  switch(msg.type) {
450
652
  case 'init':
451
- // Restore logs from server
452
653
  Object.assign(logs, msg.data.logs || {});
453
- appPort = msg.data.appPort || 3000;
454
- document.getElementById('url-bar').value = `http://localhost:${appPort}/`;
455
654
  renderLogs();
456
655
  break;
457
-
458
656
  case 'app-log':
459
657
  addLog('app', msg.data.data, msg.data.level || detectLogLevel(msg.data.data));
460
658
  break;
461
-
462
659
  case 'test-output':
463
660
  addLog('test', msg.data.data, msg.data.stream === 'stderr' ? 'error' : '');
661
+ document.getElementById('test-results').innerHTML +=
662
+ `<div class="log-line ${msg.data.stream === 'stderr' ? 'error' : ''}">${escapeHtml(msg.data.data)}</div>`;
464
663
  break;
465
-
466
664
  case 'test-complete':
467
665
  const result = msg.data.exitCode === 0 ? 'passed' : 'failed';
468
666
  addLog('test', `Tests ${result}`, msg.data.exitCode === 0 ? 'success' : 'error');
469
667
  refreshStats();
470
668
  break;
471
-
472
669
  case 'git-activity':
473
670
  addLog('git', msg.data.entry, 'info');
671
+ refreshChangelog();
474
672
  break;
475
-
476
- case 'app-restart':
477
- addLog('app', '--- App restarting ---', 'warn');
478
- setTimeout(refreshPreview, 2000);
479
- break;
480
-
481
- case 'app-start':
482
- appPort = msg.data.port;
483
- document.getElementById('url-bar').value = `http://localhost:${appPort}/`;
484
- setTimeout(refreshPreview, 1000);
485
- break;
486
-
487
- case 'file-change':
488
- addLog('app', `File changed: ${msg.data.path}`, 'info');
489
- break;
490
-
491
- case 'task-update':
492
- refreshTasks();
493
- break;
494
-
495
673
  case 'bug-created':
496
674
  addLog('app', `Bug ${msg.data.bugId} created`, 'warn');
497
- refreshStats();
498
- break;
499
-
500
- case 'bug-update':
675
+ refreshBugs();
501
676
  refreshStats();
502
677
  break;
503
678
  }
@@ -506,83 +681,54 @@
506
681
  function updateStatus(connected) {
507
682
  const dot = document.getElementById('status-dot');
508
683
  const text = document.getElementById('status-text');
509
-
510
- if (connected) {
511
- dot.className = 'dot running';
512
- text.textContent = 'Running';
513
- } else {
514
- dot.className = 'dot stopped';
515
- text.textContent = 'Disconnected';
516
- }
684
+ dot.className = 'dot ' + (connected ? 'running' : 'stopped');
685
+ text.textContent = connected ? 'Running' : 'Disconnected';
517
686
  }
518
687
 
519
688
  function detectLogLevel(text) {
520
- if (/error|fail|exception|ECONNREFUSED/i.test(text)) return 'error';
689
+ if (/error|fail|exception/i.test(text)) return 'error';
521
690
  if (/warn/i.test(text)) return 'warn';
522
- if (/success|✓|pass|ready|started|listening/i.test(text)) return 'success';
523
- if (/info|GET|POST|PUT|DELETE/i.test(text)) return 'info';
691
+ if (/success|✓|pass|ready|started/i.test(text)) return 'success';
692
+ if (/info|GET|POST/i.test(text)) return 'info';
524
693
  return '';
525
694
  }
526
695
 
527
696
  function addLog(type, text, level = '') {
528
- if (!text || !text.trim()) return;
529
-
530
- const lines = text.split('\n');
531
- for (const line of lines) {
697
+ if (!text?.trim()) return;
698
+ text.split('\n').forEach(line => {
532
699
  if (line.trim()) {
533
- logs[type].push({ text: line, level: level || detectLogLevel(line), time: new Date() });
700
+ logs[type].push({ text: line, level: level || detectLogLevel(line) });
534
701
  }
535
- }
536
-
537
- // Keep last 1000 lines per type
538
- while (logs[type].length > 1000) {
539
- logs[type].shift();
540
- }
541
-
542
- if (type === currentLogType) {
543
- renderLogs();
544
- }
702
+ });
703
+ while (logs[type].length > 1000) logs[type].shift();
704
+ if (type === currentLogType) renderLogs();
545
705
  }
546
706
 
547
707
  function renderLogs() {
548
- const container = document.getElementById('logs');
708
+ const container = document.getElementById('logs-output');
549
709
  container.innerHTML = logs[currentLogType].map(l =>
550
710
  `<div class="log-line ${l.level}">${escapeHtml(l.text)}</div>`
551
711
  ).join('');
552
712
  container.scrollTop = container.scrollHeight;
553
713
  }
554
714
 
555
- function escapeHtml(text) {
556
- const div = document.createElement('div');
557
- div.textContent = text;
558
- return div.innerHTML;
559
- }
560
-
561
715
  function showLogs(type) {
562
716
  currentLogType = type;
563
- document.querySelectorAll('.logs-header button').forEach(b => {
564
- b.classList.toggle('active', b.dataset.type === type);
717
+ document.querySelectorAll('.logs-tabs button').forEach(b => {
718
+ b.classList.toggle('active', b.dataset.logtype === type);
565
719
  });
566
720
  renderLogs();
567
721
  }
568
722
 
569
- function navigateTo(url) {
570
- const proxyUrl = url.replace(/^http:\/\/localhost:\d+/, '/app');
571
- document.getElementById('app-frame').src = proxyUrl;
572
- }
573
-
574
- function refreshPreview() {
575
- const frame = document.getElementById('app-frame');
576
- frame.src = frame.src;
577
- }
578
-
579
- function openInTab() {
580
- window.open(`http://localhost:${appPort}`, '_blank');
723
+ function escapeHtml(text) {
724
+ const div = document.createElement('div');
725
+ div.textContent = text;
726
+ return div.innerHTML;
581
727
  }
582
728
 
583
729
  async function runTests() {
730
+ document.getElementById('test-results').innerHTML = '<div class="log-line info">Running tests...</div>';
584
731
  addLog('test', '--- Running tests ---', 'info');
585
- showLogs('test');
586
732
  try {
587
733
  await fetch('/api/test', { method: 'POST' });
588
734
  } catch (e) {
@@ -590,96 +736,133 @@
590
736
  }
591
737
  }
592
738
 
593
- async function restartApp() {
739
+ async function runPlaywright() {
740
+ document.getElementById('test-results').innerHTML = '<div class="log-line info">Running Playwright tests...</div>';
741
+ addLog('test', '--- Running Playwright ---', 'info');
594
742
  try {
595
- await fetch('/api/restart', { method: 'POST' });
743
+ await fetch('/api/playwright', { method: 'POST' });
596
744
  } catch (e) {
597
- addLog('app', 'Failed to restart app', 'error');
745
+ addLog('test', 'Playwright not configured. Add playwright container to docker-compose.', 'warn');
598
746
  }
599
747
  }
600
748
 
601
749
  async function submitBug() {
602
750
  const desc = document.getElementById('bug-desc').value.trim();
603
- if (!desc) {
604
- alert('Please describe the bug');
605
- return;
606
- }
751
+ const severity = document.getElementById('bug-severity').value;
752
+ if (!desc) { alert('Please describe the bug'); return; }
607
753
 
608
754
  try {
609
755
  const res = await fetch('/api/bug', {
610
756
  method: 'POST',
611
757
  headers: { 'Content-Type': 'application/json' },
612
- body: JSON.stringify({
613
- description: desc,
614
- url: document.getElementById('url-bar').value,
615
- screenshot: window.lastScreenshot || null
616
- })
758
+ body: JSON.stringify({ description: desc, severity })
617
759
  });
618
760
  const data = await res.json();
619
-
620
761
  if (data.success) {
621
762
  alert(`Bug ${data.bugId} created!`);
622
763
  document.getElementById('bug-desc').value = '';
623
- window.lastScreenshot = null;
624
- } else {
625
- alert('Failed to create bug: ' + (data.error || 'Unknown error'));
764
+ refreshBugs();
626
765
  }
627
766
  } catch (e) {
628
767
  alert('Failed to submit bug');
629
768
  }
630
769
  }
631
770
 
632
- async function takeScreenshot() {
633
- // For now, just indicate screenshot capture
634
- // Full implementation would use html2canvas or server-side puppeteer
635
- alert('Screenshot feature: Click Submit to log bug with current URL');
636
- window.lastScreenshot = 'url-only';
771
+ async function refreshPlan() {
772
+ try {
773
+ const res = await fetch('/api/plan');
774
+ const data = await res.json();
775
+
776
+ document.getElementById('phase-badge').textContent = `Phase ${data.phase || '?'}`;
777
+
778
+ if (data.content) {
779
+ // Simple markdown-ish rendering
780
+ let html = escapeHtml(data.content)
781
+ .replace(/^### (.+)$/gm, '<h3>$1</h3>')
782
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
783
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
784
+ .replace(/\[x@(\w+)\]/g, '<span class="task-done">[✓ @$1]</span>')
785
+ .replace(/\[>@(\w+)\]/g, '<span class="task-working">[→ @$1]</span>')
786
+ .replace(/\[ \]/g, '<span class="task-todo">[ ]</span>');
787
+ document.getElementById('plan-content').innerHTML = html;
788
+ } else {
789
+ document.getElementById('plan-content').innerHTML = `
790
+ <div class="empty-state">
791
+ <div class="icon">📋</div>
792
+ <p>No plan found. Run /tlc:plan to create one.</p>
793
+ </div>`;
794
+ }
795
+ } catch (e) {
796
+ console.error('Failed to load plan:', e);
797
+ }
637
798
  }
638
799
 
639
- async function refreshTasks() {
800
+ async function refreshTests() {
640
801
  try {
641
- const res = await fetch('/api/tasks');
642
- const tasks = await res.json();
802
+ const res = await fetch('/api/tests');
803
+ const data = await res.json();
804
+
805
+ if (data.items?.length) {
806
+ document.getElementById('test-checklist').innerHTML = data.items.map((t, i) => `
807
+ <div class="test-item">
808
+ <div class="checkbox ${t.checked ? 'checked' : ''}" onclick="toggleTest(${i})"></div>
809
+ <span class="text">${escapeHtml(t.text)}</span>
810
+ </div>
811
+ `).join('');
812
+ }
813
+ } catch (e) {
814
+ console.error('Failed to load tests:', e);
815
+ }
816
+ }
643
817
 
644
- const container = document.getElementById('tasks');
645
- const phaseName = document.getElementById('phase-name');
818
+ async function refreshBugs() {
819
+ try {
820
+ const res = await fetch('/api/bugs');
821
+ const data = await res.json();
646
822
 
647
- if (tasks.phase) {
648
- phaseName.textContent = `Phase ${tasks.phase}: ${tasks.phaseName || ''}`;
823
+ const openBugs = data.filter(b => b.status === 'open').length;
824
+ const badge = document.getElementById('bugs-badge');
825
+ if (openBugs > 0) {
826
+ badge.textContent = openBugs;
827
+ badge.style.display = 'inline';
649
828
  } else {
650
- phaseName.textContent = 'No active phase';
829
+ badge.style.display = 'none';
651
830
  }
652
831
 
653
- const taskList = tasks.items || [];
654
- if (taskList.length === 0) {
655
- container.innerHTML = `
656
- <div class="tasks-header">
657
- <h2>Tasks</h2>
658
- <span class="phase-name" id="phase-name">${phaseName.textContent}</span>
659
- </div>
660
- <div style="color: #8b949e; font-size: 13px; padding: 20px 0;">
661
- No tasks found. Run /tlc:plan to create tasks.
832
+ if (data.length) {
833
+ document.getElementById('bugs-list').innerHTML = data.map(b => `
834
+ <div class="bug-item ${b.status}">
835
+ <div class="bug-header">
836
+ <span class="bug-id">${b.id}</span>
837
+ <span class="bug-date">${b.date || ''}</span>
838
+ </div>
839
+ <div class="bug-desc">${escapeHtml(b.description)}</div>
662
840
  </div>
663
- `;
664
- return;
841
+ `).join('');
665
842
  }
843
+ } catch (e) {
844
+ console.error('Failed to load bugs:', e);
845
+ }
846
+ }
666
847
 
667
- container.innerHTML = `
668
- <div class="tasks-header">
669
- <h2>Tasks</h2>
670
- <span class="phase-name" id="phase-name">${phaseName.textContent}</span>
671
- </div>
672
- ` + taskList.map(t => `
673
- <div class="task ${t.status}">
674
- <div class="title">
675
- <span class="icon">${t.status === 'done' ? '' : t.status === 'working' ? '→' : '○'}</span>
676
- ${escapeHtml(t.title)}
848
+ async function refreshChangelog() {
849
+ try {
850
+ const res = await fetch('/api/changelog');
851
+ const data = await res.json();
852
+
853
+ if (data.commits?.length) {
854
+ document.getElementById('changelog-content').innerHTML = data.commits.map(c => `
855
+ <div class="commit">
856
+ <span class="hash">${c.hash?.slice(0, 7) || '---'}</span>
857
+ <div class="message">
858
+ <div class="title">${escapeHtml(c.message || '')}</div>
859
+ <div class="meta">${c.author || ''} • ${c.date || ''}</div>
860
+ </div>
677
861
  </div>
678
- <div class="meta">${t.owner ? '@' + t.owner : 'Available'}</div>
679
- </div>
680
- `).join('');
862
+ `).join('');
863
+ }
681
864
  } catch (e) {
682
- console.error('Failed to refresh tasks:', e);
865
+ console.error('Failed to load changelog:', e);
683
866
  }
684
867
  }
685
868
 
@@ -688,20 +871,26 @@
688
871
  const res = await fetch('/api/status');
689
872
  const data = await res.json();
690
873
 
691
- document.getElementById('tests-pass').textContent = data.testsPass || 0;
692
- document.getElementById('tests-fail').textContent = data.testsFail || 0;
693
- document.getElementById('bugs-open').textContent = data.bugsOpen || 0;
874
+ document.getElementById('stat-pass').textContent = data.testsPass || 0;
875
+ document.getElementById('stat-fail').textContent = data.testsFail || 0;
876
+ document.getElementById('stat-tasks').textContent = data.tasks || 0;
877
+ document.getElementById('stat-bugs').textContent = data.bugsOpen || 0;
694
878
  } catch (e) {
695
- console.error('Failed to refresh stats:', e);
879
+ console.error('Failed to load stats:', e);
696
880
  }
697
881
  }
698
882
 
883
+ function refreshAll() {
884
+ refreshPlan();
885
+ refreshTests();
886
+ refreshBugs();
887
+ refreshChangelog();
888
+ refreshStats();
889
+ }
890
+
699
891
  // Initialize
700
892
  connect();
701
- refreshTasks();
702
- refreshStats();
703
-
704
- // Refresh periodically
893
+ refreshAll();
705
894
  setInterval(refreshStats, 30000);
706
895
  </script>
707
896
  </body>