@ariso-ai/ivan 1.0.31 → 1.0.33
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/dist/database/migration.d.ts +2 -2
- package/dist/database/migration.d.ts.map +1 -1
- package/dist/database/migration.js.map +1 -1
- package/dist/database/migrations/017_create_pr_reviews_table.d.ts +3 -0
- package/dist/database/migrations/017_create_pr_reviews_table.d.ts.map +1 -0
- package/dist/database/migrations/017_create_pr_reviews_table.js +23 -0
- package/dist/database/migrations/017_create_pr_reviews_table.js.map +1 -0
- package/dist/database/migrations/index.d.ts.map +1 -1
- package/dist/database/migrations/index.js +3 -1
- package/dist/database/migrations/index.js.map +1 -1
- package/dist/database/types.d.ts +13 -0
- package/dist/database/types.d.ts.map +1 -1
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -1
- package/dist/learnings/database.d.ts.map +1 -1
- package/dist/learnings/database.js.map +1 -1
- package/dist/services/address-executor.d.ts.map +1 -1
- package/dist/services/address-executor.js +3 -2
- package/dist/services/address-executor.js.map +1 -1
- package/dist/services/git-interfaces.d.ts +3 -3
- package/dist/services/git-interfaces.d.ts.map +1 -1
- package/dist/services/pr-service-cli.d.ts +3 -3
- package/dist/services/pr-service-cli.d.ts.map +1 -1
- package/dist/services/pr-service-cli.js +7 -7
- package/dist/services/pr-service-cli.js.map +1 -1
- package/dist/services/pr-service-pat.d.ts +3 -3
- package/dist/services/pr-service-pat.d.ts.map +1 -1
- package/dist/services/pr-service-pat.js +7 -7
- package/dist/services/pr-service-pat.js.map +1 -1
- package/dist/services/review-executor.d.ts +2 -1
- package/dist/services/review-executor.d.ts.map +1 -1
- package/dist/services/review-executor.js +135 -114
- package/dist/services/review-executor.js.map +1 -1
- package/dist/web-server.d.ts +1 -0
- package/dist/web-server.d.ts.map +1 -1
- package/dist/web-server.js +393 -200
- package/dist/web-server.js.map +1 -1
- package/package.json +1 -1
package/dist/web-server.js
CHANGED
|
@@ -20,6 +20,7 @@ export class WebServer {
|
|
|
20
20
|
// API Routes
|
|
21
21
|
this.app.get('/api/jobs', this.getJobs.bind(this));
|
|
22
22
|
this.app.get('/api/jobs/:jobId/tasks', this.getJobTasks.bind(this));
|
|
23
|
+
this.app.get('/api/jobs/:jobId/reviews', this.getJobReviews.bind(this));
|
|
23
24
|
// Serve the main HTML page for all non-API routes
|
|
24
25
|
this.app.get('/', (req, res) => {
|
|
25
26
|
res.send(this.getMainHTML());
|
|
@@ -65,6 +66,31 @@ export class WebServer {
|
|
|
65
66
|
res.status(500).json({ error: 'Failed to fetch job tasks' });
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
async getJobReviews(req, res) {
|
|
70
|
+
try {
|
|
71
|
+
const { jobId } = req.params;
|
|
72
|
+
const db = this.dbManager.getKysely();
|
|
73
|
+
const job = await db
|
|
74
|
+
.selectFrom('jobs')
|
|
75
|
+
.selectAll()
|
|
76
|
+
.where('uuid', '=', jobId)
|
|
77
|
+
.executeTakeFirst();
|
|
78
|
+
if (!job) {
|
|
79
|
+
res.status(404).json({ error: 'Job not found' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const reviews = await db
|
|
83
|
+
.selectFrom('pr_reviews')
|
|
84
|
+
.selectAll()
|
|
85
|
+
.where('job_uuid', '=', jobId)
|
|
86
|
+
.execute();
|
|
87
|
+
res.json({ job, reviews });
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.error('Error fetching job reviews:', error);
|
|
91
|
+
res.status(500).json({ error: 'Failed to fetch job reviews' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
68
94
|
getMainHTML() {
|
|
69
95
|
return `<!DOCTYPE html>
|
|
70
96
|
<html lang="en">
|
|
@@ -72,6 +98,7 @@ export class WebServer {
|
|
|
72
98
|
<meta charset="UTF-8">
|
|
73
99
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
74
100
|
<title>Ivan - Job Dashboard</title>
|
|
101
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
75
102
|
<style>
|
|
76
103
|
* {
|
|
77
104
|
margin: 0;
|
|
@@ -79,265 +106,378 @@ export class WebServer {
|
|
|
79
106
|
box-sizing: border-box;
|
|
80
107
|
}
|
|
81
108
|
|
|
109
|
+
html, body {
|
|
110
|
+
height: 100%;
|
|
111
|
+
}
|
|
112
|
+
|
|
82
113
|
body {
|
|
83
114
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
84
115
|
line-height: 1.6;
|
|
85
116
|
color: #333;
|
|
86
117
|
background: #f5f7fa;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.container {
|
|
90
|
-
max-width: 1200px;
|
|
91
|
-
margin: 0 auto;
|
|
92
|
-
padding: 20px;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.header {
|
|
96
|
-
background: white;
|
|
97
|
-
padding: 20px 30px;
|
|
98
|
-
border-radius: 12px;
|
|
99
|
-
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
|
100
|
-
margin-bottom: 30px;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.header h1 {
|
|
104
|
-
color: #2c3e50;
|
|
105
|
-
font-size: 2rem;
|
|
106
|
-
margin-bottom: 10px;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.header p {
|
|
110
|
-
color: #7f8c8d;
|
|
111
|
-
font-size: 1.1rem;
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
.main-content {
|
|
115
123
|
display: flex;
|
|
116
|
-
|
|
124
|
+
flex: 1;
|
|
125
|
+
min-height: 0;
|
|
126
|
+
gap: 0;
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
.jobs-list {
|
|
120
|
-
|
|
130
|
+
width: 300px;
|
|
131
|
+
flex-shrink: 0;
|
|
121
132
|
background: white;
|
|
122
|
-
border-
|
|
123
|
-
|
|
133
|
+
border-right: 1px solid #e1e4e8;
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-direction: column;
|
|
124
136
|
overflow: hidden;
|
|
125
137
|
}
|
|
126
138
|
|
|
139
|
+
.jobs-scroll {
|
|
140
|
+
flex: 1;
|
|
141
|
+
overflow-y: auto;
|
|
142
|
+
}
|
|
143
|
+
|
|
127
144
|
.job-detail {
|
|
128
|
-
flex:
|
|
145
|
+
flex: 1;
|
|
146
|
+
min-width: 0;
|
|
129
147
|
background: white;
|
|
130
|
-
border-radius: 12px;
|
|
131
|
-
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
|
132
|
-
overflow: hidden;
|
|
133
148
|
display: none;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.job-detail.visible {
|
|
154
|
+
display: flex;
|
|
134
155
|
}
|
|
135
156
|
|
|
136
157
|
.section-header {
|
|
137
|
-
background: #
|
|
158
|
+
background: #24292f;
|
|
138
159
|
color: white;
|
|
139
|
-
padding: 20px
|
|
140
|
-
font-size:
|
|
160
|
+
padding: 14px 20px;
|
|
161
|
+
font-size: 0.95rem;
|
|
141
162
|
font-weight: 600;
|
|
163
|
+
flex-shrink: 0;
|
|
142
164
|
}
|
|
143
165
|
|
|
144
166
|
.job-item {
|
|
145
|
-
padding: 20px
|
|
146
|
-
border-bottom: 1px solid #
|
|
167
|
+
padding: 14px 20px;
|
|
168
|
+
border-bottom: 1px solid #f0f0f0;
|
|
147
169
|
cursor: pointer;
|
|
148
|
-
transition: background 0.
|
|
170
|
+
transition: background 0.15s ease;
|
|
149
171
|
}
|
|
150
172
|
|
|
151
|
-
.job-item:hover {
|
|
152
|
-
background: #f8f9fa;
|
|
153
|
-
}
|
|
173
|
+
.job-item:hover { background: #f6f8fa; }
|
|
154
174
|
|
|
155
175
|
.job-item.active {
|
|
156
|
-
background: #
|
|
157
|
-
border-left:
|
|
176
|
+
background: #dbeafe;
|
|
177
|
+
border-left: 3px solid #3b82f6;
|
|
158
178
|
}
|
|
159
179
|
|
|
160
|
-
.job-item:last-child {
|
|
161
|
-
border-bottom: none;
|
|
162
|
-
}
|
|
180
|
+
.job-item:last-child { border-bottom: none; }
|
|
163
181
|
|
|
164
182
|
.job-title {
|
|
165
183
|
font-weight: 600;
|
|
166
|
-
|
|
167
|
-
|
|
184
|
+
font-size: 0.875rem;
|
|
185
|
+
color: #24292f;
|
|
186
|
+
margin-bottom: 3px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.job-badge {
|
|
190
|
+
display: inline-block;
|
|
191
|
+
font-size: 0.65rem;
|
|
192
|
+
padding: 1px 5px;
|
|
193
|
+
border-radius: 6px;
|
|
194
|
+
margin-left: 5px;
|
|
195
|
+
vertical-align: middle;
|
|
196
|
+
background: #7c3aed;
|
|
197
|
+
color: white;
|
|
198
|
+
font-weight: 600;
|
|
199
|
+
letter-spacing: 0.02em;
|
|
168
200
|
}
|
|
169
201
|
|
|
170
202
|
.job-meta {
|
|
171
|
-
font-size: 0.
|
|
172
|
-
color: #
|
|
203
|
+
font-size: 0.75rem;
|
|
204
|
+
color: #6e7781;
|
|
173
205
|
}
|
|
174
206
|
|
|
175
207
|
.job-detail-content {
|
|
176
208
|
display: flex;
|
|
177
|
-
|
|
209
|
+
flex: 1;
|
|
210
|
+
min-height: 0;
|
|
178
211
|
}
|
|
179
212
|
|
|
180
213
|
.tasks-sidebar {
|
|
181
|
-
width:
|
|
182
|
-
|
|
214
|
+
width: 300px;
|
|
215
|
+
flex-shrink: 0;
|
|
216
|
+
border-right: 1px solid #e1e4e8;
|
|
217
|
+
display: flex;
|
|
218
|
+
flex-direction: column;
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.sidebar-tabs {
|
|
223
|
+
display: flex;
|
|
224
|
+
border-bottom: 1px solid #e1e4e8;
|
|
225
|
+
flex-shrink: 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.sidebar-tab {
|
|
229
|
+
flex: 1;
|
|
230
|
+
padding: 10px;
|
|
231
|
+
text-align: center;
|
|
232
|
+
cursor: pointer;
|
|
233
|
+
font-size: 0.8rem;
|
|
234
|
+
font-weight: 500;
|
|
235
|
+
color: #6e7781;
|
|
236
|
+
border-bottom: 2px solid transparent;
|
|
237
|
+
margin-bottom: -1px;
|
|
238
|
+
transition: color 0.15s;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.sidebar-tab:hover { color: #24292f; }
|
|
242
|
+
|
|
243
|
+
.sidebar-tab.active {
|
|
244
|
+
color: #0969da;
|
|
245
|
+
border-bottom-color: #0969da;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.sidebar-scroll {
|
|
249
|
+
flex: 1;
|
|
183
250
|
overflow-y: auto;
|
|
184
251
|
}
|
|
185
252
|
|
|
186
253
|
.task-content {
|
|
187
254
|
flex: 1;
|
|
255
|
+
min-width: 0;
|
|
188
256
|
overflow-y: auto;
|
|
189
|
-
padding: 30px;
|
|
257
|
+
padding: 30px 40px;
|
|
190
258
|
}
|
|
191
259
|
|
|
192
|
-
.task-item {
|
|
193
|
-
padding:
|
|
194
|
-
border-bottom: 1px solid #
|
|
260
|
+
.task-item, .review-item {
|
|
261
|
+
padding: 12px 16px;
|
|
262
|
+
border-bottom: 1px solid #f0f0f0;
|
|
195
263
|
cursor: pointer;
|
|
196
|
-
transition: background 0.
|
|
264
|
+
transition: background 0.15s ease;
|
|
197
265
|
}
|
|
198
266
|
|
|
199
|
-
.task-item:hover {
|
|
200
|
-
background: #f8f9fa;
|
|
201
|
-
}
|
|
267
|
+
.task-item:hover, .review-item:hover { background: #f6f8fa; }
|
|
202
268
|
|
|
203
269
|
.task-item.active {
|
|
204
|
-
background: #
|
|
205
|
-
border-left:
|
|
270
|
+
background: #dcfce7;
|
|
271
|
+
border-left: 3px solid #16a34a;
|
|
206
272
|
}
|
|
207
273
|
|
|
208
|
-
.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
margin-bottom: 5px;
|
|
274
|
+
.review-item.active {
|
|
275
|
+
background: #ede9fe;
|
|
276
|
+
border-left: 3px solid #7c3aed;
|
|
212
277
|
}
|
|
213
278
|
|
|
214
|
-
.task-
|
|
215
|
-
font-size: 0.8rem;
|
|
216
|
-
padding: 2px 8px;
|
|
217
|
-
border-radius: 12px;
|
|
279
|
+
.task-title, .review-title {
|
|
218
280
|
font-weight: 500;
|
|
281
|
+
font-size: 0.85rem;
|
|
282
|
+
color: #24292f;
|
|
283
|
+
margin-bottom: 4px;
|
|
219
284
|
}
|
|
220
285
|
|
|
221
|
-
.status
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
background: #74b9ff;
|
|
228
|
-
color: #0984e3;
|
|
286
|
+
.task-status {
|
|
287
|
+
display: inline-block;
|
|
288
|
+
font-size: 0.7rem;
|
|
289
|
+
padding: 1px 7px;
|
|
290
|
+
border-radius: 10px;
|
|
291
|
+
font-weight: 600;
|
|
229
292
|
}
|
|
230
293
|
|
|
231
|
-
.status-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
294
|
+
.status-not_started { background: #fef9c3; color: #854d0e; }
|
|
295
|
+
.status-active { background: #dbeafe; color: #1d4ed8; }
|
|
296
|
+
.status-completed { background: #dcfce7; color: #15803d; }
|
|
297
|
+
.status-failed { background: #fee2e2; color: #b91c1c; }
|
|
235
298
|
|
|
236
|
-
.
|
|
237
|
-
border-bottom: 1px solid #ecf0f1;
|
|
299
|
+
.detail-header {
|
|
238
300
|
padding-bottom: 20px;
|
|
239
|
-
margin-bottom:
|
|
301
|
+
margin-bottom: 24px;
|
|
302
|
+
border-bottom: 1px solid #e1e4e8;
|
|
240
303
|
}
|
|
241
304
|
|
|
242
|
-
.
|
|
243
|
-
font-size: 1.
|
|
244
|
-
|
|
245
|
-
|
|
305
|
+
.detail-title {
|
|
306
|
+
font-size: 1.25rem;
|
|
307
|
+
font-weight: 600;
|
|
308
|
+
color: #24292f;
|
|
309
|
+
margin-bottom: 8px;
|
|
246
310
|
}
|
|
247
311
|
|
|
248
312
|
.pr-link {
|
|
249
313
|
display: inline-block;
|
|
250
|
-
background: #
|
|
314
|
+
background: #0969da;
|
|
251
315
|
color: white;
|
|
252
|
-
padding:
|
|
316
|
+
padding: 6px 14px;
|
|
253
317
|
border-radius: 6px;
|
|
254
318
|
text-decoration: none;
|
|
255
|
-
font-size: 0.
|
|
256
|
-
margin-top:
|
|
319
|
+
font-size: 0.85rem;
|
|
320
|
+
margin-top: 8px;
|
|
321
|
+
font-weight: 500;
|
|
257
322
|
}
|
|
258
323
|
|
|
259
|
-
.pr-link:hover {
|
|
260
|
-
background: #2980b9;
|
|
261
|
-
}
|
|
324
|
+
.pr-link:hover { background: #0860ca; }
|
|
262
325
|
|
|
263
326
|
.execution-log {
|
|
264
|
-
background: #
|
|
265
|
-
color: #
|
|
327
|
+
background: #161b22;
|
|
328
|
+
color: #c9d1d9;
|
|
266
329
|
padding: 20px;
|
|
267
330
|
border-radius: 8px;
|
|
268
|
-
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
269
|
-
font-size: 0.
|
|
270
|
-
line-height: 1.
|
|
331
|
+
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
332
|
+
font-size: 0.825rem;
|
|
333
|
+
line-height: 1.5;
|
|
271
334
|
white-space: pre-wrap;
|
|
272
335
|
overflow-x: auto;
|
|
273
|
-
max-height: 500px;
|
|
274
|
-
overflow-y: auto;
|
|
275
336
|
}
|
|
276
337
|
|
|
338
|
+
/* Markdown-rendered review output */
|
|
339
|
+
.review-markdown {
|
|
340
|
+
font-size: 0.95rem;
|
|
341
|
+
line-height: 1.7;
|
|
342
|
+
color: #24292f;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.review-markdown h1,
|
|
346
|
+
.review-markdown h2,
|
|
347
|
+
.review-markdown h3,
|
|
348
|
+
.review-markdown h4 {
|
|
349
|
+
margin-top: 1.5em;
|
|
350
|
+
margin-bottom: 0.5em;
|
|
351
|
+
font-weight: 600;
|
|
352
|
+
color: #24292f;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.review-markdown h1 { font-size: 1.4rem; border-bottom: 1px solid #e1e4e8; padding-bottom: 0.3em; }
|
|
356
|
+
.review-markdown h2 { font-size: 1.15rem; border-bottom: 1px solid #e1e4e8; padding-bottom: 0.2em; }
|
|
357
|
+
.review-markdown h3 { font-size: 1rem; }
|
|
358
|
+
.review-markdown h4 { font-size: 0.9rem; }
|
|
359
|
+
|
|
360
|
+
.review-markdown p { margin-bottom: 1em; }
|
|
361
|
+
|
|
362
|
+
.review-markdown ul, .review-markdown ol {
|
|
363
|
+
margin: 0.5em 0 1em 1.5em;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.review-markdown li { margin-bottom: 0.25em; }
|
|
367
|
+
|
|
368
|
+
.review-markdown code {
|
|
369
|
+
background: #f6f8fa;
|
|
370
|
+
border: 1px solid #e1e4e8;
|
|
371
|
+
border-radius: 4px;
|
|
372
|
+
padding: 0.1em 0.4em;
|
|
373
|
+
font-family: 'SF Mono', 'Monaco', 'Menlo', monospace;
|
|
374
|
+
font-size: 0.85em;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.review-markdown pre {
|
|
378
|
+
background: #161b22;
|
|
379
|
+
border-radius: 8px;
|
|
380
|
+
padding: 16px;
|
|
381
|
+
overflow-x: auto;
|
|
382
|
+
margin: 1em 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.review-markdown pre code {
|
|
386
|
+
background: none;
|
|
387
|
+
border: none;
|
|
388
|
+
padding: 0;
|
|
389
|
+
color: #c9d1d9;
|
|
390
|
+
font-size: 0.85rem;
|
|
391
|
+
line-height: 1.5;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.review-markdown blockquote {
|
|
395
|
+
border-left: 4px solid #d0d7de;
|
|
396
|
+
padding-left: 1em;
|
|
397
|
+
color: #6e7781;
|
|
398
|
+
margin: 1em 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.review-markdown hr {
|
|
402
|
+
border: none;
|
|
403
|
+
border-top: 1px solid #e1e4e8;
|
|
404
|
+
margin: 1.5em 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.review-markdown strong { font-weight: 600; }
|
|
408
|
+
|
|
409
|
+
.review-markdown a { color: #0969da; text-decoration: none; }
|
|
410
|
+
.review-markdown a:hover { text-decoration: underline; }
|
|
411
|
+
|
|
277
412
|
.empty-state {
|
|
278
413
|
padding: 60px 30px;
|
|
279
414
|
text-align: center;
|
|
280
|
-
color: #
|
|
415
|
+
color: #6e7781;
|
|
281
416
|
}
|
|
282
417
|
|
|
283
418
|
.empty-state h3 {
|
|
284
|
-
margin-bottom:
|
|
285
|
-
color: #
|
|
419
|
+
margin-bottom: 8px;
|
|
420
|
+
color: #8c959f;
|
|
421
|
+
font-weight: 500;
|
|
286
422
|
}
|
|
287
423
|
|
|
288
424
|
.loading {
|
|
289
|
-
padding:
|
|
425
|
+
padding: 40px 20px;
|
|
290
426
|
text-align: center;
|
|
291
|
-
color: #
|
|
427
|
+
color: #6e7781;
|
|
428
|
+
font-size: 0.875rem;
|
|
292
429
|
}
|
|
293
430
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
431
|
+
.section-label {
|
|
432
|
+
font-size: 0.75rem;
|
|
433
|
+
font-weight: 600;
|
|
434
|
+
color: #6e7781;
|
|
435
|
+
text-transform: uppercase;
|
|
436
|
+
letter-spacing: 0.05em;
|
|
437
|
+
margin-bottom: 12px;
|
|
438
|
+
}
|
|
303
439
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
440
|
+
@media (max-width: 900px) {
|
|
441
|
+
.main-content { flex-direction: column; }
|
|
442
|
+
.jobs-list { width: 100%; border-right: none; border-bottom: 1px solid #e1e4e8; max-height: 250px; }
|
|
443
|
+
.tasks-sidebar { width: 100%; border-right: none; border-bottom: 1px solid #e1e4e8; max-height: 220px; }
|
|
444
|
+
.job-detail-content { flex-direction: column; }
|
|
308
445
|
}
|
|
309
446
|
</style>
|
|
310
447
|
</head>
|
|
311
448
|
<body>
|
|
312
|
-
<div class="
|
|
313
|
-
<div class="
|
|
314
|
-
<div class="
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
</div>
|
|
318
|
-
<div id="jobs-container">
|
|
319
|
-
<div class="loading">Loading jobs...</div>
|
|
320
|
-
</div>
|
|
449
|
+
<div class="main-content">
|
|
450
|
+
<div class="jobs-list">
|
|
451
|
+
<div class="section-header">Jobs</div>
|
|
452
|
+
<div class="jobs-scroll" id="jobs-container">
|
|
453
|
+
<div class="loading">Loading jobs...</div>
|
|
321
454
|
</div>
|
|
455
|
+
</div>
|
|
322
456
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
457
|
+
<div class="job-detail" id="job-detail">
|
|
458
|
+
<div class="section-header">
|
|
459
|
+
<span id="job-detail-title">Job Details</span>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="job-detail-content">
|
|
462
|
+
<div class="tasks-sidebar">
|
|
463
|
+
<div class="sidebar-tabs">
|
|
464
|
+
<div class="sidebar-tab active" id="tab-tasks" onclick="switchTab('tasks')">Tasks</div>
|
|
465
|
+
<div class="sidebar-tab" id="tab-reviews" onclick="switchTab('reviews')">Reviews</div>
|
|
466
|
+
</div>
|
|
467
|
+
<div class="sidebar-scroll">
|
|
329
468
|
<div id="tasks-container">
|
|
330
|
-
<div class="empty-state">
|
|
331
|
-
|
|
332
|
-
|
|
469
|
+
<div class="empty-state"><h3>Select a job</h3></div>
|
|
470
|
+
</div>
|
|
471
|
+
<div id="reviews-container" style="display:none;">
|
|
472
|
+
<div class="empty-state"><h3>Select a job</h3></div>
|
|
333
473
|
</div>
|
|
334
474
|
</div>
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
475
|
+
</div>
|
|
476
|
+
<div class="task-content">
|
|
477
|
+
<div id="detail-container">
|
|
478
|
+
<div class="empty-state">
|
|
479
|
+
<h3>Select an item to view details</h3>
|
|
480
|
+
<p>Choose a task or review from the sidebar.</p>
|
|
341
481
|
</div>
|
|
342
482
|
</div>
|
|
343
483
|
</div>
|
|
@@ -349,8 +489,11 @@ export class WebServer {
|
|
|
349
489
|
let jobs = [];
|
|
350
490
|
let currentJob = null;
|
|
351
491
|
let currentTasks = [];
|
|
492
|
+
let currentReviews = [];
|
|
493
|
+
let activeTab = 'tasks';
|
|
494
|
+
|
|
495
|
+
marked.setOptions({ breaks: true, gfm: true });
|
|
352
496
|
|
|
353
|
-
// Load jobs on page load
|
|
354
497
|
loadJobs();
|
|
355
498
|
|
|
356
499
|
async function loadJobs() {
|
|
@@ -365,93 +508,143 @@ export class WebServer {
|
|
|
365
508
|
}
|
|
366
509
|
}
|
|
367
510
|
|
|
511
|
+
function esc(str) {
|
|
512
|
+
return String(str)
|
|
513
|
+
.replace(/&/g, '&')
|
|
514
|
+
.replace(/</g, '<')
|
|
515
|
+
.replace(/>/g, '>')
|
|
516
|
+
.replace(/"/g, '"');
|
|
517
|
+
}
|
|
518
|
+
|
|
368
519
|
function renderJobs() {
|
|
369
520
|
const container = document.getElementById('jobs-container');
|
|
370
|
-
|
|
371
521
|
if (jobs.length === 0) {
|
|
372
|
-
container.innerHTML = '<div class="empty-state"><h3>No jobs
|
|
522
|
+
container.innerHTML = '<div class="empty-state"><h3>No jobs yet</h3><p>Run some Ivan commands to see jobs here.</p></div>';
|
|
373
523
|
return;
|
|
374
524
|
}
|
|
375
|
-
|
|
376
525
|
container.innerHTML = jobs.map(job => {
|
|
377
526
|
const date = new Date(job.created_at).toLocaleString();
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
</div>
|
|
385
|
-
</div>
|
|
386
|
-
\`;
|
|
527
|
+
const isReview = job.description.startsWith('PR Review -');
|
|
528
|
+
const badge = isReview ? '<span class="job-badge">review</span>' : '';
|
|
529
|
+
return \`<div class="job-item" onclick="selectJob('\${esc(job.uuid)}', event)">
|
|
530
|
+
<div class="job-title">\${esc(job.description)}\${badge}</div>
|
|
531
|
+
<div class="job-meta">\${date}</div>
|
|
532
|
+
</div>\`;
|
|
387
533
|
}).join('');
|
|
388
534
|
}
|
|
389
535
|
|
|
390
|
-
async function selectJob(jobId) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
event.target.closest('.job-item').classList.add('active');
|
|
536
|
+
async function selectJob(jobId, evt) {
|
|
537
|
+
document.querySelectorAll('.job-item').forEach(el => el.classList.remove('active'));
|
|
538
|
+
evt.target.closest('.job-item').classList.add('active');
|
|
394
539
|
|
|
395
540
|
try {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
document.getElementById('
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
541
|
+
const [tasksResp, reviewsResp] = await Promise.all([
|
|
542
|
+
fetch(\`/api/jobs/\${jobId}/tasks\`),
|
|
543
|
+
fetch(\`/api/jobs/\${jobId}/reviews\`)
|
|
544
|
+
]);
|
|
545
|
+
const tasksData = await tasksResp.json();
|
|
546
|
+
const reviewsData = await reviewsResp.json();
|
|
547
|
+
|
|
548
|
+
currentJob = tasksData.job;
|
|
549
|
+
currentTasks = tasksData.tasks;
|
|
550
|
+
currentReviews = reviewsData.reviews || [];
|
|
551
|
+
|
|
552
|
+
document.getElementById('job-detail-title').textContent = currentJob.description;
|
|
553
|
+
document.getElementById('job-detail').classList.add('visible');
|
|
554
|
+
|
|
555
|
+
if (currentReviews.length > 0 && currentTasks.length === 0) {
|
|
556
|
+
switchTab('reviews');
|
|
557
|
+
} else {
|
|
558
|
+
switchTab('tasks');
|
|
559
|
+
}
|
|
413
560
|
|
|
561
|
+
document.getElementById('detail-container').innerHTML =
|
|
562
|
+
'<div class="empty-state"><h3>Select an item to view details</h3><p>Choose a task or review from the sidebar.</p></div>';
|
|
414
563
|
} catch (error) {
|
|
415
|
-
console.error('Failed to load job
|
|
564
|
+
console.error('Failed to load job data:', error);
|
|
416
565
|
}
|
|
417
566
|
}
|
|
418
567
|
|
|
568
|
+
function switchTab(tab) {
|
|
569
|
+
activeTab = tab;
|
|
570
|
+
document.getElementById('tab-tasks').classList.toggle('active', tab === 'tasks');
|
|
571
|
+
document.getElementById('tab-reviews').classList.toggle('active', tab === 'reviews');
|
|
572
|
+
document.getElementById('tasks-container').style.display = tab === 'tasks' ? '' : 'none';
|
|
573
|
+
document.getElementById('reviews-container').style.display = tab === 'reviews' ? '' : 'none';
|
|
574
|
+
if (tab === 'tasks') renderTasks();
|
|
575
|
+
else renderReviews();
|
|
576
|
+
}
|
|
577
|
+
|
|
419
578
|
function renderTasks() {
|
|
420
579
|
const container = document.getElementById('tasks-container');
|
|
421
|
-
|
|
422
580
|
if (currentTasks.length === 0) {
|
|
423
|
-
container.innerHTML = '<div class="empty-state"><h3>No tasks
|
|
581
|
+
container.innerHTML = '<div class="empty-state"><h3>No tasks</h3></div>';
|
|
424
582
|
return;
|
|
425
583
|
}
|
|
426
|
-
|
|
427
584
|
container.innerHTML = currentTasks.map(task => \`
|
|
428
|
-
<div class="task-item" onclick="selectTask('\${task.uuid}')">
|
|
429
|
-
<div class="task-title">\${task.description}</div>
|
|
430
|
-
<
|
|
585
|
+
<div class="task-item" onclick="selectTask('\${esc(task.uuid)}')">
|
|
586
|
+
<div class="task-title">\${esc(task.description)}</div>
|
|
587
|
+
<span class="task-status status-\${task.status}">\${task.status.replace('_', ' ')}</span>
|
|
588
|
+
</div>
|
|
589
|
+
\`).join('');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function renderReviews() {
|
|
593
|
+
const container = document.getElementById('reviews-container');
|
|
594
|
+
if (currentReviews.length === 0) {
|
|
595
|
+
container.innerHTML = '<div class="empty-state"><h3>No reviews</h3></div>';
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
container.innerHTML = currentReviews.map(review => \`
|
|
599
|
+
<div class="review-item" onclick="selectReview('\${esc(review.uuid)}')">
|
|
600
|
+
<div class="review-title">PR #\${review.pr_number}\${review.pr_title ? ': ' + esc(review.pr_title) : ''}</div>
|
|
601
|
+
<span class="task-status status-\${review.status}">\${review.status.replace('_', ' ')}</span>
|
|
431
602
|
</div>
|
|
432
603
|
\`).join('');
|
|
433
604
|
}
|
|
434
605
|
|
|
435
606
|
function selectTask(taskId) {
|
|
436
|
-
|
|
437
|
-
document.querySelectorAll('.task-item').forEach(item => item.classList.remove('active'));
|
|
607
|
+
document.querySelectorAll('.task-item').forEach(el => el.classList.remove('active'));
|
|
438
608
|
event.target.closest('.task-item').classList.add('active');
|
|
439
609
|
|
|
440
610
|
const task = currentTasks.find(t => t.uuid === taskId);
|
|
441
611
|
if (!task) return;
|
|
442
612
|
|
|
443
|
-
|
|
613
|
+
document.getElementById('detail-container').innerHTML = \`
|
|
614
|
+
<div class="detail-header">
|
|
615
|
+
<div class="detail-title">\${esc(task.description)}</div>
|
|
616
|
+
<span class="task-status status-\${task.status}">\${task.status.replace('_', ' ')}</span>
|
|
617
|
+
\${task.pr_link ? \`<br><a href="\${esc(task.pr_link)}" target="_blank" class="pr-link">View Pull Request</a>\` : ''}
|
|
618
|
+
</div>
|
|
619
|
+
\${task.execution_log
|
|
620
|
+
? \`<div class="section-label">Execution Log</div><div class="execution-log">\${esc(task.execution_log)}</div>\`
|
|
621
|
+
: '<div class="empty-state"><h3>No execution log</h3><p>This task has not run yet.</p></div>'}
|
|
622
|
+
\`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function selectReview(reviewId) {
|
|
626
|
+
document.querySelectorAll('.review-item').forEach(el => el.classList.remove('active'));
|
|
627
|
+
event.target.closest('.review-item').classList.add('active');
|
|
628
|
+
|
|
629
|
+
const review = currentReviews.find(r => r.uuid === reviewId);
|
|
630
|
+
if (!review) return;
|
|
631
|
+
|
|
632
|
+
let bodyHtml;
|
|
633
|
+
if (review.review_output) {
|
|
634
|
+
bodyHtml = \`<div class="section-label">Review</div><div class="review-markdown">\${marked.parse(review.review_output)}</div>\`;
|
|
635
|
+
} else if (review.review_log) {
|
|
636
|
+
bodyHtml = \`<div class="section-label">Log</div><div class="execution-log">\${esc(review.review_log)}</div>\`;
|
|
637
|
+
} else {
|
|
638
|
+
bodyHtml = '<div class="empty-state"><h3>No review output yet</h3><p>The review is still running or has not started.</p></div>';
|
|
639
|
+
}
|
|
444
640
|
|
|
445
|
-
container.innerHTML = \`
|
|
446
|
-
<div class="
|
|
447
|
-
<div class="
|
|
448
|
-
<
|
|
449
|
-
\${
|
|
641
|
+
document.getElementById('detail-container').innerHTML = \`
|
|
642
|
+
<div class="detail-header">
|
|
643
|
+
<div class="detail-title">PR #\${review.pr_number}\${review.pr_title ? ': ' + esc(review.pr_title) : ''}</div>
|
|
644
|
+
<span class="task-status status-\${review.status}">\${review.status.replace('_', ' ')}</span>
|
|
645
|
+
\${review.pr_url ? \`<br><a href="\${esc(review.pr_url)}" target="_blank" class="pr-link">View PR on GitHub</a>\` : ''}
|
|
450
646
|
</div>
|
|
451
|
-
\${
|
|
452
|
-
<h3 style="margin-bottom: 15px; color: #2c3e50;">Execution Log</h3>
|
|
453
|
-
<div class="execution-log">\${task.execution_log}</div>
|
|
454
|
-
\` : '<div class="empty-state"><h3>No execution log</h3><p>This task hasn\\'t been executed yet or no log was recorded.</p></div>'}
|
|
647
|
+
\${bodyHtml}
|
|
455
648
|
\`;
|
|
456
649
|
}
|
|
457
650
|
</script>
|