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 +18 -29
- package/bin/install.js +6 -0
- package/package.json +1 -1
- package/server/dashboard/index.html +572 -383
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
|
|
132
|
-
/tlc:who
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
border-bottom: 1px solid #30363d;
|
|
24
24
|
}
|
|
25
25
|
.header h1 {
|
|
26
|
-
font-size:
|
|
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:
|
|
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
|
|
50
|
-
padding:
|
|
51
|
-
background: #
|
|
52
|
-
border:
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
.
|
|
58
|
+
.container {
|
|
63
59
|
display: grid;
|
|
64
|
-
grid-template-columns: 1fr
|
|
65
|
-
|
|
66
|
-
height: calc(100vh - 50px);
|
|
60
|
+
grid-template-columns: 1fr 300px;
|
|
61
|
+
height: calc(100vh - 54px);
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
.
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
background: #161b22;
|
|
69
|
+
|
|
70
|
+
.tabs {
|
|
78
71
|
display: flex;
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
gap: 0;
|
|
73
|
+
background: #161b22;
|
|
81
74
|
border-bottom: 1px solid #30363d;
|
|
82
|
-
gap: 10px;
|
|
83
75
|
}
|
|
84
|
-
.
|
|
85
|
-
|
|
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:
|
|
102
|
-
border-radius: 6px;
|
|
79
|
+
border: none;
|
|
103
80
|
color: #8b949e;
|
|
104
81
|
cursor: pointer;
|
|
105
|
-
font-size:
|
|
82
|
+
font-size: 14px;
|
|
83
|
+
border-bottom: 2px solid transparent;
|
|
84
|
+
transition: all 0.2s;
|
|
106
85
|
}
|
|
107
|
-
.
|
|
86
|
+
.tab:hover {
|
|
108
87
|
color: #e6edf3;
|
|
109
|
-
|
|
88
|
+
background: #21262d;
|
|
110
89
|
}
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
border:
|
|
114
|
-
|
|
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
|
-
|
|
102
|
+
|
|
103
|
+
.tab-content {
|
|
117
104
|
flex: 1;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
131
|
-
padding:
|
|
147
|
+
.test-item {
|
|
148
|
+
padding: 12px 15px;
|
|
132
149
|
background: #161b22;
|
|
150
|
+
border-radius: 6px;
|
|
151
|
+
margin-bottom: 8px;
|
|
133
152
|
display: flex;
|
|
134
|
-
|
|
135
|
-
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 12px;
|
|
136
155
|
}
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
border:
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
background: #21262d;
|
|
148
|
-
color: #e6edf3;
|
|
149
|
-
border-color: #58a6ff;
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
justify-content: center;
|
|
150
165
|
}
|
|
151
|
-
.
|
|
152
|
-
background: #
|
|
166
|
+
.test-item .checkbox.checked {
|
|
167
|
+
background: #238636;
|
|
168
|
+
border-color: #238636;
|
|
153
169
|
}
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
.
|
|
178
|
+
.test-actions {
|
|
180
179
|
display: flex;
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
197
|
-
|
|
183
|
+
|
|
184
|
+
/* Bugs Panel */
|
|
185
|
+
.bug-item {
|
|
186
|
+
padding: 15px;
|
|
198
187
|
background: #161b22;
|
|
199
188
|
border-radius: 6px;
|
|
200
|
-
margin-bottom:
|
|
201
|
-
border-left: 3px solid
|
|
202
|
-
cursor: default;
|
|
189
|
+
margin-bottom: 10px;
|
|
190
|
+
border-left: 3px solid #f85149;
|
|
203
191
|
}
|
|
204
|
-
.
|
|
192
|
+
.bug-item.closed {
|
|
205
193
|
border-left-color: #3fb950;
|
|
206
194
|
opacity: 0.6;
|
|
207
195
|
}
|
|
208
|
-
.
|
|
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
|
-
|
|
220
|
-
|
|
198
|
+
justify-content: space-between;
|
|
199
|
+
margin-bottom: 8px;
|
|
221
200
|
}
|
|
222
|
-
.
|
|
223
|
-
font-
|
|
201
|
+
.bug-item .bug-id {
|
|
202
|
+
font-weight: bold;
|
|
203
|
+
color: #f85149;
|
|
224
204
|
}
|
|
225
|
-
.
|
|
226
|
-
.
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
217
|
+
padding: 20px;
|
|
218
|
+
border-radius: 8px;
|
|
219
|
+
margin-bottom: 20px;
|
|
239
220
|
}
|
|
240
|
-
.bug-form
|
|
241
|
-
font-size:
|
|
242
|
-
|
|
243
|
-
color: #
|
|
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:
|
|
250
|
-
padding:
|
|
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:
|
|
256
|
-
margin-bottom:
|
|
234
|
+
resize: vertical;
|
|
235
|
+
margin-bottom: 10px;
|
|
257
236
|
font-family: inherit;
|
|
258
|
-
font-size:
|
|
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
|
|
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
|
-
.
|
|
269
|
-
padding:
|
|
270
|
-
|
|
271
|
-
border:
|
|
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
|
-
.
|
|
277
|
-
background: #
|
|
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
|
-
.
|
|
283
|
-
|
|
305
|
+
.commit .message .title {
|
|
306
|
+
font-weight: 500;
|
|
307
|
+
margin-bottom: 4px;
|
|
284
308
|
}
|
|
285
|
-
.
|
|
286
|
-
|
|
287
|
-
color:
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
.
|
|
292
|
-
|
|
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:
|
|
298
|
-
gap:
|
|
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:
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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="
|
|
355
|
-
<div class="
|
|
356
|
-
<div class="
|
|
357
|
-
<
|
|
358
|
-
<
|
|
359
|
-
<button
|
|
360
|
-
<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
|
-
|
|
366
|
-
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
|
|
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="
|
|
376
|
-
<
|
|
377
|
-
|
|
378
|
-
<
|
|
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="
|
|
383
|
-
<
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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="
|
|
392
|
-
<
|
|
393
|
-
|
|
394
|
-
<
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
<
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
523
|
-
if (/info|GET|POST
|
|
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
|
|
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)
|
|
700
|
+
logs[type].push({ text: line, level: level || detectLogLevel(line) });
|
|
534
701
|
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
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-
|
|
564
|
-
b.classList.toggle('active', b.dataset.
|
|
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
|
|
570
|
-
const
|
|
571
|
-
|
|
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
|
|
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/
|
|
743
|
+
await fetch('/api/playwright', { method: 'POST' });
|
|
596
744
|
} catch (e) {
|
|
597
|
-
addLog('
|
|
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
|
-
|
|
604
|
-
|
|
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
|
-
|
|
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
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
|
800
|
+
async function refreshTests() {
|
|
640
801
|
try {
|
|
641
|
-
const res = await fetch('/api/
|
|
642
|
-
const
|
|
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
|
-
|
|
645
|
-
|
|
818
|
+
async function refreshBugs() {
|
|
819
|
+
try {
|
|
820
|
+
const res = await fetch('/api/bugs');
|
|
821
|
+
const data = await res.json();
|
|
646
822
|
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
829
|
+
badge.style.display = 'none';
|
|
651
830
|
}
|
|
652
831
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
<div class="
|
|
675
|
-
<span class="
|
|
676
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
`).join('');
|
|
862
|
+
`).join('');
|
|
863
|
+
}
|
|
681
864
|
} catch (e) {
|
|
682
|
-
console.error('Failed to
|
|
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('
|
|
692
|
-
document.getElementById('
|
|
693
|
-
document.getElementById('
|
|
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
|
|
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
|
-
|
|
702
|
-
refreshStats();
|
|
703
|
-
|
|
704
|
-
// Refresh periodically
|
|
893
|
+
refreshAll();
|
|
705
894
|
setInterval(refreshStats, 30000);
|
|
706
895
|
</script>
|
|
707
896
|
</body>
|