pm-orchestrator-runner 1.0.12 → 1.0.13
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/web/public/index.html +951 -0
- package/package.json +1 -1
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ja">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>PM Orchestrator Runner - Web UI</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
16
|
+
background: #f5f5f5;
|
|
17
|
+
color: #333;
|
|
18
|
+
line-height: 1.6;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.container {
|
|
22
|
+
max-width: 1200px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
padding: 20px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
header {
|
|
28
|
+
background: #2563eb;
|
|
29
|
+
color: white;
|
|
30
|
+
padding: 16px 20px;
|
|
31
|
+
position: sticky;
|
|
32
|
+
top: 0;
|
|
33
|
+
z-index: 100;
|
|
34
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.header-content {
|
|
38
|
+
max-width: 1200px;
|
|
39
|
+
margin: 0 auto;
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
align-items: center;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
header h1 {
|
|
46
|
+
font-size: 1.3rem;
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
nav {
|
|
52
|
+
display: flex;
|
|
53
|
+
gap: 16px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
nav a {
|
|
57
|
+
color: rgba(255, 255, 255, 0.9);
|
|
58
|
+
text-decoration: none;
|
|
59
|
+
font-size: 0.9rem;
|
|
60
|
+
padding: 8px 16px;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
transition: background 0.2s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
nav a:hover {
|
|
66
|
+
background: rgba(255, 255, 255, 0.1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
nav a.active {
|
|
70
|
+
background: rgba(255, 255, 255, 0.2);
|
|
71
|
+
color: white;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.page-header {
|
|
75
|
+
display: flex;
|
|
76
|
+
justify-content: space-between;
|
|
77
|
+
align-items: center;
|
|
78
|
+
margin-bottom: 20px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.page-header h2 {
|
|
82
|
+
font-size: 1.5rem;
|
|
83
|
+
color: #1f2937;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.back-btn {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 6px;
|
|
90
|
+
padding: 8px 16px;
|
|
91
|
+
background: #e5e7eb;
|
|
92
|
+
color: #374151;
|
|
93
|
+
border: none;
|
|
94
|
+
border-radius: 6px;
|
|
95
|
+
font-size: 0.9rem;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
text-decoration: none;
|
|
98
|
+
transition: background 0.2s;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.back-btn:hover {
|
|
102
|
+
background: #d1d5db;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.card {
|
|
106
|
+
background: white;
|
|
107
|
+
border-radius: 8px;
|
|
108
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
109
|
+
padding: 20px;
|
|
110
|
+
margin-bottom: 16px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.card h3 {
|
|
114
|
+
font-size: 1.1rem;
|
|
115
|
+
margin-bottom: 16px;
|
|
116
|
+
color: #1f2937;
|
|
117
|
+
border-bottom: 1px solid #e5e7eb;
|
|
118
|
+
padding-bottom: 8px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.list-item {
|
|
122
|
+
display: flex;
|
|
123
|
+
justify-content: space-between;
|
|
124
|
+
align-items: center;
|
|
125
|
+
padding: 12px 16px;
|
|
126
|
+
border: 1px solid #e5e7eb;
|
|
127
|
+
border-radius: 6px;
|
|
128
|
+
margin-bottom: 8px;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
transition: all 0.2s;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.list-item:hover {
|
|
134
|
+
background: #f9fafb;
|
|
135
|
+
border-color: #2563eb;
|
|
136
|
+
transform: translateX(4px);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.list-item-main {
|
|
140
|
+
flex: 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.list-item-title {
|
|
144
|
+
font-weight: 500;
|
|
145
|
+
color: #111827;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.list-item-meta {
|
|
149
|
+
font-size: 0.85rem;
|
|
150
|
+
color: #6b7280;
|
|
151
|
+
margin-top: 4px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.list-item-arrow {
|
|
155
|
+
color: #9ca3af;
|
|
156
|
+
font-size: 1.2rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.badge {
|
|
160
|
+
display: inline-block;
|
|
161
|
+
padding: 4px 10px;
|
|
162
|
+
border-radius: 4px;
|
|
163
|
+
font-size: 0.75rem;
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
margin-right: 8px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.badge-queued {
|
|
170
|
+
background: #fef3c7;
|
|
171
|
+
color: #92400e;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.badge-running {
|
|
175
|
+
background: #dbeafe;
|
|
176
|
+
color: #1e40af;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.badge-complete {
|
|
180
|
+
background: #d1fae5;
|
|
181
|
+
color: #065f46;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.badge-error {
|
|
185
|
+
background: #fee2e2;
|
|
186
|
+
color: #991b1b;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.form-group {
|
|
190
|
+
margin-bottom: 16px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.form-group label {
|
|
194
|
+
display: block;
|
|
195
|
+
font-weight: 500;
|
|
196
|
+
margin-bottom: 6px;
|
|
197
|
+
color: #374151;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.form-group input,
|
|
201
|
+
.form-group textarea,
|
|
202
|
+
.form-group select {
|
|
203
|
+
width: 100%;
|
|
204
|
+
padding: 10px 12px;
|
|
205
|
+
border: 1px solid #d1d5db;
|
|
206
|
+
border-radius: 6px;
|
|
207
|
+
font-size: 1rem;
|
|
208
|
+
transition: border-color 0.2s;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.form-group input:focus,
|
|
212
|
+
.form-group textarea:focus,
|
|
213
|
+
.form-group select:focus {
|
|
214
|
+
outline: none;
|
|
215
|
+
border-color: #2563eb;
|
|
216
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.form-group textarea {
|
|
220
|
+
min-height: 120px;
|
|
221
|
+
resize: vertical;
|
|
222
|
+
font-family: inherit;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.btn {
|
|
226
|
+
display: inline-block;
|
|
227
|
+
padding: 10px 20px;
|
|
228
|
+
border: none;
|
|
229
|
+
border-radius: 6px;
|
|
230
|
+
font-size: 1rem;
|
|
231
|
+
font-weight: 500;
|
|
232
|
+
cursor: pointer;
|
|
233
|
+
transition: background 0.2s;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.btn-primary {
|
|
237
|
+
background: #2563eb;
|
|
238
|
+
color: white;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.btn-primary:hover {
|
|
242
|
+
background: #1d4ed8;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.btn-primary:disabled {
|
|
246
|
+
background: #9ca3af;
|
|
247
|
+
cursor: not-allowed;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.btn-secondary {
|
|
251
|
+
background: #6b7280;
|
|
252
|
+
color: white;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.btn-secondary:hover {
|
|
256
|
+
background: #4b5563;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.detail-grid {
|
|
260
|
+
display: grid;
|
|
261
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
262
|
+
gap: 16px;
|
|
263
|
+
margin-bottom: 20px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.detail-item {
|
|
267
|
+
background: #f9fafb;
|
|
268
|
+
padding: 12px;
|
|
269
|
+
border-radius: 6px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.detail-item label {
|
|
273
|
+
display: block;
|
|
274
|
+
font-size: 0.8rem;
|
|
275
|
+
color: #6b7280;
|
|
276
|
+
text-transform: uppercase;
|
|
277
|
+
margin-bottom: 4px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.detail-item value {
|
|
281
|
+
display: block;
|
|
282
|
+
font-weight: 500;
|
|
283
|
+
color: #1f2937;
|
|
284
|
+
word-break: break-all;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.prompt-box {
|
|
288
|
+
background: #f9fafb;
|
|
289
|
+
padding: 16px;
|
|
290
|
+
border-radius: 6px;
|
|
291
|
+
font-family: 'Monaco', 'Consolas', monospace;
|
|
292
|
+
font-size: 0.9rem;
|
|
293
|
+
white-space: pre-wrap;
|
|
294
|
+
word-break: break-word;
|
|
295
|
+
border-left: 4px solid #2563eb;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.log-section {
|
|
299
|
+
margin-top: 20px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.log-entry {
|
|
303
|
+
padding: 12px;
|
|
304
|
+
border-left: 3px solid #e5e7eb;
|
|
305
|
+
margin-bottom: 8px;
|
|
306
|
+
background: #fafafa;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.log-entry.user-input {
|
|
310
|
+
border-left-color: #2563eb;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.log-entry.task-started {
|
|
314
|
+
border-left-color: #f59e0b;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.log-entry.task-completed {
|
|
318
|
+
border-left-color: #10b981;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.log-entry.error {
|
|
322
|
+
border-left-color: #ef4444;
|
|
323
|
+
background: #fef2f2;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.log-time {
|
|
327
|
+
font-size: 0.75rem;
|
|
328
|
+
color: #6b7280;
|
|
329
|
+
font-family: monospace;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.log-type {
|
|
333
|
+
font-size: 0.8rem;
|
|
334
|
+
font-weight: 600;
|
|
335
|
+
color: #374151;
|
|
336
|
+
margin: 4px 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.log-content {
|
|
340
|
+
font-size: 0.9rem;
|
|
341
|
+
color: #1f2937;
|
|
342
|
+
white-space: pre-wrap;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.empty-state {
|
|
346
|
+
text-align: center;
|
|
347
|
+
padding: 60px 20px;
|
|
348
|
+
color: #6b7280;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.empty-state p {
|
|
352
|
+
margin-bottom: 16px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.loading {
|
|
356
|
+
text-align: center;
|
|
357
|
+
padding: 60px 20px;
|
|
358
|
+
color: #6b7280;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.loading::after {
|
|
362
|
+
content: '';
|
|
363
|
+
display: inline-block;
|
|
364
|
+
width: 20px;
|
|
365
|
+
height: 20px;
|
|
366
|
+
border: 2px solid #e5e7eb;
|
|
367
|
+
border-top-color: #2563eb;
|
|
368
|
+
border-radius: 50%;
|
|
369
|
+
animation: spin 0.8s linear infinite;
|
|
370
|
+
margin-left: 10px;
|
|
371
|
+
vertical-align: middle;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
@keyframes spin {
|
|
375
|
+
to { transform: rotate(360deg); }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.error-message {
|
|
379
|
+
background: #fee2e2;
|
|
380
|
+
color: #991b1b;
|
|
381
|
+
padding: 12px;
|
|
382
|
+
border-radius: 6px;
|
|
383
|
+
margin-bottom: 16px;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.success-message {
|
|
387
|
+
background: #d1fae5;
|
|
388
|
+
color: #065f46;
|
|
389
|
+
padding: 12px;
|
|
390
|
+
border-radius: 6px;
|
|
391
|
+
margin-bottom: 16px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.refresh-btn {
|
|
395
|
+
background: none;
|
|
396
|
+
border: 1px solid #d1d5db;
|
|
397
|
+
padding: 6px 12px;
|
|
398
|
+
border-radius: 4px;
|
|
399
|
+
cursor: pointer;
|
|
400
|
+
font-size: 0.85rem;
|
|
401
|
+
color: #6b7280;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.refresh-btn:hover {
|
|
405
|
+
background: #f3f4f6;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.action-bar {
|
|
409
|
+
display: flex;
|
|
410
|
+
gap: 12px;
|
|
411
|
+
margin-bottom: 16px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
@media (max-width: 600px) {
|
|
415
|
+
.container {
|
|
416
|
+
padding: 12px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
header {
|
|
420
|
+
padding: 12px 16px;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.header-content {
|
|
424
|
+
flex-direction: column;
|
|
425
|
+
gap: 12px;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
header h1 {
|
|
429
|
+
font-size: 1.1rem;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
nav {
|
|
433
|
+
width: 100%;
|
|
434
|
+
justify-content: center;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.card {
|
|
438
|
+
padding: 16px;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.page-header {
|
|
442
|
+
flex-direction: column;
|
|
443
|
+
gap: 12px;
|
|
444
|
+
align-items: flex-start;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.detail-grid {
|
|
448
|
+
grid-template-columns: 1fr;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
</style>
|
|
452
|
+
</head>
|
|
453
|
+
<body>
|
|
454
|
+
<header>
|
|
455
|
+
<div class="header-content">
|
|
456
|
+
<h1 onclick="navigate('/')">PM Orchestrator Runner</h1>
|
|
457
|
+
<nav id="main-nav">
|
|
458
|
+
<a href="/" data-nav="home">Task Groups</a>
|
|
459
|
+
<a href="/new" data-nav="new">+ New Command</a>
|
|
460
|
+
</nav>
|
|
461
|
+
</div>
|
|
462
|
+
</header>
|
|
463
|
+
|
|
464
|
+
<main class="container">
|
|
465
|
+
<div id="app">
|
|
466
|
+
<div class="loading">Loading</div>
|
|
467
|
+
</div>
|
|
468
|
+
</main>
|
|
469
|
+
|
|
470
|
+
<script>
|
|
471
|
+
const app = document.getElementById('app');
|
|
472
|
+
let currentPath = '/';
|
|
473
|
+
|
|
474
|
+
// API helper
|
|
475
|
+
async function api(path, options = {}) {
|
|
476
|
+
const response = await fetch(`/api${path}`, {
|
|
477
|
+
headers: { 'Content-Type': 'application/json' },
|
|
478
|
+
...options,
|
|
479
|
+
});
|
|
480
|
+
const data = await response.json();
|
|
481
|
+
if (!response.ok) {
|
|
482
|
+
throw new Error(data.message || 'Request failed');
|
|
483
|
+
}
|
|
484
|
+
return data;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Format date
|
|
488
|
+
function formatDate(isoString) {
|
|
489
|
+
if (!isoString) return '-';
|
|
490
|
+
const date = new Date(isoString);
|
|
491
|
+
return date.toLocaleString('ja-JP', {
|
|
492
|
+
month: '2-digit',
|
|
493
|
+
day: '2-digit',
|
|
494
|
+
hour: '2-digit',
|
|
495
|
+
minute: '2-digit',
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Format full date
|
|
500
|
+
function formatFullDate(isoString) {
|
|
501
|
+
if (!isoString) return '-';
|
|
502
|
+
const date = new Date(isoString);
|
|
503
|
+
return date.toLocaleString('ja-JP', {
|
|
504
|
+
year: 'numeric',
|
|
505
|
+
month: '2-digit',
|
|
506
|
+
day: '2-digit',
|
|
507
|
+
hour: '2-digit',
|
|
508
|
+
minute: '2-digit',
|
|
509
|
+
second: '2-digit',
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Get status badge class
|
|
514
|
+
function getStatusBadgeClass(status) {
|
|
515
|
+
const map = {
|
|
516
|
+
'QUEUED': 'badge-queued',
|
|
517
|
+
'RUNNING': 'badge-running',
|
|
518
|
+
'COMPLETE': 'badge-complete',
|
|
519
|
+
'ERROR': 'badge-error',
|
|
520
|
+
};
|
|
521
|
+
return map[status] || 'badge-queued';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Escape HTML
|
|
525
|
+
function escapeHtml(str) {
|
|
526
|
+
if (str === null || str === undefined) return '';
|
|
527
|
+
return String(str)
|
|
528
|
+
.replace(/&/g, '&')
|
|
529
|
+
.replace(/</g, '<')
|
|
530
|
+
.replace(/>/g, '>')
|
|
531
|
+
.replace(/"/g, '"')
|
|
532
|
+
.replace(/'/g, ''');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Update nav active state
|
|
536
|
+
function updateNav(path) {
|
|
537
|
+
document.querySelectorAll('#main-nav a').forEach(link => {
|
|
538
|
+
link.classList.remove('active');
|
|
539
|
+
if (path === '/' && link.dataset.nav === 'home') {
|
|
540
|
+
link.classList.add('active');
|
|
541
|
+
} else if (path === '/new' && link.dataset.nav === 'new') {
|
|
542
|
+
link.classList.add('active');
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Render task group list (Home)
|
|
548
|
+
async function renderTaskGroupList() {
|
|
549
|
+
app.innerHTML = '<div class="loading">Loading</div>';
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
const data = await api('/task-groups');
|
|
553
|
+
const groups = data.task_groups || [];
|
|
554
|
+
|
|
555
|
+
if (groups.length === 0) {
|
|
556
|
+
app.innerHTML = `
|
|
557
|
+
<div class="page-header">
|
|
558
|
+
<h2>Task Groups</h2>
|
|
559
|
+
<button class="refresh-btn" onclick="renderTaskGroupList()">Refresh</button>
|
|
560
|
+
</div>
|
|
561
|
+
<div class="card">
|
|
562
|
+
<div class="empty-state">
|
|
563
|
+
<p>No task groups yet.</p>
|
|
564
|
+
<button class="btn btn-primary" onclick="navigate('/new')">+ Create New Command</button>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
`;
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const items = groups.map(g => `
|
|
572
|
+
<div class="list-item" onclick="navigate('/task-groups/${encodeURIComponent(g.task_group_id)}')">
|
|
573
|
+
<div class="list-item-main">
|
|
574
|
+
<div class="list-item-title">${escapeHtml(g.task_group_id)}</div>
|
|
575
|
+
<div class="list-item-meta">
|
|
576
|
+
${g.task_count} task(s) | Created: ${formatDate(g.created_at)}
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
<span class="list-item-arrow">›</span>
|
|
580
|
+
</div>
|
|
581
|
+
`).join('');
|
|
582
|
+
|
|
583
|
+
app.innerHTML = `
|
|
584
|
+
<div class="page-header">
|
|
585
|
+
<h2>Task Groups</h2>
|
|
586
|
+
<button class="refresh-btn" onclick="renderTaskGroupList()">Refresh</button>
|
|
587
|
+
</div>
|
|
588
|
+
<div class="card">
|
|
589
|
+
${items}
|
|
590
|
+
</div>
|
|
591
|
+
`;
|
|
592
|
+
} catch (error) {
|
|
593
|
+
app.innerHTML = `
|
|
594
|
+
<div class="page-header">
|
|
595
|
+
<h2>Task Groups</h2>
|
|
596
|
+
</div>
|
|
597
|
+
<div class="card">
|
|
598
|
+
<div class="error-message">Error: ${escapeHtml(error.message)}</div>
|
|
599
|
+
<button class="btn btn-secondary" onclick="renderTaskGroupList()">Retry</button>
|
|
600
|
+
</div>
|
|
601
|
+
`;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Render task list for a task group
|
|
606
|
+
async function renderTaskList(taskGroupId) {
|
|
607
|
+
app.innerHTML = '<div class="loading">Loading</div>';
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
const data = await api(`/task-groups/${encodeURIComponent(taskGroupId)}/tasks`);
|
|
611
|
+
const tasks = data.tasks || [];
|
|
612
|
+
|
|
613
|
+
const backBtn = `<a href="/" class="back-btn" onclick="event.preventDefault(); navigate('/')">← Back to Groups</a>`;
|
|
614
|
+
|
|
615
|
+
if (tasks.length === 0) {
|
|
616
|
+
app.innerHTML = `
|
|
617
|
+
<div class="page-header">
|
|
618
|
+
<h2>${escapeHtml(taskGroupId)}</h2>
|
|
619
|
+
<div class="action-bar">
|
|
620
|
+
${backBtn}
|
|
621
|
+
<button class="refresh-btn" onclick="renderTaskList('${escapeHtml(taskGroupId)}')">Refresh</button>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
<div class="card">
|
|
625
|
+
<div class="empty-state">
|
|
626
|
+
<p>No tasks in this group yet.</p>
|
|
627
|
+
<button class="btn btn-primary" onclick="navigate('/new?group=${encodeURIComponent(taskGroupId)}')">+ Add Task</button>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
`;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const items = tasks.map(t => `
|
|
635
|
+
<div class="list-item" onclick="navigate('/tasks/${encodeURIComponent(t.task_id)}')">
|
|
636
|
+
<div class="list-item-main">
|
|
637
|
+
<span class="badge ${getStatusBadgeClass(t.status)}">${t.status}</span>
|
|
638
|
+
<span class="list-item-title">${escapeHtml(t.task_id)}</span>
|
|
639
|
+
<div class="list-item-meta">
|
|
640
|
+
${escapeHtml(t.prompt.substring(0, 80))}${t.prompt.length > 80 ? '...' : ''}
|
|
641
|
+
</div>
|
|
642
|
+
<div class="list-item-meta">
|
|
643
|
+
${formatDate(t.created_at)}
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
<span class="list-item-arrow">›</span>
|
|
647
|
+
</div>
|
|
648
|
+
`).join('');
|
|
649
|
+
|
|
650
|
+
app.innerHTML = `
|
|
651
|
+
<div class="page-header">
|
|
652
|
+
<h2>${escapeHtml(taskGroupId)}</h2>
|
|
653
|
+
<div class="action-bar">
|
|
654
|
+
${backBtn}
|
|
655
|
+
<button class="refresh-btn" onclick="renderTaskList('${escapeHtml(taskGroupId)}')">Refresh</button>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
<div class="card">
|
|
659
|
+
<h3>Tasks (${tasks.length})</h3>
|
|
660
|
+
${items}
|
|
661
|
+
</div>
|
|
662
|
+
<div style="text-align: center; margin-top: 16px;">
|
|
663
|
+
<button class="btn btn-primary" onclick="navigate('/new?group=${encodeURIComponent(taskGroupId)}')">+ Add Task to This Group</button>
|
|
664
|
+
</div>
|
|
665
|
+
`;
|
|
666
|
+
} catch (error) {
|
|
667
|
+
app.innerHTML = `
|
|
668
|
+
<div class="page-header">
|
|
669
|
+
<h2>${escapeHtml(taskGroupId)}</h2>
|
|
670
|
+
<a href="/" class="back-btn" onclick="event.preventDefault(); navigate('/')">← Back</a>
|
|
671
|
+
</div>
|
|
672
|
+
<div class="card">
|
|
673
|
+
<div class="error-message">Error: ${escapeHtml(error.message)}</div>
|
|
674
|
+
<button class="btn btn-secondary" onclick="renderTaskList('${escapeHtml(taskGroupId)}')">Retry</button>
|
|
675
|
+
</div>
|
|
676
|
+
`;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Render task detail with logs
|
|
681
|
+
async function renderTaskDetail(taskId) {
|
|
682
|
+
app.innerHTML = '<div class="loading">Loading</div>';
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
const task = await api(`/tasks/${encodeURIComponent(taskId)}`);
|
|
686
|
+
|
|
687
|
+
const backBtn = `<a href="/task-groups/${encodeURIComponent(task.task_group_id)}" class="back-btn" onclick="event.preventDefault(); navigate('/task-groups/${encodeURIComponent(task.task_group_id)}')">← Back to Tasks</a>`;
|
|
688
|
+
|
|
689
|
+
let errorSection = '';
|
|
690
|
+
if (task.error_message) {
|
|
691
|
+
errorSection = `
|
|
692
|
+
<div class="card">
|
|
693
|
+
<h3>Error</h3>
|
|
694
|
+
<div class="log-entry error">
|
|
695
|
+
<div class="log-content">${escapeHtml(task.error_message)}</div>
|
|
696
|
+
</div>
|
|
697
|
+
</div>
|
|
698
|
+
`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Log entries (simulated based on task status)
|
|
702
|
+
let logEntries = '';
|
|
703
|
+
if (task.status !== 'QUEUED') {
|
|
704
|
+
logEntries = `
|
|
705
|
+
<div class="card log-section">
|
|
706
|
+
<h3>Execution Log</h3>
|
|
707
|
+
<div class="log-entry user-input">
|
|
708
|
+
<div class="log-time">${formatFullDate(task.created_at)}</div>
|
|
709
|
+
<div class="log-type">USER INPUT</div>
|
|
710
|
+
<div class="log-content">${escapeHtml(task.prompt)}</div>
|
|
711
|
+
</div>
|
|
712
|
+
${task.status === 'RUNNING' ? `
|
|
713
|
+
<div class="log-entry task-started">
|
|
714
|
+
<div class="log-time">${formatFullDate(task.updated_at)}</div>
|
|
715
|
+
<div class="log-type">TASK STARTED</div>
|
|
716
|
+
<div class="log-content">Task is currently running...</div>
|
|
717
|
+
</div>
|
|
718
|
+
` : ''}
|
|
719
|
+
${task.status === 'COMPLETE' ? `
|
|
720
|
+
<div class="log-entry task-completed">
|
|
721
|
+
<div class="log-time">${formatFullDate(task.updated_at)}</div>
|
|
722
|
+
<div class="log-type">TASK COMPLETED</div>
|
|
723
|
+
<div class="log-content">Task completed successfully.</div>
|
|
724
|
+
</div>
|
|
725
|
+
` : ''}
|
|
726
|
+
${task.status === 'ERROR' ? `
|
|
727
|
+
<div class="log-entry error">
|
|
728
|
+
<div class="log-time">${formatFullDate(task.updated_at)}</div>
|
|
729
|
+
<div class="log-type">TASK ERROR</div>
|
|
730
|
+
<div class="log-content">${escapeHtml(task.error_message || 'Unknown error')}</div>
|
|
731
|
+
</div>
|
|
732
|
+
` : ''}
|
|
733
|
+
</div>
|
|
734
|
+
`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
app.innerHTML = `
|
|
738
|
+
<div class="page-header">
|
|
739
|
+
<h2>Task Detail</h2>
|
|
740
|
+
<div class="action-bar">
|
|
741
|
+
${backBtn}
|
|
742
|
+
<button class="refresh-btn" onclick="renderTaskDetail('${escapeHtml(taskId)}')">Refresh</button>
|
|
743
|
+
</div>
|
|
744
|
+
</div>
|
|
745
|
+
|
|
746
|
+
<div class="card">
|
|
747
|
+
<h3>Status</h3>
|
|
748
|
+
<span class="badge ${getStatusBadgeClass(task.status)}" style="font-size: 1rem; padding: 8px 16px;">${task.status}</span>
|
|
749
|
+
</div>
|
|
750
|
+
|
|
751
|
+
<div class="card">
|
|
752
|
+
<h3>Details</h3>
|
|
753
|
+
<div class="detail-grid">
|
|
754
|
+
<div class="detail-item">
|
|
755
|
+
<label>Task ID</label>
|
|
756
|
+
<value>${escapeHtml(task.task_id)}</value>
|
|
757
|
+
</div>
|
|
758
|
+
<div class="detail-item">
|
|
759
|
+
<label>Task Group</label>
|
|
760
|
+
<value><a href="/task-groups/${encodeURIComponent(task.task_group_id)}" onclick="event.preventDefault(); navigate('/task-groups/${encodeURIComponent(task.task_group_id)}')">${escapeHtml(task.task_group_id)}</a></value>
|
|
761
|
+
</div>
|
|
762
|
+
<div class="detail-item">
|
|
763
|
+
<label>Session ID</label>
|
|
764
|
+
<value>${escapeHtml(task.session_id)}</value>
|
|
765
|
+
</div>
|
|
766
|
+
<div class="detail-item">
|
|
767
|
+
<label>Created</label>
|
|
768
|
+
<value>${formatFullDate(task.created_at)}</value>
|
|
769
|
+
</div>
|
|
770
|
+
<div class="detail-item">
|
|
771
|
+
<label>Updated</label>
|
|
772
|
+
<value>${formatFullDate(task.updated_at)}</value>
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
|
|
777
|
+
<div class="card">
|
|
778
|
+
<h3>Prompt</h3>
|
|
779
|
+
<div class="prompt-box">${escapeHtml(task.prompt)}</div>
|
|
780
|
+
</div>
|
|
781
|
+
|
|
782
|
+
${errorSection}
|
|
783
|
+
${logEntries}
|
|
784
|
+
`;
|
|
785
|
+
} catch (error) {
|
|
786
|
+
app.innerHTML = `
|
|
787
|
+
<div class="page-header">
|
|
788
|
+
<h2>Task Detail</h2>
|
|
789
|
+
<a href="/" class="back-btn" onclick="event.preventDefault(); navigate('/')">← Back</a>
|
|
790
|
+
</div>
|
|
791
|
+
<div class="card">
|
|
792
|
+
<div class="error-message">Error: ${escapeHtml(error.message)}</div>
|
|
793
|
+
<button class="btn btn-secondary" onclick="renderTaskDetail('${escapeHtml(taskId)}')">Retry</button>
|
|
794
|
+
</div>
|
|
795
|
+
`;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Render new command form
|
|
800
|
+
async function renderNewCommandForm(prefilledGroup = '') {
|
|
801
|
+
let taskGroups = [];
|
|
802
|
+
try {
|
|
803
|
+
const data = await api('/task-groups');
|
|
804
|
+
taskGroups = data.task_groups || [];
|
|
805
|
+
} catch (error) {
|
|
806
|
+
// Ignore - we can still create new task groups
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const options = taskGroups.length > 0
|
|
810
|
+
? taskGroups.map(g => `<option value="${escapeHtml(g.task_group_id)}">${escapeHtml(g.task_group_id)}</option>`).join('')
|
|
811
|
+
: '';
|
|
812
|
+
|
|
813
|
+
const backBtn = `<a href="/" class="back-btn" onclick="event.preventDefault(); navigate('/')">← Back to Groups</a>`;
|
|
814
|
+
|
|
815
|
+
app.innerHTML = `
|
|
816
|
+
<div class="page-header">
|
|
817
|
+
<h2>New Command</h2>
|
|
818
|
+
${backBtn}
|
|
819
|
+
</div>
|
|
820
|
+
|
|
821
|
+
<div class="card">
|
|
822
|
+
<form id="new-command-form">
|
|
823
|
+
<div class="form-group">
|
|
824
|
+
<label for="task_group_id">Task Group ID</label>
|
|
825
|
+
<input type="text" id="task_group_id" name="task_group_id"
|
|
826
|
+
placeholder="Enter new or select existing"
|
|
827
|
+
list="task-groups-list"
|
|
828
|
+
value="${escapeHtml(prefilledGroup)}"
|
|
829
|
+
required>
|
|
830
|
+
<datalist id="task-groups-list">
|
|
831
|
+
${options}
|
|
832
|
+
</datalist>
|
|
833
|
+
</div>
|
|
834
|
+
|
|
835
|
+
<div class="form-group">
|
|
836
|
+
<label for="prompt">Command / Prompt</label>
|
|
837
|
+
<textarea id="prompt" name="prompt"
|
|
838
|
+
placeholder="Enter your command or prompt here..." required></textarea>
|
|
839
|
+
</div>
|
|
840
|
+
|
|
841
|
+
<div id="form-message"></div>
|
|
842
|
+
|
|
843
|
+
<button type="submit" class="btn btn-primary" id="submit-btn">Submit to Queue</button>
|
|
844
|
+
</form>
|
|
845
|
+
</div>
|
|
846
|
+
|
|
847
|
+
<div class="card" style="background: #fffbeb; border-left: 4px solid #f59e0b;">
|
|
848
|
+
<p style="color: #92400e; font-size: 0.9rem;">
|
|
849
|
+
<strong>Note:</strong> This adds a task to the queue. The Runner will pick it up and execute it automatically.
|
|
850
|
+
</p>
|
|
851
|
+
</div>
|
|
852
|
+
`;
|
|
853
|
+
|
|
854
|
+
// Form submission
|
|
855
|
+
document.getElementById('new-command-form').addEventListener('submit', async (e) => {
|
|
856
|
+
e.preventDefault();
|
|
857
|
+
const form = e.target;
|
|
858
|
+
const submitBtn = document.getElementById('submit-btn');
|
|
859
|
+
const messageDiv = document.getElementById('form-message');
|
|
860
|
+
|
|
861
|
+
const taskGroupId = form.task_group_id.value.trim();
|
|
862
|
+
const prompt = form.prompt.value.trim();
|
|
863
|
+
|
|
864
|
+
if (!taskGroupId || !prompt) {
|
|
865
|
+
messageDiv.innerHTML = '<div class="error-message">Please fill in all fields.</div>';
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
submitBtn.disabled = true;
|
|
870
|
+
submitBtn.textContent = 'Submitting...';
|
|
871
|
+
|
|
872
|
+
try {
|
|
873
|
+
const result = await api('/tasks', {
|
|
874
|
+
method: 'POST',
|
|
875
|
+
body: JSON.stringify({ task_group_id: taskGroupId, prompt }),
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
messageDiv.innerHTML = `
|
|
879
|
+
<div class="success-message">
|
|
880
|
+
Task queued successfully!<br>
|
|
881
|
+
Task ID: <strong>${escapeHtml(result.task_id)}</strong>
|
|
882
|
+
</div>
|
|
883
|
+
`;
|
|
884
|
+
|
|
885
|
+
// Clear prompt only
|
|
886
|
+
form.prompt.value = '';
|
|
887
|
+
|
|
888
|
+
// Navigate to task detail after delay
|
|
889
|
+
setTimeout(() => {
|
|
890
|
+
navigate(`/tasks/${encodeURIComponent(result.task_id)}`);
|
|
891
|
+
}, 1000);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
messageDiv.innerHTML = `<div class="error-message">Error: ${escapeHtml(error.message)}</div>`;
|
|
894
|
+
} finally {
|
|
895
|
+
submitBtn.disabled = false;
|
|
896
|
+
submitBtn.textContent = 'Submit to Queue';
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Router
|
|
902
|
+
function router() {
|
|
903
|
+
const path = window.location.pathname;
|
|
904
|
+
const search = new URLSearchParams(window.location.search);
|
|
905
|
+
currentPath = path;
|
|
906
|
+
updateNav(path);
|
|
907
|
+
|
|
908
|
+
if (path === '/' || path === '') {
|
|
909
|
+
renderTaskGroupList();
|
|
910
|
+
} else if (path.startsWith('/task-groups/')) {
|
|
911
|
+
const taskGroupId = decodeURIComponent(path.split('/task-groups/')[1]);
|
|
912
|
+
renderTaskList(taskGroupId);
|
|
913
|
+
} else if (path.startsWith('/tasks/')) {
|
|
914
|
+
const taskId = decodeURIComponent(path.split('/tasks/')[1]);
|
|
915
|
+
renderTaskDetail(taskId);
|
|
916
|
+
} else if (path === '/new') {
|
|
917
|
+
const group = search.get('group') || '';
|
|
918
|
+
renderNewCommandForm(group);
|
|
919
|
+
} else {
|
|
920
|
+
app.innerHTML = `
|
|
921
|
+
<div class="card">
|
|
922
|
+
<h2>404 - Page Not Found</h2>
|
|
923
|
+
<p>The page you're looking for doesn't exist.</p>
|
|
924
|
+
<button class="btn btn-primary" onclick="navigate('/')" style="margin-top: 16px;">Go Home</button>
|
|
925
|
+
</div>
|
|
926
|
+
`;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Navigate
|
|
931
|
+
function navigate(path) {
|
|
932
|
+
history.pushState(null, '', path);
|
|
933
|
+
router();
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Handle back/forward
|
|
937
|
+
window.addEventListener('popstate', router);
|
|
938
|
+
|
|
939
|
+
// Handle navigation links
|
|
940
|
+
document.querySelectorAll('#main-nav a').forEach(link => {
|
|
941
|
+
link.addEventListener('click', (e) => {
|
|
942
|
+
e.preventDefault();
|
|
943
|
+
navigate(link.getAttribute('href'));
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
// Initial load
|
|
948
|
+
router();
|
|
949
|
+
</script>
|
|
950
|
+
</body>
|
|
951
|
+
</html>
|