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.
@@ -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, '&amp;')
529
+ .replace(/</g, '&lt;')
530
+ .replace(/>/g, '&gt;')
531
+ .replace(/"/g, '&quot;')
532
+ .replace(/'/g, '&#039;');
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pm-orchestrator-runner",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "CLI tool that enforces controlled execution of Claude Code through mandatory orchestration",
5
5
  "main": "dist/cli/index.js",
6
6
  "bin": {