@runcore-sh/runcore 0.1.7 → 0.1.9

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.
Files changed (173) hide show
  1. package/dist/access/manifest.d.ts +59 -0
  2. package/dist/access/manifest.d.ts.map +1 -0
  3. package/dist/access/manifest.js +251 -0
  4. package/dist/access/manifest.js.map +1 -0
  5. package/dist/activity/log.d.ts +1 -1
  6. package/dist/activity/log.d.ts.map +1 -1
  7. package/dist/agents/autonomous.d.ts.map +1 -1
  8. package/dist/agents/autonomous.js +38 -0
  9. package/dist/agents/autonomous.js.map +1 -1
  10. package/dist/agents/governance.d.ts +70 -0
  11. package/dist/agents/governance.d.ts.map +1 -0
  12. package/dist/agents/governance.js +220 -0
  13. package/dist/agents/governance.js.map +1 -0
  14. package/dist/agents/governed-spawn.d.ts +83 -0
  15. package/dist/agents/governed-spawn.d.ts.map +1 -0
  16. package/dist/agents/governed-spawn.js +186 -0
  17. package/dist/agents/governed-spawn.js.map +1 -0
  18. package/dist/agents/heartbeat.d.ts +91 -0
  19. package/dist/agents/heartbeat.d.ts.map +1 -0
  20. package/dist/agents/heartbeat.js +323 -0
  21. package/dist/agents/heartbeat.js.map +1 -0
  22. package/dist/agents/index.d.ts +4 -1
  23. package/dist/agents/index.d.ts.map +1 -1
  24. package/dist/agents/index.js +6 -1
  25. package/dist/agents/index.js.map +1 -1
  26. package/dist/agents/spawn-policy.d.ts +45 -0
  27. package/dist/agents/spawn-policy.d.ts.map +1 -0
  28. package/dist/agents/spawn-policy.js +202 -0
  29. package/dist/agents/spawn-policy.js.map +1 -0
  30. package/dist/alert.d.ts +16 -0
  31. package/dist/alert.d.ts.map +1 -0
  32. package/dist/alert.js +70 -0
  33. package/dist/alert.js.map +1 -0
  34. package/dist/cli.js +35 -27
  35. package/dist/cli.js.map +1 -1
  36. package/dist/credentials/store.d.ts +1 -1
  37. package/dist/credentials/store.d.ts.map +1 -1
  38. package/dist/credentials/store.js +14 -3
  39. package/dist/credentials/store.js.map +1 -1
  40. package/dist/crystallizer.d.ts +56 -0
  41. package/dist/crystallizer.d.ts.map +1 -0
  42. package/dist/crystallizer.js +159 -0
  43. package/dist/crystallizer.js.map +1 -0
  44. package/dist/distiller.d.ts +48 -0
  45. package/dist/distiller.d.ts.map +1 -0
  46. package/dist/distiller.js +140 -0
  47. package/dist/distiller.js.map +1 -0
  48. package/dist/google/auth.d.ts +2 -0
  49. package/dist/google/auth.d.ts.map +1 -1
  50. package/dist/google/auth.js +2 -0
  51. package/dist/google/auth.js.map +1 -1
  52. package/dist/integrations/gate.d.ts +40 -0
  53. package/dist/integrations/gate.d.ts.map +1 -0
  54. package/dist/integrations/gate.js +100 -0
  55. package/dist/integrations/gate.js.map +1 -0
  56. package/dist/lib/audit.d.ts +43 -0
  57. package/dist/lib/audit.d.ts.map +1 -0
  58. package/dist/lib/audit.js +120 -0
  59. package/dist/lib/audit.js.map +1 -0
  60. package/dist/lib/brain-io.d.ts.map +1 -1
  61. package/dist/lib/brain-io.js +52 -0
  62. package/dist/lib/brain-io.js.map +1 -1
  63. package/dist/lib/dpapi.d.ts +14 -0
  64. package/dist/lib/dpapi.d.ts.map +1 -0
  65. package/dist/lib/dpapi.js +104 -0
  66. package/dist/lib/dpapi.js.map +1 -0
  67. package/dist/lib/glob-match.d.ts +22 -0
  68. package/dist/lib/glob-match.d.ts.map +1 -0
  69. package/dist/lib/glob-match.js +64 -0
  70. package/dist/lib/glob-match.js.map +1 -0
  71. package/dist/lib/locked.d.ts +40 -0
  72. package/dist/lib/locked.d.ts.map +1 -0
  73. package/dist/lib/locked.js +130 -0
  74. package/dist/lib/locked.js.map +1 -0
  75. package/dist/llm/complete.d.ts.map +1 -1
  76. package/dist/llm/complete.js +5 -2
  77. package/dist/llm/complete.js.map +1 -1
  78. package/dist/llm/fetch-guard.d.ts +16 -0
  79. package/dist/llm/fetch-guard.d.ts.map +1 -0
  80. package/dist/llm/fetch-guard.js +61 -0
  81. package/dist/llm/fetch-guard.js.map +1 -0
  82. package/dist/llm/guard.d.ts +40 -0
  83. package/dist/llm/guard.d.ts.map +1 -0
  84. package/dist/llm/guard.js +88 -0
  85. package/dist/llm/guard.js.map +1 -0
  86. package/dist/llm/membrane.d.ts +46 -0
  87. package/dist/llm/membrane.d.ts.map +1 -0
  88. package/dist/llm/membrane.js +123 -0
  89. package/dist/llm/membrane.js.map +1 -0
  90. package/dist/llm/providers/index.d.ts +5 -1
  91. package/dist/llm/providers/index.d.ts.map +1 -1
  92. package/dist/llm/providers/index.js +8 -1
  93. package/dist/llm/providers/index.js.map +1 -1
  94. package/dist/llm/redact.d.ts +39 -0
  95. package/dist/llm/redact.d.ts.map +1 -0
  96. package/dist/llm/redact.js +155 -0
  97. package/dist/llm/redact.js.map +1 -0
  98. package/dist/llm/sensitive-registry.d.ts +33 -0
  99. package/dist/llm/sensitive-registry.d.ts.map +1 -0
  100. package/dist/llm/sensitive-registry.js +106 -0
  101. package/dist/llm/sensitive-registry.js.map +1 -0
  102. package/dist/mcp-server.d.ts +11 -0
  103. package/dist/mcp-server.d.ts.map +1 -0
  104. package/dist/mcp-server.js +520 -0
  105. package/dist/mcp-server.js.map +1 -0
  106. package/dist/mdns.d.ts +17 -0
  107. package/dist/mdns.d.ts.map +1 -0
  108. package/dist/mdns.js +110 -0
  109. package/dist/mdns.js.map +1 -0
  110. package/dist/nerve/push.d.ts +26 -0
  111. package/dist/nerve/push.d.ts.map +1 -0
  112. package/dist/nerve/push.js +170 -0
  113. package/dist/nerve/push.js.map +1 -0
  114. package/dist/nerve/state.d.ts +35 -0
  115. package/dist/nerve/state.d.ts.map +1 -0
  116. package/dist/nerve/state.js +257 -0
  117. package/dist/nerve/state.js.map +1 -0
  118. package/dist/resend/inbox.d.ts +23 -0
  119. package/dist/resend/inbox.d.ts.map +1 -0
  120. package/dist/resend/inbox.js +198 -0
  121. package/dist/resend/inbox.js.map +1 -0
  122. package/dist/resend/webhooks.d.ts +30 -0
  123. package/dist/resend/webhooks.d.ts.map +1 -0
  124. package/dist/resend/webhooks.js +244 -0
  125. package/dist/resend/webhooks.js.map +1 -0
  126. package/dist/server.d.ts +2 -0
  127. package/dist/server.d.ts.map +1 -1
  128. package/dist/server.js +585 -16
  129. package/dist/server.js.map +1 -1
  130. package/dist/settings.d.ts +14 -1
  131. package/dist/settings.d.ts.map +1 -1
  132. package/dist/settings.js +35 -1
  133. package/dist/settings.js.map +1 -1
  134. package/dist/updater.d.ts +32 -0
  135. package/dist/updater.d.ts.map +1 -0
  136. package/dist/updater.js +145 -0
  137. package/dist/updater.js.map +1 -0
  138. package/dist/vault/policy.d.ts +42 -0
  139. package/dist/vault/policy.d.ts.map +1 -0
  140. package/dist/vault/policy.js +159 -0
  141. package/dist/vault/policy.js.map +1 -0
  142. package/dist/vault/store.d.ts +6 -0
  143. package/dist/vault/store.d.ts.map +1 -1
  144. package/dist/vault/store.js +15 -5
  145. package/dist/vault/store.js.map +1 -1
  146. package/dist/vault/transfer.d.ts +33 -0
  147. package/dist/vault/transfer.d.ts.map +1 -0
  148. package/dist/vault/transfer.js +187 -0
  149. package/dist/vault/transfer.js.map +1 -0
  150. package/dist/voucher.d.ts +39 -0
  151. package/dist/voucher.d.ts.map +1 -0
  152. package/dist/voucher.js +105 -0
  153. package/dist/voucher.js.map +1 -0
  154. package/dist/webhooks/handlers.d.ts +10 -0
  155. package/dist/webhooks/handlers.d.ts.map +1 -1
  156. package/dist/webhooks/handlers.js +53 -0
  157. package/dist/webhooks/handlers.js.map +1 -1
  158. package/dist/webhooks/index.d.ts +2 -2
  159. package/dist/webhooks/index.d.ts.map +1 -1
  160. package/dist/webhooks/index.js +2 -2
  161. package/dist/webhooks/index.js.map +1 -1
  162. package/dist/webhooks/verify.d.ts +8 -0
  163. package/dist/webhooks/verify.d.ts.map +1 -1
  164. package/dist/webhooks/verify.js +56 -0
  165. package/dist/webhooks/verify.js.map +1 -1
  166. package/package.json +8 -2
  167. package/public/board.html +8 -3
  168. package/public/browser.html +8 -3
  169. package/public/library.html +8 -3
  170. package/public/observatory.html +8 -3
  171. package/public/ops.html +8 -3
  172. package/public/registry.html +627 -0
  173. package/public/roadmap.html +975 -0
@@ -0,0 +1,975 @@
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>{{INSTANCE_NAME}} — Roadmap</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0e0e10;
10
+ --surface: #18181b;
11
+ --surface2: #222226;
12
+ --border: #2e2e33;
13
+ --text: #e4e4e7;
14
+ --text-dim: #8b8b94;
15
+ --accent: #6d5dfc;
16
+ --green: #22c55e;
17
+ --red: #ef4444;
18
+ --yellow: #eab308;
19
+ --orange: #f97316;
20
+ --blue: #3b82f6;
21
+ --mono: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
22
+ }
23
+
24
+ * { margin: 0; padding: 0; box-sizing: border-box; }
25
+
26
+ body {
27
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ min-height: 100vh;
31
+ line-height: 1.5;
32
+ }
33
+
34
+ header {
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: space-between;
38
+ padding: 16px 24px;
39
+ border-bottom: 1px solid var(--border);
40
+ background: var(--surface);
41
+ }
42
+
43
+ header h1 {
44
+ font-size: 18px;
45
+ font-weight: 600;
46
+ letter-spacing: -0.3px;
47
+ }
48
+
49
+ header h1 a {
50
+ color: var(--text);
51
+ text-decoration: none;
52
+ }
53
+
54
+ header h1 a:hover { opacity: 0.8; }
55
+ header h1 span { color: var(--text-dim); font-weight: 400; }
56
+
57
+ .header-nav {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 8px;
61
+ margin-left: 16px;
62
+ }
63
+
64
+ .header-nav a {
65
+ color: var(--text-dim);
66
+ text-decoration: none;
67
+ font-size: 13px;
68
+ padding: 4px 10px;
69
+ border-radius: 6px;
70
+ border: 1px solid transparent;
71
+ transition: all 0.15s;
72
+ }
73
+
74
+ .header-nav a:hover { color: var(--text); border-color: var(--accent); background: rgba(109,93,252,0.1); }
75
+ .header-nav a.active { color: var(--text); border-color: var(--border); background: var(--surface2); }
76
+
77
+ .header-nav .nav-divider {
78
+ width: 1px;
79
+ height: 18px;
80
+ background: var(--border);
81
+ margin: 0 4px;
82
+ align-self: center;
83
+ }
84
+
85
+ /* --- Toolbar --- */
86
+ .roadmap-toolbar {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 8px;
90
+ padding: 12px 24px;
91
+ border-bottom: 1px solid var(--border);
92
+ background: var(--surface);
93
+ flex-wrap: wrap;
94
+ }
95
+
96
+ .toolbar-label {
97
+ font-size: 12px;
98
+ color: var(--text-dim);
99
+ text-transform: uppercase;
100
+ letter-spacing: 0.5px;
101
+ margin-right: 4px;
102
+ }
103
+
104
+ .filter-chip {
105
+ font-size: 12px;
106
+ padding: 4px 10px;
107
+ border-radius: 12px;
108
+ border: 1px solid var(--border);
109
+ background: transparent;
110
+ color: var(--text-dim);
111
+ cursor: pointer;
112
+ transition: all 0.15s;
113
+ font-family: inherit;
114
+ }
115
+
116
+ .filter-chip:hover {
117
+ border-color: var(--text-dim);
118
+ color: var(--text);
119
+ }
120
+
121
+ .filter-chip.active {
122
+ background: var(--chip-color, var(--accent));
123
+ border-color: var(--chip-color, var(--accent));
124
+ color: #fff;
125
+ }
126
+
127
+ .filter-chip[data-stream="all"].active {
128
+ background: var(--surface2);
129
+ border-color: var(--border);
130
+ color: var(--text);
131
+ }
132
+
133
+ .status-filter {
134
+ margin-left: auto;
135
+ font-size: 12px;
136
+ padding: 4px 8px;
137
+ border-radius: 6px;
138
+ border: 1px solid var(--border);
139
+ background: var(--surface2);
140
+ color: var(--text);
141
+ font-family: inherit;
142
+ cursor: pointer;
143
+ }
144
+
145
+ .status-filter option { background: var(--surface); }
146
+
147
+ /* --- Graph area --- */
148
+ .roadmap-wrapper {
149
+ display: flex;
150
+ flex: 1;
151
+ overflow: hidden;
152
+ height: calc(100vh - 110px);
153
+ }
154
+
155
+ .stream-sidebar {
156
+ flex-shrink: 0;
157
+ width: 140px;
158
+ border-right: 1px solid var(--border);
159
+ background: var(--surface);
160
+ overflow-y: auto;
161
+ z-index: 2;
162
+ }
163
+
164
+ .stream-label {
165
+ height: 72px;
166
+ display: flex;
167
+ align-items: center;
168
+ padding: 0 16px;
169
+ font-size: 13px;
170
+ font-weight: 500;
171
+ border-bottom: 1px solid var(--border);
172
+ gap: 8px;
173
+ transition: opacity 0.2s;
174
+ }
175
+
176
+ .stream-label.hidden { opacity: 0.25; }
177
+
178
+ .stream-dot {
179
+ width: 8px;
180
+ height: 8px;
181
+ border-radius: 50%;
182
+ flex-shrink: 0;
183
+ }
184
+
185
+ .graph-scroll {
186
+ flex: 1;
187
+ overflow-x: auto;
188
+ overflow-y: hidden;
189
+ }
190
+
191
+ .graph-container {
192
+ position: relative;
193
+ min-width: 100%;
194
+ height: 100%;
195
+ }
196
+
197
+ /* Phase headers */
198
+ .phase-headers {
199
+ display: flex;
200
+ height: 48px;
201
+ border-bottom: 1px solid var(--border);
202
+ position: sticky;
203
+ top: 0;
204
+ z-index: 1;
205
+ }
206
+
207
+ .phase-header {
208
+ flex: 1;
209
+ display: flex;
210
+ flex-direction: column;
211
+ align-items: center;
212
+ justify-content: center;
213
+ border-right: 1px solid var(--border);
214
+ background: var(--surface);
215
+ min-width: 200px;
216
+ }
217
+
218
+ .phase-header:last-child { border-right: none; }
219
+
220
+ .phase-name {
221
+ font-size: 13px;
222
+ font-weight: 600;
223
+ color: var(--text);
224
+ }
225
+
226
+ .phase-range {
227
+ font-size: 11px;
228
+ color: var(--text-dim);
229
+ }
230
+
231
+ /* SVG graph */
232
+ .graph-svg {
233
+ position: absolute;
234
+ top: 48px;
235
+ left: 0;
236
+ }
237
+
238
+ /* Tooltip */
239
+ .tooltip {
240
+ position: fixed;
241
+ background: var(--surface2);
242
+ border: 1px solid var(--border);
243
+ border-radius: 8px;
244
+ padding: 10px 14px;
245
+ font-size: 12px;
246
+ pointer-events: none;
247
+ opacity: 0;
248
+ transition: opacity 0.15s;
249
+ z-index: 100;
250
+ max-width: 280px;
251
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5);
252
+ }
253
+
254
+ .tooltip.visible { opacity: 1; }
255
+
256
+ .tooltip-title {
257
+ font-weight: 600;
258
+ margin-bottom: 4px;
259
+ color: var(--text);
260
+ }
261
+
262
+ .tooltip-meta {
263
+ color: var(--text-dim);
264
+ line-height: 1.6;
265
+ }
266
+
267
+ .tooltip-status {
268
+ display: inline-block;
269
+ padding: 1px 6px;
270
+ border-radius: 4px;
271
+ font-size: 11px;
272
+ font-weight: 500;
273
+ }
274
+
275
+ /* Loading */
276
+ .loading {
277
+ display: flex;
278
+ align-items: center;
279
+ justify-content: center;
280
+ height: 100%;
281
+ color: var(--text-dim);
282
+ font-size: 14px;
283
+ }
284
+
285
+ /* --- View tabs --- */
286
+ .view-tabs {
287
+ display: flex;
288
+ gap: 0;
289
+ margin-right: 12px;
290
+ }
291
+
292
+ .view-tab {
293
+ font-size: 12px;
294
+ padding: 5px 14px;
295
+ border: 1px solid var(--border);
296
+ background: transparent;
297
+ color: var(--text-dim);
298
+ cursor: pointer;
299
+ font-family: inherit;
300
+ transition: all 0.15s;
301
+ }
302
+
303
+ .view-tab:first-child { border-radius: 6px 0 0 6px; }
304
+ .view-tab:last-child { border-radius: 0 6px 6px 0; border-left: none; }
305
+
306
+ .view-tab.active {
307
+ background: var(--accent);
308
+ border-color: var(--accent);
309
+ color: #fff;
310
+ }
311
+
312
+ .view-tab:hover:not(.active) {
313
+ border-color: var(--text-dim);
314
+ color: var(--text);
315
+ }
316
+
317
+ /* --- Rearview timeline --- */
318
+ .rearview-container {
319
+ flex: 1;
320
+ overflow-y: auto;
321
+ padding: 24px;
322
+ display: none;
323
+ }
324
+
325
+ .rearview-container.active { display: block; }
326
+
327
+ .rearview-stats {
328
+ display: flex;
329
+ gap: 16px;
330
+ margin-bottom: 24px;
331
+ }
332
+
333
+ .rearview-stat {
334
+ background: var(--surface);
335
+ border: 1px solid var(--border);
336
+ border-radius: 8px;
337
+ padding: 12px 20px;
338
+ min-width: 120px;
339
+ }
340
+
341
+ .rearview-stat-value {
342
+ font-size: 24px;
343
+ font-weight: 700;
344
+ font-family: var(--mono);
345
+ }
346
+
347
+ .rearview-stat-label {
348
+ font-size: 11px;
349
+ color: var(--text-dim);
350
+ text-transform: uppercase;
351
+ letter-spacing: 0.5px;
352
+ margin-top: 2px;
353
+ }
354
+
355
+ .hour-group {
356
+ margin-bottom: 24px;
357
+ }
358
+
359
+ .hour-label {
360
+ font-size: 12px;
361
+ font-family: var(--mono);
362
+ color: var(--text-dim);
363
+ padding: 4px 0;
364
+ margin-bottom: 8px;
365
+ border-bottom: 1px solid var(--border);
366
+ display: flex;
367
+ align-items: center;
368
+ gap: 8px;
369
+ }
370
+
371
+ .hour-count {
372
+ background: var(--surface2);
373
+ border-radius: 10px;
374
+ padding: 1px 8px;
375
+ font-size: 11px;
376
+ }
377
+
378
+ .commit-item {
379
+ display: flex;
380
+ align-items: flex-start;
381
+ gap: 12px;
382
+ padding: 8px 12px;
383
+ border-radius: 6px;
384
+ transition: background 0.1s;
385
+ }
386
+
387
+ .commit-item:hover { background: var(--surface); }
388
+
389
+ .commit-hash {
390
+ font-family: var(--mono);
391
+ font-size: 12px;
392
+ color: var(--accent);
393
+ flex-shrink: 0;
394
+ min-width: 70px;
395
+ }
396
+
397
+ .commit-msg {
398
+ font-size: 13px;
399
+ color: var(--text);
400
+ flex: 1;
401
+ }
402
+
403
+ .commit-tag {
404
+ font-size: 10px;
405
+ padding: 1px 6px;
406
+ border-radius: 4px;
407
+ font-weight: 600;
408
+ text-transform: uppercase;
409
+ letter-spacing: 0.3px;
410
+ flex-shrink: 0;
411
+ }
412
+
413
+ .commit-tag.dash {
414
+ background: rgba(109,93,252,0.2);
415
+ color: var(--accent);
416
+ }
417
+
418
+ .commit-tag.human {
419
+ background: rgba(34,197,94,0.15);
420
+ color: var(--green);
421
+ }
422
+
423
+ .commit-time {
424
+ font-size: 11px;
425
+ font-family: var(--mono);
426
+ color: var(--text-dim);
427
+ flex-shrink: 0;
428
+ min-width: 50px;
429
+ }
430
+
431
+ .rearview-empty {
432
+ text-align: center;
433
+ color: var(--text-dim);
434
+ padding: 48px;
435
+ font-size: 14px;
436
+ }
437
+
438
+ .hours-select {
439
+ font-size: 12px;
440
+ padding: 4px 8px;
441
+ border-radius: 6px;
442
+ border: 1px solid var(--border);
443
+ background: var(--surface2);
444
+ color: var(--text);
445
+ font-family: inherit;
446
+ cursor: pointer;
447
+ margin-left: auto;
448
+ }
449
+
450
+ .hours-select option { background: var(--surface); }
451
+ </style>
452
+ </head>
453
+ <body>
454
+
455
+ <header>
456
+ <div style="display:flex;align-items:center;">
457
+ <h1><a href="/">{{INSTANCE_NAME}}</a> <span>· Roadmap</span></h1>
458
+ <nav class="header-nav">
459
+ <a href="/">Chat</a>
460
+ <a href="/library">Library</a>
461
+ <a href="/personal">Personal</a>
462
+ <a href="/life">Life</a>
463
+ <a href="/registry">Registry</a>
464
+ <span class="nav-divider"></span>
465
+ <a href="/observatory">Observatory</a>
466
+ <a href="/ops">Operations</a>
467
+ <a href="/board">Board</a>
468
+ <a href="/roadmap" class="active">Roadmap</a>
469
+ </nav>
470
+ </div>
471
+ </header>
472
+
473
+ <div class="roadmap-toolbar" id="toolbar">
474
+ <div class="view-tabs">
475
+ <button class="view-tab active" data-view="roadmap" onclick="switchView('roadmap')">Roadmap</button>
476
+ <button class="view-tab" data-view="rearview" onclick="switchView('rearview')">Rearview</button>
477
+ </div>
478
+ </div>
479
+
480
+ <div class="roadmap-wrapper" id="roadmapView">
481
+ <div class="stream-sidebar" id="sidebar"></div>
482
+ <div class="graph-scroll" id="graphScroll">
483
+ <div class="graph-container" id="graphContainer">
484
+ <div class="phase-headers" id="phaseHeaders"></div>
485
+ <svg class="graph-svg" id="graphSvg"></svg>
486
+ </div>
487
+ <div class="loading" id="loading">Loading roadmap...</div>
488
+ </div>
489
+ </div>
490
+
491
+ <div class="rearview-container" id="rearviewView"></div>
492
+
493
+ <div class="tooltip" id="tooltip">
494
+ <div class="tooltip-title" id="tooltipTitle"></div>
495
+ <div class="tooltip-meta" id="tooltipMeta"></div>
496
+ </div>
497
+
498
+ <script>
499
+ // --- View switching (global scope for inline onclick) ---
500
+ let currentView = 'roadmap';
501
+ let rearviewLoaded = false;
502
+
503
+ function switchView(view) {
504
+ currentView = view;
505
+ document.querySelectorAll('.view-tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
506
+ document.getElementById('roadmapView').style.display = view === 'roadmap' ? 'flex' : 'none';
507
+ document.getElementById('rearviewView').classList.toggle('active', view === 'rearview');
508
+
509
+ // Show/hide roadmap toolbar items (stream filters, status filter)
510
+ document.querySelectorAll('#toolbar .toolbar-label, #toolbar .filter-chip, #toolbar .status-filter, #toolbar .hours-select').forEach(el => {
511
+ if (view === 'roadmap') {
512
+ el.style.display = el.classList.contains('hours-select') ? 'none' : '';
513
+ } else {
514
+ el.style.display = el.classList.contains('hours-select') ? '' : (el.classList.contains('toolbar-label') || el.classList.contains('filter-chip') || el.classList.contains('status-filter') ? 'none' : '');
515
+ }
516
+ });
517
+
518
+ if (view === 'rearview' && !rearviewLoaded) {
519
+ loadRearview();
520
+ }
521
+ }
522
+
523
+ async function loadRearview(hours) {
524
+ hours = hours || 24;
525
+ const container = document.getElementById('rearviewView');
526
+ container.innerHTML = '<div class="loading">Loading recent activity...</div>';
527
+
528
+ try {
529
+ const res = await fetch('/api/roadmap/recent?hours=' + hours);
530
+ if (!res.ok) throw new Error('Failed to load');
531
+ const data = await res.json();
532
+ rearviewLoaded = true;
533
+ renderRearview(data, container);
534
+ } catch (err) {
535
+ container.innerHTML = '<div class="rearview-empty">Failed to load recent activity</div>';
536
+ }
537
+ }
538
+
539
+ function renderRearview(data, container) {
540
+ if (!data.commits || data.commits.length === 0) {
541
+ container.innerHTML = '<div class="rearview-empty">No commits in the last ' + data.hours + ' hours</div>';
542
+ return;
543
+ }
544
+
545
+ const autoCount = data.commits.filter(c => c.autonomous).length;
546
+ const humanCount = data.commits.length - autoCount;
547
+
548
+ let html = '<div class="rearview-stats">';
549
+ html += '<div class="rearview-stat"><div class="rearview-stat-value">' + data.total + '</div><div class="rearview-stat-label">Commits</div></div>';
550
+ html += '<div class="rearview-stat"><div class="rearview-stat-value" style="color:var(--accent)">' + autoCount + '</div><div class="rearview-stat-label">Autonomous</div></div>';
551
+ html += '<div class="rearview-stat"><div class="rearview-stat-value" style="color:var(--green)">' + humanCount + '</div><div class="rearview-stat-label">Human / Other</div></div>';
552
+ html += '<div class="rearview-stat"><div class="rearview-stat-value">' + data.groups.length + '</div><div class="rearview-stat-label">Active Hours</div></div>';
553
+ html += '</div>';
554
+
555
+ for (const group of data.groups) {
556
+ html += '<div class="hour-group">';
557
+ html += '<div class="hour-label">' + group.hour + '<span class="hour-count">' + group.count + ' commit' + (group.count !== 1 ? 's' : '') + '</span></div>';
558
+
559
+ for (const c of group.commits) {
560
+ const time = new Date(c.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
561
+ const escapedMsg = c.message.replace(/</g, '&lt;').replace(/>/g, '&gt;');
562
+ html += '<div class="commit-item">';
563
+ html += '<span class="commit-time">' + time + '</span>';
564
+ html += '<span class="commit-hash">' + c.hash + '</span>';
565
+ html += '<span class="commit-msg">' + escapedMsg + '</span>';
566
+ html += '<span class="commit-tag ' + c.tag + '">' + c.tag + '</span>';
567
+ html += '</div>';
568
+ }
569
+
570
+ html += '</div>';
571
+ }
572
+
573
+ container.innerHTML = html;
574
+ }
575
+
576
+ (function() {
577
+ const ROW_H = 72;
578
+ const NODE_R = 7;
579
+ const PHASE_MIN_W = 220;
580
+
581
+ let data = null;
582
+ let activeStreams = new Set();
583
+ let statusFilter = 'all';
584
+
585
+ // --- Fetch & parse ---
586
+ async function load() {
587
+ const res = await fetch('/api/roadmap');
588
+ if (!res.ok) { document.getElementById('loading').textContent = 'Failed to load roadmap'; return; }
589
+ data = await res.json();
590
+ activeStreams = new Set(data.streams.map(s => s.id));
591
+ document.getElementById('loading').style.display = 'none';
592
+ buildToolbar();
593
+ render();
594
+ }
595
+
596
+ // --- Toolbar ---
597
+ function buildToolbar() {
598
+ const tb = document.getElementById('toolbar');
599
+ tb.innerHTML = '';
600
+
601
+ // View tabs (re-add since we cleared)
602
+ const viewTabs = document.createElement('div');
603
+ viewTabs.className = 'view-tabs';
604
+ viewTabs.innerHTML = '<button class="view-tab active" data-view="roadmap" onclick="switchView(\'roadmap\')">Roadmap</button><button class="view-tab" data-view="rearview" onclick="switchView(\'rearview\')">Rearview</button>';
605
+ tb.appendChild(viewTabs);
606
+
607
+ const lbl = document.createElement('span');
608
+ lbl.className = 'toolbar-label';
609
+ lbl.textContent = 'Streams';
610
+ tb.appendChild(lbl);
611
+
612
+ // All chip
613
+ const allChip = makeChip('All', 'all', null, true);
614
+ allChip.addEventListener('click', () => {
615
+ if (activeStreams.size === data.streams.length) {
616
+ activeStreams.clear();
617
+ } else {
618
+ activeStreams = new Set(data.streams.map(s => s.id));
619
+ }
620
+ syncChips();
621
+ render();
622
+ });
623
+ tb.appendChild(allChip);
624
+
625
+ for (const s of data.streams) {
626
+ const chip = makeChip(s.name, s.id, s.color, true);
627
+ chip.addEventListener('click', () => {
628
+ if (activeStreams.has(s.id)) activeStreams.delete(s.id);
629
+ else activeStreams.add(s.id);
630
+ syncChips();
631
+ render();
632
+ });
633
+ tb.appendChild(chip);
634
+ }
635
+
636
+ // Status filter
637
+ const sel = document.createElement('select');
638
+ sel.className = 'status-filter';
639
+ sel.id = 'statusFilter';
640
+ [['all', 'All statuses'], ['active', 'Active (in_progress + planned)'], ['done', 'Done'], ['research', 'Research']].forEach(([v, l]) => {
641
+ const o = document.createElement('option');
642
+ o.value = v; o.textContent = l;
643
+ sel.appendChild(o);
644
+ });
645
+ sel.addEventListener('change', () => { statusFilter = sel.value; render(); });
646
+ tb.appendChild(sel);
647
+
648
+ // Hours selector (for rearview, hidden by default)
649
+ const hoursSel = document.createElement('select');
650
+ hoursSel.className = 'hours-select';
651
+ hoursSel.style.display = 'none';
652
+ [['24', 'Last 24 hours'], ['48', 'Last 48 hours'], ['72', 'Last 3 days'], ['168', 'Last 7 days']].forEach(([v, l]) => {
653
+ const o = document.createElement('option');
654
+ o.value = v; o.textContent = l;
655
+ hoursSel.appendChild(o);
656
+ });
657
+ hoursSel.addEventListener('change', () => { rearviewLoaded = false; loadRearview(parseInt(hoursSel.value)); });
658
+ tb.appendChild(hoursSel);
659
+
660
+ syncChips();
661
+ }
662
+
663
+ function makeChip(label, stream, color) {
664
+ const btn = document.createElement('button');
665
+ btn.className = 'filter-chip';
666
+ btn.dataset.stream = stream;
667
+ btn.textContent = label;
668
+ if (color) btn.style.setProperty('--chip-color', color);
669
+ return btn;
670
+ }
671
+
672
+ function syncChips() {
673
+ const allActive = activeStreams.size === data.streams.length;
674
+ document.querySelectorAll('.filter-chip').forEach(c => {
675
+ if (c.dataset.stream === 'all') {
676
+ c.classList.toggle('active', allActive);
677
+ } else {
678
+ c.classList.toggle('active', activeStreams.has(c.dataset.stream));
679
+ }
680
+ });
681
+ }
682
+
683
+ // --- Rendering ---
684
+ function render() {
685
+ if (!data) return;
686
+
687
+ const phases = data.phases;
688
+ const streams = data.streams;
689
+ const milestones = data.milestones;
690
+
691
+ // Filter milestones by status
692
+ const filteredMs = milestones.filter(m => {
693
+ if (statusFilter === 'done') return m.status === 'done';
694
+ if (statusFilter === 'active') return m.status === 'in_progress' || m.status === 'planned';
695
+ if (statusFilter === 'research') return m.status === 'research';
696
+ return true;
697
+ });
698
+
699
+ // Phase index for x positioning
700
+ const phaseIdx = {};
701
+ phases.forEach((p, i) => { phaseIdx[p.id] = i; });
702
+ const phaseCount = phases.length;
703
+ const phaseW = Math.max(PHASE_MIN_W, (window.innerWidth - 140) / phaseCount);
704
+ const totalW = phaseW * phaseCount;
705
+
706
+ // Stream index for y positioning
707
+ const streamIdx = {};
708
+ streams.forEach((s, i) => { streamIdx[s.id] = i; });
709
+ const streamMap = {};
710
+ streams.forEach(s => { streamMap[s.id] = s; });
711
+
712
+ const totalH = streams.length * ROW_H;
713
+
714
+ // Phase headers
715
+ const phEl = document.getElementById('phaseHeaders');
716
+ phEl.innerHTML = '';
717
+ phEl.style.width = totalW + 'px';
718
+ for (const p of phases) {
719
+ const d = document.createElement('div');
720
+ d.className = 'phase-header';
721
+ d.style.minWidth = phaseW + 'px';
722
+ d.style.maxWidth = phaseW + 'px';
723
+ d.innerHTML = `<div class="phase-name">${p.label}</div><div class="phase-range">${p.start}${p.end !== p.start ? ' – ' + p.end : ''}</div>`;
724
+ phEl.appendChild(d);
725
+ }
726
+
727
+ // Sidebar
728
+ const sb = document.getElementById('sidebar');
729
+ sb.innerHTML = '<div style="height:48px;border-bottom:1px solid var(--border);"></div>';
730
+ for (const s of streams) {
731
+ const d = document.createElement('div');
732
+ d.className = 'stream-label' + (activeStreams.has(s.id) ? '' : ' hidden');
733
+ d.innerHTML = `<span class="stream-dot" style="background:${s.color}"></span>${s.name}`;
734
+ sb.appendChild(d);
735
+ }
736
+
737
+ // Graph container sizing
738
+ const gc = document.getElementById('graphContainer');
739
+ gc.style.width = totalW + 'px';
740
+ gc.style.height = (totalH + 48) + 'px';
741
+
742
+ // SVG
743
+ const svg = document.getElementById('graphSvg');
744
+ svg.setAttribute('width', totalW);
745
+ svg.setAttribute('height', totalH);
746
+ svg.innerHTML = '';
747
+
748
+ // Phase background bands
749
+ for (let i = 0; i < phaseCount; i++) {
750
+ const rect = svgEl('rect', {
751
+ x: i * phaseW, y: 0, width: phaseW, height: totalH,
752
+ fill: i % 2 === 0 ? 'rgba(255,255,255,0.01)' : 'rgba(255,255,255,0.025)',
753
+ });
754
+ svg.appendChild(rect);
755
+ }
756
+
757
+ // Row separator lines
758
+ for (let i = 0; i < streams.length; i++) {
759
+ const line = svgEl('line', {
760
+ x1: 0, y1: (i + 1) * ROW_H, x2: totalW, y2: (i + 1) * ROW_H,
761
+ stroke: '#2e2e33', 'stroke-width': 1
762
+ });
763
+ svg.appendChild(line);
764
+ }
765
+
766
+ // Compute milestone positions
767
+ const msPositions = {};
768
+ const phaseStreamGroups = {};
769
+ for (const m of filteredMs) {
770
+ const key = m.phase + ':' + m.stream;
771
+ if (!phaseStreamGroups[key]) phaseStreamGroups[key] = [];
772
+ phaseStreamGroups[key].push(m.id);
773
+ }
774
+
775
+ for (const m of filteredMs) {
776
+ const px = phaseIdx[m.phase];
777
+ const sy = streamIdx[m.stream];
778
+ if (px === undefined || sy === undefined) continue;
779
+ const key = m.phase + ':' + m.stream;
780
+ const group = phaseStreamGroups[key];
781
+ const posInGroup = group.indexOf(m.id);
782
+ const groupSpread = phaseW / (group.length + 1);
783
+
784
+ msPositions[m.id] = {
785
+ x: px * phaseW + groupSpread * (posInGroup + 1),
786
+ y: sy * ROW_H + ROW_H / 2,
787
+ milestone: m
788
+ };
789
+ }
790
+
791
+ // Draw stream lines (connecting milestones within same stream)
792
+ for (const s of streams) {
793
+ if (!activeStreams.has(s.id)) continue;
794
+ const streamMs = filteredMs
795
+ .filter(m => m.stream === s.id && msPositions[m.id])
796
+ .sort((a, b) => msPositions[a.id].x - msPositions[b.id].x);
797
+
798
+ if (streamMs.length < 2) continue;
799
+
800
+ let pathD = `M ${msPositions[streamMs[0].id].x} ${msPositions[streamMs[0].id].y}`;
801
+ for (let i = 1; i < streamMs.length; i++) {
802
+ pathD += ` L ${msPositions[streamMs[i].id].x} ${msPositions[streamMs[i].id].y}`;
803
+ }
804
+
805
+ const path = svgEl('path', {
806
+ d: pathD,
807
+ stroke: s.color,
808
+ 'stroke-width': 2,
809
+ fill: 'none',
810
+ opacity: 0.5,
811
+ });
812
+ svg.appendChild(path);
813
+ }
814
+
815
+ // Draw dependency arrows
816
+ const defs = svgEl('defs', {});
817
+ for (const s of streams) {
818
+ const marker = svgEl('marker', {
819
+ id: 'arrow-' + s.id,
820
+ viewBox: '0 0 10 6',
821
+ refX: 10, refY: 3,
822
+ markerWidth: 8, markerHeight: 6,
823
+ orient: 'auto-start-reverse'
824
+ });
825
+ const markerPath = svgEl('path', { d: 'M 0 0 L 10 3 L 0 6 z', fill: s.color, opacity: 0.6 });
826
+ marker.appendChild(markerPath);
827
+ defs.appendChild(marker);
828
+ }
829
+ svg.appendChild(defs);
830
+
831
+ for (const m of filteredMs) {
832
+ if (!m.depends_on || !msPositions[m.id]) continue;
833
+ if (!activeStreams.has(m.stream)) continue;
834
+
835
+ for (const depId of m.depends_on) {
836
+ const from = msPositions[depId];
837
+ const to = msPositions[m.id];
838
+ if (!from || !to) continue;
839
+ if (!activeStreams.has(from.milestone.stream)) continue;
840
+
841
+ const dx = to.x - from.x;
842
+ const dy = to.y - from.y;
843
+ const cx1 = from.x + dx * 0.4;
844
+ const cy1 = from.y;
845
+ const cx2 = to.x - dx * 0.4;
846
+ const cy2 = to.y;
847
+
848
+ const depPath = svgEl('path', {
849
+ d: `M ${from.x + NODE_R + 2} ${from.y} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${to.x - NODE_R - 2} ${to.y}`,
850
+ stroke: streamMap[m.stream].color,
851
+ 'stroke-width': 1.5,
852
+ 'stroke-dasharray': '6 3',
853
+ fill: 'none',
854
+ opacity: 0.45,
855
+ 'marker-end': `url(#arrow-${m.stream})`
856
+ });
857
+ svg.appendChild(depPath);
858
+ }
859
+ }
860
+
861
+ // Draw milestone nodes
862
+ for (const m of filteredMs) {
863
+ const pos = msPositions[m.id];
864
+ if (!pos) continue;
865
+ const stream = streamMap[m.stream];
866
+ const visible = activeStreams.has(m.stream);
867
+
868
+ const g = svgEl('g', {
869
+ opacity: visible ? 1 : 0.15,
870
+ cursor: 'pointer',
871
+ 'data-id': m.id
872
+ });
873
+
874
+ // Node circle
875
+ if (m.status === 'done') {
876
+ g.appendChild(svgEl('circle', {
877
+ cx: pos.x, cy: pos.y, r: NODE_R,
878
+ fill: stream.color, stroke: stream.color, 'stroke-width': 2
879
+ }));
880
+ } else if (m.status === 'in_progress') {
881
+ const clipId = 'clip-' + m.id;
882
+ const clip = svgEl('clipPath', { id: clipId });
883
+ clip.appendChild(svgEl('rect', { x: pos.x - NODE_R, y: pos.y, width: NODE_R * 2, height: NODE_R }));
884
+ defs.appendChild(clip);
885
+
886
+ g.appendChild(svgEl('circle', {
887
+ cx: pos.x, cy: pos.y, r: NODE_R,
888
+ fill: 'none', stroke: stream.color, 'stroke-width': 2
889
+ }));
890
+ g.appendChild(svgEl('circle', {
891
+ cx: pos.x, cy: pos.y, r: NODE_R - 1,
892
+ fill: stream.color, 'clip-path': `url(#${clipId})`
893
+ }));
894
+ } else if (m.status === 'research') {
895
+ g.appendChild(svgEl('circle', {
896
+ cx: pos.x, cy: pos.y, r: NODE_R,
897
+ fill: 'none', stroke: stream.color, 'stroke-width': 2,
898
+ 'stroke-dasharray': '3 2'
899
+ }));
900
+ } else {
901
+ g.appendChild(svgEl('circle', {
902
+ cx: pos.x, cy: pos.y, r: NODE_R,
903
+ fill: 'none', stroke: stream.color, 'stroke-width': 2
904
+ }));
905
+ }
906
+
907
+ // Label below node
908
+ const label = svgEl('text', {
909
+ x: pos.x, y: pos.y + NODE_R + 14,
910
+ 'text-anchor': 'middle',
911
+ fill: visible ? '#8b8b94' : '#555',
912
+ 'font-size': 11,
913
+ 'font-family': '-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif',
914
+ });
915
+ const titleText = m.title.length > 28 ? m.title.slice(0, 26) + '...' : m.title;
916
+ label.textContent = titleText;
917
+ g.appendChild(label);
918
+
919
+ // Hover events
920
+ g.addEventListener('mouseenter', (e) => showTooltip(e, m, stream));
921
+ g.addEventListener('mousemove', (e) => moveTooltip(e));
922
+ g.addEventListener('mouseleave', hideTooltip);
923
+
924
+ svg.appendChild(g);
925
+ }
926
+ }
927
+
928
+ // --- SVG helper ---
929
+ function svgEl(tag, attrs) {
930
+ const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
931
+ for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
932
+ return el;
933
+ }
934
+
935
+ // --- Tooltip ---
936
+ const statusColors = {
937
+ done: { bg: 'rgba(34,197,94,0.15)', color: '#22c55e' },
938
+ in_progress: { bg: 'rgba(234,179,8,0.15)', color: '#eab308' },
939
+ planned: { bg: 'rgba(139,139,148,0.15)', color: '#8b8b94' },
940
+ research: { bg: 'rgba(168,139,250,0.15)', color: '#a78bfa' },
941
+ blocked: { bg: 'rgba(239,68,68,0.15)', color: '#ef4444' },
942
+ };
943
+
944
+ function showTooltip(e, m, stream) {
945
+ const tt = document.getElementById('tooltip');
946
+ const sc = statusColors[m.status] || statusColors.planned;
947
+ document.getElementById('tooltipTitle').textContent = m.title;
948
+ let meta = `<span class="tooltip-status" style="background:${sc.bg};color:${sc.color}">${m.status.replace('_', ' ')}</span>`;
949
+ meta += `<br>Stream: ${stream.name}`;
950
+ if (m.target) meta += `<br>Target: ${m.target}`;
951
+ if (m.depends_on && m.depends_on.length) meta += `<br>Depends on: ${m.depends_on.join(', ')}`;
952
+ document.getElementById('tooltipMeta').innerHTML = meta;
953
+ tt.classList.add('visible');
954
+ moveTooltip(e);
955
+ }
956
+
957
+ function moveTooltip(e) {
958
+ const tt = document.getElementById('tooltip');
959
+ tt.style.left = (e.clientX + 12) + 'px';
960
+ tt.style.top = (e.clientY - 10) + 'px';
961
+ }
962
+
963
+ function hideTooltip() {
964
+ document.getElementById('tooltip').classList.remove('visible');
965
+ }
966
+
967
+ // --- Resize handler ---
968
+ window.addEventListener('resize', () => { if (data) render(); });
969
+
970
+ // --- Init ---
971
+ load();
972
+ })();
973
+ </script>
974
+ </body>
975
+ </html>