@kennethsolomon/shipkit 3.3.0 → 3.5.0

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,939 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ShipKit Mission Control</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Orbitron:wght@600&display=swap" rel="stylesheet">
10
+ <style>
11
+ *,
12
+ *::before,
13
+ *::after {
14
+ margin: 0;
15
+ padding: 0;
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ :root {
20
+ --color-bg: #080C14;
21
+ --color-surface: #111827;
22
+ --color-surface-alt: #1A2234;
23
+ --color-border: #1E2D4A;
24
+ --color-text: #E2E8F0;
25
+ --color-text-muted: #64748B;
26
+ --color-done: #10B981;
27
+ --color-active: #3B82F6;
28
+ --color-pending: #334155;
29
+ --color-skipped: #F59E0B;
30
+ --color-gate: #EF4444;
31
+ --color-partial: #8B5CF6;
32
+ --font-display: 'Orbitron', sans-serif;
33
+ --font-mono: 'JetBrains Mono', monospace;
34
+ }
35
+
36
+ html, body {
37
+ height: 100%;
38
+ background: var(--color-bg);
39
+ color: var(--color-text);
40
+ font-family: var(--font-mono);
41
+ font-size: 13px;
42
+ font-weight: 400;
43
+ -webkit-font-smoothing: antialiased;
44
+ -moz-osx-font-smoothing: grayscale;
45
+ }
46
+
47
+ /* ── Header ── */
48
+
49
+ .header {
50
+ position: fixed;
51
+ top: 0;
52
+ left: 0;
53
+ right: 0;
54
+ height: 56px;
55
+ background: var(--color-surface);
56
+ border-bottom: 1px solid var(--color-border);
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: space-between;
60
+ padding: 0 20px;
61
+ z-index: 100;
62
+ }
63
+
64
+ .header-left {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 10px;
68
+ }
69
+
70
+ .header-left svg {
71
+ width: 18px;
72
+ height: 18px;
73
+ fill: var(--color-active);
74
+ }
75
+
76
+ .header-title {
77
+ font-family: var(--font-display);
78
+ font-size: 14px;
79
+ font-weight: 600;
80
+ letter-spacing: 3px;
81
+ color: var(--color-text);
82
+ }
83
+
84
+ .header-right {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 16px;
88
+ font-size: 12px;
89
+ font-weight: 500;
90
+ color: var(--color-text-muted);
91
+ }
92
+
93
+ .live-indicator {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 6px;
97
+ }
98
+
99
+ .live-dot {
100
+ width: 7px;
101
+ height: 7px;
102
+ border-radius: 50%;
103
+ background: var(--color-done);
104
+ animation: pulse-live 2s infinite ease-in-out;
105
+ }
106
+
107
+ @keyframes pulse-live {
108
+ 0%, 100% { opacity: 1; }
109
+ 50% { opacity: 0.3; }
110
+ }
111
+
112
+ .live-label {
113
+ font-weight: 600;
114
+ color: var(--color-done);
115
+ font-size: 11px;
116
+ letter-spacing: 1px;
117
+ }
118
+
119
+ /* ── Content ── */
120
+
121
+ .content {
122
+ padding: 80px 24px 60px;
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: 16px;
126
+ min-height: 100vh;
127
+ }
128
+
129
+ /* ── Empty State ── */
130
+
131
+ .empty-state {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ min-height: 60vh;
136
+ color: var(--color-text-muted);
137
+ font-size: 15px;
138
+ font-weight: 500;
139
+ }
140
+
141
+ /* ── Swimlane ── */
142
+
143
+ .swimlane {
144
+ background: var(--color-surface);
145
+ border: 1px solid var(--color-border);
146
+ border-radius: 8px;
147
+ overflow: hidden;
148
+ }
149
+
150
+ .swimlane-header {
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: space-between;
154
+ padding: 14px 16px;
155
+ cursor: pointer;
156
+ user-select: none;
157
+ transition: background 0.15s;
158
+ }
159
+
160
+ .swimlane-header:hover {
161
+ background: var(--color-surface-alt);
162
+ }
163
+
164
+ .swimlane-header-left {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 10px;
168
+ min-width: 0;
169
+ }
170
+
171
+ .swimlane-header-left svg {
172
+ width: 16px;
173
+ height: 16px;
174
+ flex-shrink: 0;
175
+ stroke: var(--color-text-muted);
176
+ fill: none;
177
+ stroke-width: 2;
178
+ stroke-linecap: round;
179
+ stroke-linejoin: round;
180
+ }
181
+
182
+ .branch-name {
183
+ font-size: 14px;
184
+ font-weight: 600;
185
+ color: var(--color-text);
186
+ white-space: nowrap;
187
+ overflow: hidden;
188
+ text-overflow: ellipsis;
189
+ }
190
+
191
+ .header-dot {
192
+ color: var(--color-text-muted);
193
+ font-size: 10px;
194
+ flex-shrink: 0;
195
+ }
196
+
197
+ .task-name {
198
+ font-size: 13px;
199
+ font-weight: 400;
200
+ color: var(--color-text-muted);
201
+ white-space: nowrap;
202
+ overflow: hidden;
203
+ text-overflow: ellipsis;
204
+ }
205
+
206
+ .swimlane-header-right {
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 12px;
210
+ flex-shrink: 0;
211
+ }
212
+
213
+ .progress-fraction {
214
+ font-size: 13px;
215
+ font-weight: 600;
216
+ color: var(--color-text);
217
+ }
218
+
219
+ .progress-pct {
220
+ font-size: 12px;
221
+ font-weight: 500;
222
+ color: var(--color-text-muted);
223
+ }
224
+
225
+ .chevron {
226
+ width: 16px;
227
+ height: 16px;
228
+ stroke: var(--color-text-muted);
229
+ fill: none;
230
+ stroke-width: 2;
231
+ stroke-linecap: round;
232
+ stroke-linejoin: round;
233
+ transition: transform 0.3s ease;
234
+ flex-shrink: 0;
235
+ }
236
+
237
+ .swimlane.expanded .chevron {
238
+ transform: rotate(90deg);
239
+ }
240
+
241
+ /* ── Progress Bar ── */
242
+
243
+ .progress-bar-track {
244
+ height: 4px;
245
+ background: var(--color-pending);
246
+ margin: 0 16px 14px;
247
+ border-radius: 2px;
248
+ overflow: hidden;
249
+ }
250
+
251
+ .progress-bar-fill {
252
+ height: 100%;
253
+ border-radius: 2px;
254
+ background: linear-gradient(90deg, var(--color-active), var(--color-done));
255
+ transition: width 0.5s ease;
256
+ }
257
+
258
+ /* ── Expanded Body ── */
259
+
260
+ .swimlane-body {
261
+ max-height: 0;
262
+ overflow: hidden;
263
+ transition: max-height 0.3s ease;
264
+ }
265
+
266
+ .swimlane.expanded .swimlane-body {
267
+ max-height: 4000px;
268
+ }
269
+
270
+ .swimlane-body-inner {
271
+ padding: 0 16px 16px;
272
+ display: flex;
273
+ flex-direction: column;
274
+ gap: 14px;
275
+ }
276
+
277
+ /* ── Phase Timeline ── */
278
+
279
+ .phase-timeline {
280
+ display: flex;
281
+ flex-wrap: wrap;
282
+ gap: 3px;
283
+ }
284
+
285
+ .phase-cell {
286
+ width: 44px;
287
+ height: 36px;
288
+ display: flex;
289
+ align-items: center;
290
+ justify-content: center;
291
+ border-radius: 4px;
292
+ font-size: 10px;
293
+ font-weight: 600;
294
+ font-family: var(--font-mono);
295
+ position: relative;
296
+ cursor: default;
297
+ transition: background 0.15s;
298
+ }
299
+
300
+ .phase-cell.status-done {
301
+ background: var(--color-done);
302
+ color: #064E3B;
303
+ }
304
+
305
+ .phase-cell.status-active {
306
+ background: var(--color-active);
307
+ color: #FFFFFF;
308
+ animation: glow-active 2s infinite ease-in-out;
309
+ }
310
+
311
+ @keyframes glow-active {
312
+ 0%, 100% { box-shadow: 0 0 6px rgba(59, 130, 246, 0.4); }
313
+ 50% { box-shadow: 0 0 16px rgba(59, 130, 246, 0.7); }
314
+ }
315
+
316
+ .phase-cell.status-pending {
317
+ background: var(--color-pending);
318
+ color: var(--color-text-muted);
319
+ }
320
+
321
+ .phase-cell.status-skipped {
322
+ background: transparent;
323
+ outline: 1.5px solid var(--color-skipped);
324
+ color: var(--color-skipped);
325
+ }
326
+
327
+ .phase-cell.status-partial {
328
+ background: var(--color-partial);
329
+ color: #FFFFFF;
330
+ }
331
+
332
+ .phase-cell.hard-gate {
333
+ border-bottom: 2px solid var(--color-gate);
334
+ }
335
+
336
+ /* ── Tooltip ── */
337
+
338
+ .phase-cell .tooltip {
339
+ display: none;
340
+ position: absolute;
341
+ bottom: calc(100% + 6px);
342
+ left: 50%;
343
+ transform: translateX(-50%);
344
+ background: var(--color-surface-alt);
345
+ border: 1px solid var(--color-border);
346
+ border-radius: 6px;
347
+ padding: 8px 10px;
348
+ white-space: nowrap;
349
+ font-size: 11px;
350
+ font-weight: 400;
351
+ color: var(--color-text);
352
+ z-index: 50;
353
+ pointer-events: none;
354
+ line-height: 1.5;
355
+ }
356
+
357
+ .phase-cell .tooltip .tt-name {
358
+ font-weight: 600;
359
+ }
360
+
361
+ .phase-cell .tooltip .tt-cmd {
362
+ color: var(--color-active);
363
+ }
364
+
365
+ .phase-cell .tooltip .tt-notes {
366
+ color: var(--color-text-muted);
367
+ font-style: italic;
368
+ }
369
+
370
+ .phase-cell:hover .tooltip {
371
+ display: block;
372
+ }
373
+
374
+ /* ── Legend ── */
375
+
376
+ .legend {
377
+ display: flex;
378
+ flex-wrap: wrap;
379
+ gap: 14px;
380
+ font-size: 11px;
381
+ color: var(--color-text-muted);
382
+ }
383
+
384
+ .legend-item {
385
+ display: flex;
386
+ align-items: center;
387
+ gap: 5px;
388
+ }
389
+
390
+ .legend-dot {
391
+ width: 8px;
392
+ height: 8px;
393
+ border-radius: 2px;
394
+ flex-shrink: 0;
395
+ }
396
+
397
+ .legend-dot.ld-done { background: var(--color-done); }
398
+ .legend-dot.ld-active { background: var(--color-active); }
399
+ .legend-dot.ld-gate { background: var(--color-gate); }
400
+ .legend-dot.ld-skipped { background: transparent; outline: 1.5px solid var(--color-skipped); }
401
+ .legend-dot.ld-pending { background: var(--color-pending); }
402
+ .legend-dot.ld-partial { background: var(--color-partial); }
403
+
404
+ /* ── Active Step Card ── */
405
+
406
+ .active-step-card {
407
+ background: var(--color-surface-alt);
408
+ border-left: 3px solid var(--color-active);
409
+ border-radius: 0 6px 6px 0;
410
+ padding: 12px 14px;
411
+ display: flex;
412
+ align-items: center;
413
+ gap: 10px;
414
+ }
415
+
416
+ .active-step-num {
417
+ font-size: 18px;
418
+ font-weight: 700;
419
+ color: var(--color-active);
420
+ min-width: 28px;
421
+ }
422
+
423
+ .active-step-info {
424
+ display: flex;
425
+ flex-direction: column;
426
+ gap: 2px;
427
+ }
428
+
429
+ .active-step-name {
430
+ font-size: 13px;
431
+ font-weight: 600;
432
+ color: var(--color-text);
433
+ }
434
+
435
+ .active-step-cmd {
436
+ font-size: 12px;
437
+ font-weight: 400;
438
+ color: var(--color-active);
439
+ }
440
+
441
+ /* ── Status Columns ── */
442
+
443
+ .status-columns {
444
+ display: grid;
445
+ grid-template-columns: repeat(3, 1fr);
446
+ gap: 12px;
447
+ }
448
+
449
+ .status-col {
450
+ display: flex;
451
+ flex-direction: column;
452
+ gap: 6px;
453
+ }
454
+
455
+ .status-col-header {
456
+ display: flex;
457
+ align-items: center;
458
+ gap: 6px;
459
+ font-size: 11px;
460
+ font-weight: 600;
461
+ letter-spacing: 0.5px;
462
+ text-transform: uppercase;
463
+ padding-bottom: 6px;
464
+ border-bottom: 1px solid var(--color-border);
465
+ }
466
+
467
+ .status-col-dot {
468
+ width: 7px;
469
+ height: 7px;
470
+ border-radius: 50%;
471
+ flex-shrink: 0;
472
+ }
473
+
474
+ .status-col-dot.sc-done { background: var(--color-done); }
475
+ .status-col-dot.sc-skipped { background: var(--color-skipped); }
476
+ .status-col-dot.sc-pending { background: var(--color-pending); }
477
+
478
+ .status-col-count {
479
+ color: var(--color-text-muted);
480
+ font-weight: 400;
481
+ }
482
+
483
+ .status-col-list {
484
+ list-style: none;
485
+ font-size: 11px;
486
+ font-weight: 400;
487
+ color: var(--color-text-muted);
488
+ line-height: 1.7;
489
+ }
490
+
491
+ /* ── Tasks Panel ── */
492
+
493
+ .tasks-panel {
494
+ border-top: 1px solid var(--color-border);
495
+ padding-top: 12px;
496
+ display: flex;
497
+ flex-direction: column;
498
+ gap: 10px;
499
+ }
500
+
501
+ .tasks-panel-header {
502
+ display: flex;
503
+ align-items: center;
504
+ gap: 6px;
505
+ font-size: 11px;
506
+ font-weight: 600;
507
+ letter-spacing: 0.5px;
508
+ text-transform: uppercase;
509
+ padding-bottom: 6px;
510
+ border-bottom: 1px solid var(--color-border);
511
+ }
512
+
513
+ .tasks-section-label {
514
+ font-size: 10px;
515
+ font-weight: 600;
516
+ color: var(--color-text-muted);
517
+ text-transform: uppercase;
518
+ letter-spacing: 0.5px;
519
+ margin-top: 4px;
520
+ margin-bottom: 2px;
521
+ }
522
+
523
+ .todo-item {
524
+ display: flex;
525
+ align-items: baseline;
526
+ gap: 6px;
527
+ font-size: 11px;
528
+ font-weight: 400;
529
+ line-height: 1.6;
530
+ padding: 1px 0;
531
+ }
532
+
533
+ .todo-item.todo-done {
534
+ color: rgba(16, 185, 129, 0.65);
535
+ }
536
+
537
+ .todo-item.todo-current {
538
+ color: #3B82F6;
539
+ background: rgba(59, 130, 246, 0.08);
540
+ border-left: 2px solid #3B82F6;
541
+ padding-left: 6px;
542
+ border-radius: 0 3px 3px 0;
543
+ }
544
+
545
+ .todo-item.todo-pending {
546
+ color: #6B7280;
547
+ }
548
+
549
+ .todo-item-icon {
550
+ flex-shrink: 0;
551
+ font-style: normal;
552
+ }
553
+
554
+ /* ── Footer ── */
555
+
556
+ .footer {
557
+ position: fixed;
558
+ bottom: 0;
559
+ left: 0;
560
+ right: 0;
561
+ height: 36px;
562
+ background: var(--color-surface);
563
+ border-top: 1px solid var(--color-border);
564
+ display: flex;
565
+ align-items: center;
566
+ justify-content: center;
567
+ font-size: 11px;
568
+ font-weight: 400;
569
+ color: var(--color-text-muted);
570
+ z-index: 100;
571
+ }
572
+
573
+ /* ── Responsive ── */
574
+
575
+ @media (max-width: 768px) {
576
+ .phase-timeline {
577
+ flex-wrap: wrap;
578
+ }
579
+
580
+ .status-columns {
581
+ grid-template-columns: 1fr;
582
+ }
583
+
584
+ .header-title {
585
+ font-size: 11px;
586
+ letter-spacing: 1.5px;
587
+ }
588
+
589
+ .task-name {
590
+ display: none;
591
+ }
592
+
593
+ .header-dot {
594
+ display: none;
595
+ }
596
+ }
597
+ </style>
598
+ </head>
599
+ <body>
600
+
601
+ <!-- ── Header ── -->
602
+ <header class="header">
603
+ <div class="header-left">
604
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
605
+ <path d="M12 2 L22 12 L12 22 L2 12 Z"/>
606
+ </svg>
607
+ <span class="header-title">SHIPKIT MISSION CONTROL</span>
608
+ </div>
609
+ <div class="header-right">
610
+ <div class="live-indicator">
611
+ <span class="live-dot"></span>
612
+ <span class="live-label">LIVE</span>
613
+ </div>
614
+ <span class="refresh-interval">&#8635; 3s</span>
615
+ <span class="clock" id="clock">--:--</span>
616
+ </div>
617
+ </header>
618
+
619
+ <!-- ── Content ── -->
620
+ <main class="content" id="content">
621
+ <div class="empty-state">No active workflows detected.</div>
622
+ </main>
623
+
624
+ <!-- ── Footer ── -->
625
+ <footer class="footer" id="footer">
626
+ 0 worktrees &middot; Last refresh: --s ago &middot; Port 3333
627
+ </footer>
628
+
629
+ <script>
630
+ (function () {
631
+ 'use strict';
632
+
633
+ var lastRefreshTime = Date.now();
634
+ var expandedWorktrees = {};
635
+ var initialized = false;
636
+ var lastWorktreeCount = 0;
637
+ var lastResponseJson = '';
638
+
639
+ var contentEl = document.getElementById('content');
640
+ var footerEl = document.getElementById('footer');
641
+ var clockEl = document.getElementById('clock');
642
+
643
+ // ── SVG helpers ──
644
+
645
+ function svgBranch() {
646
+ return '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">' +
647
+ '<line x1="6" y1="3" x2="6" y2="15"/>' +
648
+ '<circle cx="18" cy="6" r="3"/>' +
649
+ '<circle cx="6" cy="18" r="3"/>' +
650
+ '<path d="M18 9a9 9 0 0 1-9 9"/>' +
651
+ '</svg>';
652
+ }
653
+
654
+ function svgChevron() {
655
+ return '<svg class="chevron" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">' +
656
+ '<polyline points="9 6 15 12 9 18"/>' +
657
+ '</svg>';
658
+ }
659
+
660
+ // ── Helpers ──
661
+
662
+ function esc(str) {
663
+ return (str || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
664
+ }
665
+
666
+ function classifyStatus(raw) {
667
+ if (!raw) return 'pending';
668
+ var s = raw.toLowerCase().trim();
669
+ if (s === 'done') return 'done';
670
+ if (s === 'skipped') return 'skipped';
671
+ if (s === 'partial') return 'partial';
672
+ if (s.indexOf('>> next <<') !== -1) return 'active';
673
+ if (s === 'not yet' || s === '') return 'pending';
674
+ return 'pending';
675
+ }
676
+
677
+ function statusCol(label, dotCls, items) {
678
+ var h = '<div class="status-col">';
679
+ h += '<div class="status-col-header"><span class="status-col-dot ' + dotCls + '"></span>' + label + ' <span class="status-col-count">(' + items.length + ')</span></div>';
680
+ h += '<ul class="status-col-list">';
681
+ for (var i = 0; i < items.length; i++) h += '<li>' + esc(items[i]) + '</li>';
682
+ h += '</ul></div>';
683
+ return h;
684
+ }
685
+
686
+ function renderTodoItems(todoItems) {
687
+ if (!todoItems || todoItems.length === 0) return '';
688
+
689
+ // Find first undone item index
690
+ var currentIdx = -1;
691
+ for (var i = 0; i < todoItems.length; i++) {
692
+ if (!todoItems[i].done) { currentIdx = i; break; }
693
+ }
694
+
695
+ var h = '<div class="tasks-panel">';
696
+ h += '<div class="tasks-panel-header">TASKS</div>';
697
+
698
+ var lastSection = null;
699
+ for (var j = 0; j < todoItems.length; j++) {
700
+ var item = todoItems[j];
701
+ if (item.section && item.section !== lastSection) {
702
+ h += '<div class="tasks-section-label">' + esc(item.section) + '</div>';
703
+ lastSection = item.section;
704
+ }
705
+ var cls, icon;
706
+ if (item.done) {
707
+ cls = 'todo-done'; icon = '\u2713';
708
+ } else if (j === currentIdx) {
709
+ cls = 'todo-current'; icon = '\u2192';
710
+ } else {
711
+ cls = 'todo-pending'; icon = '\u25CB';
712
+ }
713
+ h += '<div class="todo-item ' + cls + '">';
714
+ h += '<span class="todo-item-icon">' + icon + '</span>';
715
+ h += '<span>' + esc(item.text) + '</span>';
716
+ h += '</div>';
717
+ }
718
+
719
+ h += '</div>';
720
+ return h;
721
+ }
722
+
723
+ // ── Render one worktree swimlane ──
724
+
725
+ function renderWorktree(wt, index, total) {
726
+ var steps = wt.steps || [];
727
+ var branch = wt.branch || 'unknown';
728
+ var taskName = wt.taskName || '';
729
+ var wtId = wt.path || branch;
730
+
731
+ // Set initial expand state
732
+ if (!initialized) {
733
+ expandedWorktrees[wtId] = (total === 1) || (index === 0);
734
+ }
735
+ var isExpanded = !!expandedWorktrees[wtId];
736
+
737
+ // Classify all 27 steps
738
+ var doneSteps = [];
739
+ var skippedSteps = [];
740
+ var pendingSteps = [];
741
+ var activeStep = null;
742
+
743
+ for (var i = 0; i < steps.length; i++) {
744
+ var step = steps[i];
745
+ var st = classifyStatus(step.status);
746
+ var label = step.name || ('Step ' + step.number);
747
+
748
+ if (st === 'done') {
749
+ doneSteps.push(label);
750
+ } else if (st === 'partial') {
751
+ doneSteps.push(label + ' (partial)');
752
+ } else if (st === 'skipped') {
753
+ skippedSteps.push(label);
754
+ } else if (st === 'active') {
755
+ activeStep = step;
756
+ pendingSteps.push(label);
757
+ } else {
758
+ pendingSteps.push(label);
759
+ }
760
+ }
761
+
762
+ var completed = doneSteps.length + skippedSteps.length;
763
+ var totalSteps = steps.length || 27;
764
+ var pct = Math.round((completed / totalSteps) * 100);
765
+
766
+ var h = '';
767
+ h += '<div class="swimlane' + (isExpanded ? ' expanded' : '') + '" data-wt="' + esc(wtId) + '">';
768
+
769
+ // ── Header row ──
770
+ h += '<div class="swimlane-header" data-wt-toggle="' + esc(wtId) + '">';
771
+ h += '<div class="swimlane-header-left">';
772
+ h += svgBranch();
773
+ h += '<span class="branch-name">' + esc(branch) + '</span>';
774
+ if (taskName) {
775
+ h += '<span class="header-dot">&bull;</span>';
776
+ h += '<span class="task-name">' + esc(taskName) + '</span>';
777
+ }
778
+ h += '</div>';
779
+ h += '<div class="swimlane-header-right">';
780
+ h += '<span class="progress-fraction">' + completed + '/' + totalSteps + '</span>';
781
+ h += '<span class="progress-pct">' + pct + '%</span>';
782
+ h += svgChevron();
783
+ h += '</div>';
784
+ h += '</div>';
785
+
786
+ // ── Progress bar ──
787
+ h += '<div class="progress-bar-track">';
788
+ h += '<div class="progress-bar-fill" style="width:' + pct + '%"></div>';
789
+ h += '</div>';
790
+
791
+ // ── Expanded body ──
792
+ h += '<div class="swimlane-body"><div class="swimlane-body-inner">';
793
+
794
+ // Phase timeline
795
+ h += '<div class="phase-timeline">';
796
+ for (var j = 0; j < steps.length; j++) {
797
+ var sd = steps[j];
798
+ var cls = classifyStatus(sd.status);
799
+ var cellCls = 'phase-cell status-' + cls + (sd.isHardGate ? ' hard-gate' : '');
800
+
801
+ h += '<div class="' + cellCls + '">';
802
+ h += sd.number;
803
+ h += '<div class="tooltip">';
804
+ h += '<div class="tt-name">' + esc(sd.name) + '</div>';
805
+ if (sd.command) h += '<div class="tt-cmd">' + esc(sd.command) + '</div>';
806
+ if (sd.notes) h += '<div class="tt-notes">' + esc(sd.notes) + '</div>';
807
+ h += '</div>';
808
+ h += '</div>';
809
+ }
810
+ h += '</div>';
811
+
812
+ // Legend
813
+ h += '<div class="legend">';
814
+ h += '<div class="legend-item"><div class="legend-dot ld-done"></div>Done</div>';
815
+ h += '<div class="legend-item"><div class="legend-dot ld-active"></div>Next</div>';
816
+ h += '<div class="legend-item"><div class="legend-dot ld-gate"></div>Hard Gate</div>';
817
+ h += '<div class="legend-item"><div class="legend-dot ld-skipped"></div>Skipped</div>';
818
+ h += '<div class="legend-item"><div class="legend-dot ld-partial"></div>Partial</div>';
819
+ h += '<div class="legend-item"><div class="legend-dot ld-pending"></div>Not Yet</div>';
820
+ h += '</div>';
821
+
822
+ // Active step card
823
+ if (activeStep) {
824
+ h += '<div class="active-step-card">';
825
+ h += '<span class="active-step-num">' + activeStep.number + '</span>';
826
+ h += '<div class="active-step-info">';
827
+ h += '<span class="active-step-name">' + esc(activeStep.name) + '</span>';
828
+ if (activeStep.command) {
829
+ h += '<span class="active-step-cmd">' + esc(activeStep.command) + '</span>';
830
+ }
831
+ h += '</div>';
832
+ h += '</div>';
833
+ }
834
+
835
+ // Status columns
836
+ h += '<div class="status-columns">';
837
+ h += statusCol('Done', 'sc-done', doneSteps);
838
+ h += statusCol('Skipped', 'sc-skipped', skippedSteps);
839
+ h += statusCol('Not Yet', 'sc-pending', pendingSteps);
840
+ h += '</div>'; // status-columns
841
+ h += renderTodoItems(wt.todoItems);
842
+ h += '</div></div>'; // swimlane-body-inner, swimlane-body
843
+ h += '</div>'; // swimlane
844
+
845
+ return h;
846
+ }
847
+
848
+ // ── Render all worktrees ──
849
+
850
+ function render(worktrees) {
851
+ if (!worktrees || worktrees.length === 0) {
852
+ contentEl.innerHTML = '<div class="empty-state">No active workflows detected.</div>';
853
+ document.title = 'ShipKit Mission Control';
854
+ return;
855
+ }
856
+
857
+ // Count active worktrees
858
+ var activeCount = 0;
859
+ for (var a = 0; a < worktrees.length; a++) {
860
+ if (worktrees[a].currentStep > 0) activeCount++;
861
+ }
862
+ document.title = 'ShipKit \u2014 ' + activeCount + ' active';
863
+
864
+ var html = '';
865
+ for (var i = 0; i < worktrees.length; i++) {
866
+ html += renderWorktree(worktrees[i], i, worktrees.length);
867
+ }
868
+ contentEl.innerHTML = html;
869
+ initialized = true;
870
+
871
+ // Bind toggle click handlers
872
+ var toggles = contentEl.querySelectorAll('[data-wt-toggle]');
873
+ for (var t = 0; t < toggles.length; t++) {
874
+ toggles[t].addEventListener('click', handleToggle);
875
+ }
876
+ }
877
+
878
+ function handleToggle(e) {
879
+ var el = e.currentTarget;
880
+ var wtId = el.getAttribute('data-wt-toggle');
881
+ expandedWorktrees[wtId] = !expandedWorktrees[wtId];
882
+ var swimlane = el.closest('.swimlane');
883
+ if (swimlane) swimlane.classList.toggle('expanded');
884
+ }
885
+
886
+ // ── Clock ──
887
+
888
+ function updateClock() {
889
+ var now = new Date();
890
+ var hh = String(now.getHours()).padStart(2, '0');
891
+ var mm = String(now.getMinutes()).padStart(2, '0');
892
+ clockEl.textContent = hh + ':' + mm;
893
+ }
894
+
895
+ // ── Footer ──
896
+
897
+ function updateFooter() {
898
+ var ago = Math.round((Date.now() - lastRefreshTime) / 1000);
899
+ footerEl.textContent = lastWorktreeCount + ' worktree' + (lastWorktreeCount !== 1 ? 's' : '') +
900
+ ' \u00B7 Last refresh: ' + ago + 's ago \u00B7 Port 3333';
901
+ }
902
+
903
+ // ── Polling ──
904
+
905
+ function fetchStatus() {
906
+ fetch('/api/status')
907
+ .then(function (res) {
908
+ if (!res.ok) throw new Error('HTTP ' + res.status);
909
+ return res.json();
910
+ })
911
+ .then(function (data) {
912
+ lastRefreshTime = Date.now();
913
+ var worktrees = Array.isArray(data) ? data : (data.worktrees || []);
914
+ lastWorktreeCount = worktrees.length;
915
+ var json = JSON.stringify(data);
916
+ if (json !== lastResponseJson) {
917
+ lastResponseJson = json;
918
+ render(worktrees);
919
+ }
920
+ updateFooter();
921
+ })
922
+ .catch(function (err) {
923
+ console.warn('Dashboard fetch failed:', err.message || err);
924
+ });
925
+ }
926
+
927
+ // ── Init ──
928
+
929
+ updateClock();
930
+ fetchStatus();
931
+
932
+ setInterval(fetchStatus, 3000);
933
+ setInterval(updateClock, 1000);
934
+ setInterval(updateFooter, 1000);
935
+ })();
936
+ </script>
937
+
938
+ </body>
939
+ </html>