ghostrail 0.15.2 → 0.17.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.
Files changed (167) hide show
  1. package/README.md +12 -9
  2. package/dist/agent/claude-code.d.ts.map +1 -1
  3. package/dist/agent/claude-code.js +4 -1
  4. package/dist/agent/claude-code.js.map +1 -1
  5. package/dist/board/page.d.ts +1 -1
  6. package/dist/board/page.d.ts.map +1 -1
  7. package/dist/board/page.js +2 -1
  8. package/dist/board/page.js.map +1 -1
  9. package/dist/board/server.d.ts +1 -1
  10. package/dist/board/server.d.ts.map +1 -1
  11. package/dist/board/server.js +7 -1
  12. package/dist/board/server.js.map +1 -1
  13. package/dist/board/stats.d.ts +1 -1
  14. package/dist/board/stats.d.ts.map +1 -1
  15. package/dist/board/stats.js +2 -1
  16. package/dist/board/stats.js.map +1 -1
  17. package/dist/claude-profile/index.d.ts +1 -1
  18. package/dist/claude-profile/index.d.ts.map +1 -1
  19. package/dist/claude-profile/index.js +1 -1
  20. package/dist/claude-profile/index.js.map +1 -1
  21. package/dist/claude-profile/setup.d.ts +7 -0
  22. package/dist/claude-profile/setup.d.ts.map +1 -1
  23. package/dist/claude-profile/setup.js +7 -0
  24. package/dist/claude-profile/setup.js.map +1 -1
  25. package/dist/cli/commands.d.ts +6 -0
  26. package/dist/cli/commands.d.ts.map +1 -1
  27. package/dist/cli/commands.js +204 -18
  28. package/dist/cli/commands.js.map +1 -1
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +3 -1
  31. package/dist/cli.js.map +1 -1
  32. package/dist/commands/help.d.ts.map +1 -1
  33. package/dist/commands/help.js +15 -3
  34. package/dist/commands/help.js.map +1 -1
  35. package/dist/config/load.d.ts.map +1 -1
  36. package/dist/config/load.js +18 -2
  37. package/dist/config/load.js.map +1 -1
  38. package/dist/config/schema.d.ts +8 -2
  39. package/dist/config/schema.d.ts.map +1 -1
  40. package/dist/config/schema.js.map +1 -1
  41. package/dist/dashboard/page.d.ts +7 -1
  42. package/dist/dashboard/page.d.ts.map +1 -1
  43. package/dist/dashboard/page.js +204 -39
  44. package/dist/dashboard/page.js.map +1 -1
  45. package/dist/dashboard/routes.d.ts +32 -0
  46. package/dist/dashboard/routes.d.ts.map +1 -1
  47. package/dist/dashboard/routes.js +45 -0
  48. package/dist/dashboard/routes.js.map +1 -1
  49. package/dist/dashboard/server.d.ts +35 -9
  50. package/dist/dashboard/server.d.ts.map +1 -1
  51. package/dist/dashboard/server.js +75 -11
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/doctor/checks.d.ts +87 -0
  54. package/dist/doctor/checks.d.ts.map +1 -0
  55. package/dist/doctor/checks.js +414 -0
  56. package/dist/doctor/checks.js.map +1 -0
  57. package/dist/doctor/index.d.ts +6 -0
  58. package/dist/doctor/index.d.ts.map +1 -0
  59. package/dist/doctor/index.js +5 -0
  60. package/dist/doctor/index.js.map +1 -0
  61. package/dist/doctor/probes.d.ts +42 -0
  62. package/dist/doctor/probes.d.ts.map +1 -0
  63. package/dist/doctor/probes.js +136 -0
  64. package/dist/doctor/probes.js.map +1 -0
  65. package/dist/doctor/report.d.ts +13 -0
  66. package/dist/doctor/report.d.ts.map +1 -0
  67. package/dist/doctor/report.js +32 -0
  68. package/dist/doctor/report.js.map +1 -0
  69. package/dist/doctor/run.d.ts +48 -0
  70. package/dist/doctor/run.d.ts.map +1 -0
  71. package/dist/doctor/run.js +109 -0
  72. package/dist/doctor/run.js.map +1 -0
  73. package/dist/doctor/types.d.ts +17 -0
  74. package/dist/doctor/types.d.ts.map +1 -0
  75. package/dist/doctor/types.js +6 -0
  76. package/dist/doctor/types.js.map +1 -0
  77. package/dist/factory/build.d.ts.map +1 -1
  78. package/dist/factory/build.js +1 -0
  79. package/dist/factory/build.js.map +1 -1
  80. package/dist/init/regions.d.ts +76 -0
  81. package/dist/init/regions.d.ts.map +1 -0
  82. package/dist/init/regions.js +247 -0
  83. package/dist/init/regions.js.map +1 -0
  84. package/dist/init/template.d.ts +7 -1
  85. package/dist/init/template.d.ts.map +1 -1
  86. package/dist/init/template.js +37 -2
  87. package/dist/init/template.js.map +1 -1
  88. package/dist/loop/budget.d.ts +11 -2
  89. package/dist/loop/budget.d.ts.map +1 -1
  90. package/dist/loop/budget.js +13 -3
  91. package/dist/loop/budget.js.map +1 -1
  92. package/dist/loop/comments.d.ts +20 -0
  93. package/dist/loop/comments.d.ts.map +1 -0
  94. package/dist/loop/comments.js +35 -0
  95. package/dist/loop/comments.js.map +1 -0
  96. package/dist/loop/engine.d.ts.map +1 -1
  97. package/dist/loop/engine.js +18 -0
  98. package/dist/loop/engine.js.map +1 -1
  99. package/dist/loop/index.d.ts +1 -0
  100. package/dist/loop/index.d.ts.map +1 -1
  101. package/dist/loop/index.js +1 -0
  102. package/dist/loop/index.js.map +1 -1
  103. package/dist/publish/gh.d.ts +8 -0
  104. package/dist/publish/gh.d.ts.map +1 -1
  105. package/dist/publish/gh.js +12 -0
  106. package/dist/publish/gh.js.map +1 -1
  107. package/dist/publish/githost.d.ts +8 -0
  108. package/dist/publish/githost.d.ts.map +1 -1
  109. package/dist/publish/githost.js.map +1 -1
  110. package/dist/publish/index.d.ts +1 -1
  111. package/dist/publish/index.d.ts.map +1 -1
  112. package/dist/publish/index.js +1 -1
  113. package/dist/publish/index.js.map +1 -1
  114. package/dist/respond/index.d.ts +3 -2
  115. package/dist/respond/index.d.ts.map +1 -1
  116. package/dist/respond/index.js +3 -2
  117. package/dist/respond/index.js.map +1 -1
  118. package/dist/respond/merge.d.ts +13 -0
  119. package/dist/respond/merge.d.ts.map +1 -0
  120. package/dist/respond/merge.js +181 -0
  121. package/dist/respond/merge.js.map +1 -0
  122. package/dist/respond/respond.d.ts +25 -5
  123. package/dist/respond/respond.d.ts.map +1 -1
  124. package/dist/respond/respond.js +57 -28
  125. package/dist/respond/respond.js.map +1 -1
  126. package/dist/respond/select.d.ts +15 -0
  127. package/dist/respond/select.d.ts.map +1 -1
  128. package/dist/respond/select.js +17 -0
  129. package/dist/respond/select.js.map +1 -1
  130. package/dist/source/dependencies.d.ts +81 -0
  131. package/dist/source/dependencies.d.ts.map +1 -0
  132. package/dist/source/dependencies.js +218 -0
  133. package/dist/source/dependencies.js.map +1 -0
  134. package/dist/source/index.d.ts +1 -0
  135. package/dist/source/index.d.ts.map +1 -1
  136. package/dist/source/index.js +1 -0
  137. package/dist/source/index.js.map +1 -1
  138. package/dist/source/linear.d.ts +17 -1
  139. package/dist/source/linear.d.ts.map +1 -1
  140. package/dist/source/linear.js +75 -9
  141. package/dist/source/linear.js.map +1 -1
  142. package/dist/store/events.d.ts +8 -0
  143. package/dist/store/events.d.ts.map +1 -1
  144. package/dist/store/events.js +10 -0
  145. package/dist/store/events.js.map +1 -1
  146. package/dist/store/file-store.d.ts +1 -1
  147. package/dist/store/file-store.d.ts.map +1 -1
  148. package/dist/store/file-store.js +7 -0
  149. package/dist/store/file-store.js.map +1 -1
  150. package/dist/store/index.d.ts +1 -1
  151. package/dist/store/index.d.ts.map +1 -1
  152. package/dist/store/index.js +1 -1
  153. package/dist/store/index.js.map +1 -1
  154. package/dist/store/port.d.ts +15 -0
  155. package/dist/store/port.d.ts.map +1 -1
  156. package/dist/store/port.js +17 -1
  157. package/dist/store/port.js.map +1 -1
  158. package/dist/triage/engine.d.ts.map +1 -1
  159. package/dist/triage/engine.js +2 -0
  160. package/dist/triage/engine.js.map +1 -1
  161. package/package.json +3 -1
  162. package/skill/ghostrail/SKILL.md +94 -43
  163. package/templates/code/prompts/resolve-issue.md +20 -4
  164. package/templates/code/prompts/respond.md +4 -0
  165. package/templates/code/prompts/triage.md +13 -0
  166. package/templates/content/prompts/draft.md +13 -0
  167. package/templates/content/prompts/respond.md +4 -0
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,CAAU,CAAC;AAG1E,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,gBAAgB;IAChB,WAAW;IACX,aAAa;IACb,KAAK;IACL,QAAQ;CACA,CAAC;AAiHX,mEAAmE;AACnE,MAAM,CAAC,MAAM,qBAAqB,GAAG,WAAW,CAAC;AACjD,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAkB3C,kFAAkF;AAClF,MAAM,CAAC,MAAM,qBAAqB,GAAsB;IACtD,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;CACP,CAAC;AAEF,mEAAmE;AACnE,MAAM,CAAC,MAAM,kBAAkB,GAAe;IAC5C,cAAc,EAAE,CAAC;IACjB,gBAAgB,EAAE,CAAC;IACnB,SAAS,EAAE,EAAE,GAAG,MAAM;IACtB,gBAAgB,EAAE,CAAC;IACnB,UAAU,EAAE,CAAC,GAAG,SAAS;IACzB,WAAW,EAAE,CAAC;CACf,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAC1C,MAAM,CAAC,MAAM,oBAAoB,GAAgB,gBAAgB,CAAC;AAElE;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,CAAU,CAAC;AAG1E,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,gBAAgB;IAChB,WAAW;IACX,aAAa;IACb,KAAK;IACL,QAAQ;CACA,CAAC;AAwHX,mEAAmE;AACnE,MAAM,CAAC,MAAM,qBAAqB,GAAG,WAAW,CAAC;AACjD,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAkB3C,kFAAkF;AAClF,MAAM,CAAC,MAAM,qBAAqB,GAAsB;IACtD,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;CACP,CAAC;AAEF,mEAAmE;AACnE,MAAM,CAAC,MAAM,kBAAkB,GAAe;IAC5C,cAAc,EAAE,CAAC;IACjB,gBAAgB,EAAE,CAAC;IACnB,SAAS,EAAE,EAAE,GAAG,MAAM;IACtB,gBAAgB,EAAE,CAAC;IACnB,UAAU,EAAE,CAAC,GAAG,SAAS;IACzB,WAAW,EAAE,CAAC;CACf,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAC1C,MAAM,CAAC,MAAM,oBAAoB,GAAgB,gBAAgB,CAAC;AAElE;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC"}
@@ -5,6 +5,12 @@
5
5
  * The checkbox is desired state and the copy has to keep saying so: a ticked
6
6
  * box beside an inactive unit means "will start", not "running". Showing them
7
7
  * as one thing would be a lie the operator acts on.
8
+ *
9
+ * Run detail matches the board's (TJ-1640), with two differences the dashboard
10
+ * forces. It renders detail straight from the listing rather than fetching a
11
+ * row per expanded run, because `/api/ghostrails` already carries whole rows.
12
+ * And every request that names a run or an artifact carries the ghostrail's
13
+ * `path`, since nothing here is scoped to a single repo.
8
14
  */
9
- export declare const DASHBOARD_PAGE_HTML = "<!doctype html>\n<meta charset=\"utf-8\">\n<title>ghostrails</title>\n<style>\n :root { color-scheme: light dark; }\n body { font: 15px/1.5 ui-sans-serif, system-ui, sans-serif; margin: 2rem auto; max-width: 60rem; padding: 0 1rem; }\n h1 { font-size: 1.3rem; margin: 0 0 .25rem; }\n .sub { opacity: .7; margin: 0 0 1.5rem; }\n .gr { border: 1px solid color-mix(in oklab, currentColor 20%, transparent); border-radius: 8px; padding: .9rem 1rem; margin-bottom: .75rem; }\n .top { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }\n .name { font-weight: 600; }\n .path { opacity: .6; font-family: ui-monospace, monospace; font-size: .85em; }\n .pill { font-size: .75em; padding: .1rem .45rem; border-radius: 99px; border: 1px solid currentColor; opacity: .8; }\n .runs { margin: .6rem 0 0; padding-left: 1.1rem; opacity: .8; font-size: .9em; }\n .runs li { margin: .1rem 0; }\n .none { opacity: .55; font-size: .9em; margin: .6rem 0 0; }\n label { display: inline-flex; align-items: center; gap: .4rem; cursor: pointer; }\n .err { color: #b00; }\n</style>\n<h1>ghostrails</h1>\n<p class=\"sub\">The checkbox is what <code>ghostrail start</code> should bring up. The pill beside it is what is running right now.</p>\n<div id=\"list\">loading\u2026</div>\n<script>\nconst tok = new URLSearchParams(location.search).get(\"token\");\nconst api = (p) => p + (tok ? (p.includes(\"?\") ? \"&\" : \"?\") + \"token=\" + encodeURIComponent(tok) : \"\");\n\nfunction render(ghostrails) {\n const list = document.getElementById(\"list\");\n if (!ghostrails.length) {\n list.innerHTML = '<p class=\"none\">No ghostrails registered. <code>ghostrail init &lt;template&gt;</code> registers one.</p>';\n return;\n }\n list.innerHTML = \"\";\n for (const g of ghostrails) {\n const el = document.createElement(\"div\");\n el.className = \"gr\";\n const runs = g.runs.length\n ? '<ul class=\"runs\">' + g.runs.map((r) =>\n \"<li>\" + esc(r.itemId || r.runId) + \" \u2014 \" + esc(r.state || \"?\") +\n (r.title ? \" \u2014 \" + esc(r.title) : \"\") + \"</li>\").join(\"\") + \"</ul>\"\n : '<p class=\"none\">' + (g.health === \"ok\" ? \"no runs yet\" : esc(g.health)) + \"</p>\";\n el.innerHTML =\n '<div class=\"top\">' +\n '<label><input type=\"checkbox\" ' + (g.watch ? \"checked\" : \"\") + \" data-path=\\\"\" + esc(g.path) + '\"> watch</label>' +\n '<span class=\"name\">' + esc(g.name) + \"</span>\" +\n '<span class=\"pill\">' + esc(g.state) + \"</span>\" +\n '<span class=\"path\">' + esc(g.path) + \"</span>\" +\n \"</div>\" + runs;\n list.appendChild(el);\n }\n for (const box of list.querySelectorAll(\"input[type=checkbox]\")) {\n box.addEventListener(\"change\", async (e) => {\n const t = e.target;\n try {\n const res = await fetch(api(\"/api/watch\"), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ path: t.dataset.path, watch: t.checked }),\n });\n if (!res.ok) throw new Error((await res.json()).error || res.statusText);\n render((await res.json()).ghostrails);\n } catch (err) {\n t.checked = !t.checked;\n alert(\"could not update: \" + err.message);\n }\n });\n }\n}\n\nconst esc = (s) => String(s).replace(/[&<>\"']/g, (c) =>\n ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" })[c]);\n\nasync function load() {\n try {\n const res = await fetch(api(\"/api/ghostrails\"));\n if (!res.ok) throw new Error((await res.json()).error || res.statusText);\n render((await res.json()).ghostrails);\n } catch (err) {\n document.getElementById(\"list\").innerHTML = '<p class=\"err\">' + esc(err.message) + \"</p>\";\n }\n}\nload();\nsetInterval(load, 10000);\n</script>\n";
15
+ export declare const DASHBOARD_PAGE_HTML: string;
10
16
  //# sourceMappingURL=page.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,yzHAoF/B,CAAC"}
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,QAkP/B,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { IN_PROGRESS_STATE } from "../store/events.js";
1
2
  /**
2
3
  * The dashboard page. Inline, like the board's, so the package ships one file
3
4
  * and the server needs no static asset handling.
@@ -5,13 +6,19 @@
5
6
  * The checkbox is desired state and the copy has to keep saying so: a ticked
6
7
  * box beside an inactive unit means "will start", not "running". Showing them
7
8
  * as one thing would be a lie the operator acts on.
9
+ *
10
+ * Run detail matches the board's (TJ-1640), with two differences the dashboard
11
+ * forces. It renders detail straight from the listing rather than fetching a
12
+ * row per expanded run, because `/api/ghostrails` already carries whole rows.
13
+ * And every request that names a run or an artifact carries the ghostrail's
14
+ * `path`, since nothing here is scoped to a single repo.
8
15
  */
9
16
  export const DASHBOARD_PAGE_HTML = `<!doctype html>
10
17
  <meta charset="utf-8">
11
18
  <title>ghostrails</title>
12
19
  <style>
13
20
  :root { color-scheme: light dark; }
14
- body { font: 15px/1.5 ui-sans-serif, system-ui, sans-serif; margin: 2rem auto; max-width: 60rem; padding: 0 1rem; }
21
+ body { font: 15px/1.5 ui-sans-serif, system-ui, sans-serif; margin: 2rem auto; max-width: 72rem; padding: 0 1rem; }
15
22
  h1 { font-size: 1.3rem; margin: 0 0 .25rem; }
16
23
  .sub { opacity: .7; margin: 0 0 1.5rem; }
17
24
  .gr { border: 1px solid color-mix(in oklab, currentColor 20%, transparent); border-radius: 8px; padding: .9rem 1rem; margin-bottom: .75rem; }
@@ -19,11 +26,27 @@ export const DASHBOARD_PAGE_HTML = `<!doctype html>
19
26
  .name { font-weight: 600; }
20
27
  .path { opacity: .6; font-family: ui-monospace, monospace; font-size: .85em; }
21
28
  .pill { font-size: .75em; padding: .1rem .45rem; border-radius: 99px; border: 1px solid currentColor; opacity: .8; }
22
- .runs { margin: .6rem 0 0; padding-left: 1.1rem; opacity: .8; font-size: .9em; }
23
- .runs li { margin: .1rem 0; }
24
29
  .none { opacity: .55; font-size: .9em; margin: .6rem 0 0; }
25
30
  label { display: inline-flex; align-items: center; gap: .4rem; cursor: pointer; }
26
31
  .err { color: #b00; }
32
+ table.runs { border-collapse: collapse; width: 100%; margin: .6rem 0 0; font-size: .9em; }
33
+ table.runs th, table.runs td { text-align: left; padding: .3rem .5rem; border-bottom: 1px solid color-mix(in oklab, currentColor 15%, transparent); }
34
+ table.runs th { font-weight: 600; opacity: .7; font-size: .85em; }
35
+ a { color: inherit; }
36
+ .state { font-weight: 600; }
37
+ .done { color: #2a9d3a; } .failed { color: #d33; } .blocked { color: #d90; }
38
+ .noop, .skipped { opacity: .6; } .muted { opacity: .6; }
39
+ .twirl {
40
+ background: none; border: none; cursor: pointer; font: inherit; padding: 0 .3rem;
41
+ color: inherit; display: inline-block; transition: transform .15s ease;
42
+ }
43
+ .twirl.open { transform: rotate(90deg); }
44
+ .detail-cell { padding: .75rem 1.5rem; background: color-mix(in oklab, currentColor 6%, transparent); }
45
+ .detail-cell > div { margin-bottom: .5rem; }
46
+ .detail-cell pre { white-space: pre-wrap; word-break: break-word; margin: .25rem 0 0; font: inherit; }
47
+ .detail-cell ul { margin: .25rem 0 0; padding-left: 1.2rem; }
48
+ .subtable { border-collapse: collapse; margin-top: .25rem; }
49
+ .subtable td { padding: .15rem .5rem .15rem 0; border-bottom: none; }
27
50
  </style>
28
51
  <h1>ghostrails</h1>
29
52
  <p class="sub">The checkbox is what <code>ghostrail start</code> should bring up. The pill beside it is what is running right now.</p>
@@ -31,58 +54,200 @@ export const DASHBOARD_PAGE_HTML = `<!doctype html>
31
54
  <script>
32
55
  const tok = new URLSearchParams(location.search).get("token");
33
56
  const api = (p) => p + (tok ? (p.includes("?") ? "&" : "?") + "token=" + encodeURIComponent(tok) : "");
57
+ const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) =>
58
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
59
+ const enc = encodeURIComponent;
60
+
61
+ // Expanded rows and fetched PR status outlive a re-render, so the 10s poll does
62
+ // not collapse what the operator opened. Keyed on ghostrail path plus run id,
63
+ // which is stable across renders; DOM ids are positional and are not.
64
+ const expanded = new Set();
65
+ const prStatus = new Map();
66
+ const rowKey = (path, runId) => JSON.stringify([path, runId]);
67
+ let latest = [];
68
+
69
+ function fmtWhen(ts) {
70
+ return ts ? new Date(ts).toLocaleString() : "";
71
+ }
72
+
73
+ function gateStepsHtml(gate) {
74
+ if (!gate || gate.length === 0) return "<div class=muted>no gate steps</div>";
75
+ const rows = gate.map((s) =>
76
+ "<tr><td>" + esc(s.name) + "</td><td class='" + (s.exitCode === 0 ? "done" : "failed") + "'>" +
77
+ (s.exitCode === 0 ? "passed" : "failed (exit " + esc(s.exitCode) + ")") + "</td><td class=muted>" +
78
+ esc(s.durationMs) + "ms</td></tr>"
79
+ ).join("");
80
+ return "<table class=subtable>" + rows + "</table>";
81
+ }
82
+
83
+ function artifactsHtml(path, artifacts) {
84
+ if (!artifacts || artifacts.length === 0) return "";
85
+ const items = artifacts.map((a) => {
86
+ const href = api("/api/artifact?path=" + enc(path) + "&hash=" + enc(a.hash));
87
+ return "<li><a href='" + esc(href) + "'>" + esc(a.name) + "</a></li>";
88
+ }).join("");
89
+ return "<div><b>Artifacts</b><ul>" + items + "</ul></div>";
90
+ }
91
+
92
+ function checkLineHtml(c) {
93
+ const label = c.conclusion || c.status;
94
+ const cls = c.conclusion === "success" ? "done" : c.conclusion ? "failed" : "muted";
95
+ const name = c.url ? "<a href='" + esc(c.url) + "'>" + esc(c.name) + "</a>" : esc(c.name);
96
+ return "<li>" + name + ": <span class='" + cls + "'>" + esc(label) + "</span></li>";
97
+ }
98
+
99
+ function prStatusHtml(status) {
100
+ if (!status) return "<div class=muted>loading CI status…</div>";
101
+ if (!status.available) {
102
+ return "<div class=muted>PR status unknown" + (status.error ? ": " + esc(status.error) : "") + "</div>";
103
+ }
104
+ const merged = status.state === "merged";
105
+ const parts = [
106
+ "<div><b>PR state:</b> " + esc(status.state) +
107
+ (merged && status.mergedAt ? " at " + esc(new Date(status.mergedAt).toLocaleString()) : "") + "</div>",
108
+ ];
109
+ if (status.checks && status.checks.length > 0) {
110
+ parts.push("<div><b>CI checks</b><ul>" + status.checks.map(checkLineHtml).join("") + "</ul></div>");
111
+ } else {
112
+ parts.push("<div class=muted>no CI checks reported</div>");
113
+ }
114
+ return parts.join("");
115
+ }
116
+
117
+ function detailHtml(path, r) {
118
+ const parts = [];
119
+ parts.push("<div><b>Item</b> " + esc(r.itemId || "") + (r.kind ? " (" + esc(r.kind) + ")" : "") + "</div>");
120
+ if (r.branch) parts.push("<div><b>Branch</b> " + esc(r.branch) + "</div>");
121
+ if (r.backend) parts.push("<div><b>Backend</b> " + esc(r.backend) + "</div>");
122
+ if (r.costUsd !== undefined) {
123
+ parts.push("<div><b>Cost</b> $" + esc(Number(r.costUsd).toFixed(4)) + "</div>");
124
+ }
125
+ if (r.tokensIn !== undefined || r.tokensOut !== undefined) {
126
+ parts.push("<div><b>Tokens</b> in " + esc(r.tokensIn ?? 0) + " / out " + esc(r.tokensOut ?? 0) + "</div>");
127
+ }
128
+ parts.push("<div><b>Gate</b>" + gateStepsHtml(r.gate) + "</div>");
129
+ if (r.error) parts.push("<div><b>Error</b><pre class=failed>" + esc(r.error) + "</pre></div>");
130
+ if (r.summary) parts.push("<div><b>Comment</b><pre>" + esc(r.summary) + "</pre></div>");
131
+ if (r.prUrl) {
132
+ parts.push("<div><b>PR</b> <a href='" + esc(r.prUrl) + "'>" + esc(r.prUrl) + "</a></div>");
133
+ }
134
+ parts.push(artifactsHtml(path, r.artifacts));
135
+ if (r.prNumber !== undefined) parts.push(prStatusHtml(prStatus.get(rowKey(path, r.runId))));
136
+ parts.push(
137
+ "<div class=muted>started " + esc(fmtWhen(r.startedAt)) +
138
+ ", finished " + esc(fmtWhen(r.finishedAt)) + "</div>",
139
+ );
140
+ return parts.join("");
141
+ }
142
+
143
+ function runsHtml(g, gi) {
144
+ if (!g.runs.length) {
145
+ return "<p class='none'>" + (g.health === "ok" ? "no runs yet" : esc(g.health)) + "</p>";
146
+ }
147
+ const body = g.runs.map((r, ri) => {
148
+ const open = expanded.has(rowKey(g.path, r.runId));
149
+ const gate = (r.gate || []).map((s) => (s.exitCode === 0 ? "✓" : "✗")).join("");
150
+ const pr = r.prUrl ? "<a href='" + esc(r.prUrl) + "'>#" + esc(r.prNumber) + "</a>" : "";
151
+ const state = esc(r.state || ${JSON.stringify(IN_PROGRESS_STATE)});
152
+ const main = "<tr>" +
153
+ "<td><button class='twirl" + (open ? " open" : "") + "' id='t-" + gi + "-" + ri + "'" +
154
+ " aria-expanded='" + open + "' data-path='" + esc(g.path) + "' data-run='" + esc(r.runId) + "'" +
155
+ " title='show detail'>▶</button></td>" +
156
+ "<td class=muted>" + esc(r.runId.slice(0, 8)) + "</td>" +
157
+ "<td>" + esc(r.title || r.itemId || "") + "</td>" +
158
+ "<td class='state " + state + "'>" + state + "</td>" +
159
+ "<td>" + esc(r.backend || "") + "</td>" +
160
+ "<td>" + gate + "</td>" +
161
+ "<td>" + pr + "</td>" +
162
+ "<td class=muted>" + esc(fmtWhen(r.startedAt)) + "</td></tr>";
163
+ if (!open) return main;
164
+ return main + "<tr><td colspan=8 class=detail-cell>" + detailHtml(g.path, r) + "</td></tr>";
165
+ }).join("");
166
+ return "<table class=runs><thead><tr><th></th><th>Run</th><th>Item</th><th>State</th>" +
167
+ "<th>Backend</th><th>Gate</th><th>PR</th><th>When</th></tr></thead><tbody>" +
168
+ body + "</tbody></table>";
169
+ }
34
170
 
35
171
  function render(ghostrails) {
172
+ latest = ghostrails;
36
173
  const list = document.getElementById("list");
37
174
  if (!ghostrails.length) {
38
175
  list.innerHTML = '<p class="none">No ghostrails registered. <code>ghostrail init &lt;template&gt;</code> registers one.</p>';
39
176
  return;
40
177
  }
41
- list.innerHTML = "";
42
- for (const g of ghostrails) {
43
- const el = document.createElement("div");
44
- el.className = "gr";
45
- const runs = g.runs.length
46
- ? '<ul class="runs">' + g.runs.map((r) =>
47
- "<li>" + esc(r.itemId || r.runId) + "" + esc(r.state || "?") +
48
- (r.title ? " — " + esc(r.title) : "") + "</li>").join("") + "</ul>"
49
- : '<p class="none">' + (g.health === "ok" ? "no runs yet" : esc(g.health)) + "</p>";
50
- el.innerHTML =
51
- '<div class="top">' +
52
- '<label><input type="checkbox" ' + (g.watch ? "checked" : "") + " data-path=\\"" + esc(g.path) + '"> watch</label>' +
53
- '<span class="name">' + esc(g.name) + "</span>" +
54
- '<span class="pill">' + esc(g.state) + "</span>" +
55
- '<span class="path">' + esc(g.path) + "</span>" +
56
- "</div>" + runs;
57
- list.appendChild(el);
58
- }
59
- for (const box of list.querySelectorAll("input[type=checkbox]")) {
60
- box.addEventListener("change", async (e) => {
61
- const t = e.target;
62
- try {
63
- const res = await fetch(api("/api/watch"), {
64
- method: "POST",
65
- headers: { "content-type": "application/json" },
66
- body: JSON.stringify({ path: t.dataset.path, watch: t.checked }),
67
- });
68
- if (!res.ok) throw new Error((await res.json()).error || res.statusText);
69
- render((await res.json()).ghostrails);
70
- } catch (err) {
71
- t.checked = !t.checked;
72
- alert("could not update: " + err.message);
73
- }
74
- });
178
+ list.innerHTML = ghostrails.map((g, gi) =>
179
+ "<div class='gr'><div class='top'>" +
180
+ "<label><input type='checkbox' " + (g.watch ? "checked" : "") + " data-path='" + esc(g.path) + "'> watch</label>" +
181
+ "<span class='name'>" + esc(g.name) + "</span>" +
182
+ "<span class='pill'>" + esc(g.state) + "</span>" +
183
+ "<span class='path'>" + esc(g.path) + "</span>" +
184
+ "</div>" + runsHtml(g, gi) + "</div>"
185
+ ).join("");
186
+ }
187
+
188
+ /** Fetch PR status for every expanded run that has a PR, then repaint. */
189
+ async function refreshPrStatus() {
190
+ const wanted = [];
191
+ for (const g of latest) {
192
+ for (const r of g.runs) {
193
+ const k = rowKey(g.path, r.runId);
194
+ if (r.prNumber !== undefined && expanded.has(k)) wanted.push([k, g.path, r.runId]);
195
+ }
75
196
  }
197
+ if (!wanted.length) return;
198
+ await Promise.all(wanted.map(async ([k, path, runId]) => {
199
+ try {
200
+ const res = await fetch(api("/api/pr-status?path=" + enc(path) + "&run=" + enc(runId)));
201
+ prStatus.set(k, await res.json());
202
+ } catch {
203
+ // Leave whatever was last known. A dropped poll is not new information.
204
+ }
205
+ }));
206
+ render(latest);
76
207
  }
77
208
 
78
- const esc = (s) => String(s).replace(/[&<>"']/g, (c) =>
79
- ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
209
+ function toggle(path, runId, buttonId) {
210
+ const k = rowKey(path, runId);
211
+ if (expanded.has(k)) expanded.delete(k);
212
+ else expanded.add(k);
213
+ render(latest);
214
+ // Re-render replaces the button that was clicked, so put focus back on its
215
+ // successor. Positional ids are fine for this: the list has not changed.
216
+ const btn = document.getElementById(buttonId);
217
+ if (btn) btn.focus();
218
+ if (expanded.has(k)) refreshPrStatus();
219
+ }
220
+
221
+ const list = document.getElementById("list");
222
+
223
+ list.addEventListener("click", (e) => {
224
+ const btn = e.target.closest(".twirl");
225
+ if (btn) toggle(btn.dataset.path, btn.dataset.run, btn.id);
226
+ });
227
+
228
+ list.addEventListener("change", async (e) => {
229
+ const t = e.target;
230
+ if (!t.matches("input[type=checkbox]")) return;
231
+ try {
232
+ const res = await fetch(api("/api/watch"), {
233
+ method: "POST",
234
+ headers: { "content-type": "application/json" },
235
+ body: JSON.stringify({ path: t.dataset.path, watch: t.checked }),
236
+ });
237
+ if (!res.ok) throw new Error((await res.json()).error || res.statusText);
238
+ render((await res.json()).ghostrails);
239
+ } catch (err) {
240
+ t.checked = !t.checked;
241
+ alert("could not update: " + err.message);
242
+ }
243
+ });
80
244
 
81
245
  async function load() {
82
246
  try {
83
247
  const res = await fetch(api("/api/ghostrails"));
84
248
  if (!res.ok) throw new Error((await res.json()).error || res.statusText);
85
249
  render((await res.json()).ghostrails);
250
+ await refreshPrStatus();
86
251
  } catch (err) {
87
252
  document.getElementById("list").innerHTML = '<p class="err">' + esc(err.message) + "</p>";
88
253
  }
@@ -1 +1 @@
1
- {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFlC,CAAC"}
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCAuIA,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2GnE,CAAC"}
@@ -10,11 +10,43 @@ export type DashboardRoute = {
10
10
  kind: "ghostrails";
11
11
  } | {
12
12
  kind: "watch";
13
+ } | {
14
+ kind: "prStatus";
15
+ } | {
16
+ kind: "artifact";
13
17
  } | {
14
18
  kind: "notfound";
15
19
  };
16
20
  /** Map a request pathname to a route. Total; never throws. */
17
21
  export declare function parseDashboardRoute(pathname: string): DashboardRoute;
22
+ /**
23
+ * The ghostrail a detail request is about, plus the run or artifact within it.
24
+ *
25
+ * The selector is `path`, matching the `watch` route and the deps map, and it
26
+ * travels as a query parameter because a ghostrail's identity does not survive
27
+ * a path segment: a repo identity holds slashes (`github.com/owner/name`) and
28
+ * so does the absolute path an entry falls back to. Percent-encoding one into a
29
+ * segment is exactly the shape that produced TJ-1641.
30
+ *
31
+ * Undefined for anything missing or empty, which the routes answer with 400.
32
+ * Note that `URLSearchParams.get` returns the FIRST value for a repeated key,
33
+ * so a duplicated `path` cannot smuggle a second one past a caller that checked
34
+ * the first.
35
+ */
36
+ export declare function parseDetailQuery(params: URLSearchParams, key: "run" | "hash"): {
37
+ path: string;
38
+ value: string;
39
+ } | undefined;
40
+ /**
41
+ * The artifact hash a request names, or undefined when it is not one.
42
+ *
43
+ * Separate from "was a hash supplied at all" because the two answers differ: a
44
+ * missing parameter is the caller's mistake and earns a 400, while a
45
+ * well-formed request for a hash that could never exist is simply a 404. The
46
+ * check itself is TJ-1641's, applied here so no non-digest ever reaches a store
47
+ * that would join it onto a directory.
48
+ */
49
+ export declare function parseArtifactHash(params: URLSearchParams): string | undefined;
18
50
  /**
19
51
  * True when a request looks cross-origin, which the write route refuses.
20
52
  *
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAErD;;;;GAIG;AAEH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAEzB,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAUpE;AA2BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAYxE;AAOD;;;;;;GAMG;AACH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAY3F"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGrD;;;;GAIG;AAEH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAEzB,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAcpE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,KAAK,GAAG,MAAM,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAM7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAI7E;AA2BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAYxE;AAOD;;;;;;GAMG;AACH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAY3F"}
@@ -1,3 +1,4 @@
1
+ import { isArtifactHash } from "../store/port.js";
1
2
  /** Map a request pathname to a route. Total; never throws. */
2
3
  export function parseDashboardRoute(pathname) {
3
4
  const parts = pathname.split("/").filter((p) => p.length > 0);
@@ -12,8 +13,52 @@ export function parseDashboardRoute(pathname) {
12
13
  return { kind: "ghostrails" };
13
14
  if (parts[1] === "watch")
14
15
  return { kind: "watch" };
16
+ // Which ghostrail and which run are query parameters, not path segments, so
17
+ // these stay two-segment routes like the others. See `parseDetailQuery`.
18
+ if (parts[1] === "pr-status")
19
+ return { kind: "prStatus" };
20
+ if (parts[1] === "artifact")
21
+ return { kind: "artifact" };
15
22
  return { kind: "notfound" };
16
23
  }
24
+ /**
25
+ * The ghostrail a detail request is about, plus the run or artifact within it.
26
+ *
27
+ * The selector is `path`, matching the `watch` route and the deps map, and it
28
+ * travels as a query parameter because a ghostrail's identity does not survive
29
+ * a path segment: a repo identity holds slashes (`github.com/owner/name`) and
30
+ * so does the absolute path an entry falls back to. Percent-encoding one into a
31
+ * segment is exactly the shape that produced TJ-1641.
32
+ *
33
+ * Undefined for anything missing or empty, which the routes answer with 400.
34
+ * Note that `URLSearchParams.get` returns the FIRST value for a repeated key,
35
+ * so a duplicated `path` cannot smuggle a second one past a caller that checked
36
+ * the first.
37
+ */
38
+ export function parseDetailQuery(params, key) {
39
+ const path = params.get("path");
40
+ const value = params.get(key);
41
+ if (path === null || path.length === 0)
42
+ return undefined;
43
+ if (value === null || value.length === 0)
44
+ return undefined;
45
+ return { path, value };
46
+ }
47
+ /**
48
+ * The artifact hash a request names, or undefined when it is not one.
49
+ *
50
+ * Separate from "was a hash supplied at all" because the two answers differ: a
51
+ * missing parameter is the caller's mistake and earns a 400, while a
52
+ * well-formed request for a hash that could never exist is simply a 404. The
53
+ * check itself is TJ-1641's, applied here so no non-digest ever reaches a store
54
+ * that would join it onto a directory.
55
+ */
56
+ export function parseArtifactHash(params) {
57
+ const hash = params.get("hash");
58
+ if (hash === null || !isArtifactHash(hash))
59
+ return undefined;
60
+ return hash;
61
+ }
17
62
  /**
18
63
  * Every value for a header, case-insensitively.
19
64
  *
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,8 @@
1
1
  import { type Server } from "node:http";
2
2
  import type { Ghostrail } from "../global/registry.js";
3
+ import type { PrStatus } from "../publish/githost.js";
4
+ import type { RunRow } from "../store/events.js";
5
+ import type { RunStore } from "../store/port.js";
3
6
  /** How a ghostrail stands on disk, mirroring what `status` reports. */
4
7
  export type GhostrailHealth = "ok" | "no config" | "missing";
5
8
  /** One row of the dashboard: registry entry plus everything observed about it. */
@@ -7,13 +10,15 @@ export interface GhostrailView extends Ghostrail {
7
10
  health: GhostrailHealth;
8
11
  /** Live unit state, e.g. `active`. `unknown` when no manager could be asked. */
9
12
  state: string;
10
- /** Most recent runs from this repo's state, newest first. */
11
- runs: {
12
- runId: string;
13
- itemId?: string;
14
- state?: string;
15
- title?: string;
16
- }[];
13
+ /**
14
+ * Most recent runs from this repo's state, newest first.
15
+ *
16
+ * Whole rows. The four fields this used to copy off each row were the only
17
+ * thing standing between the dashboard and a detail panel, and narrowing them
18
+ * saved nothing: `collectViews` has already read and sliced the rows by the
19
+ * time it gets here (TJ-1640).
20
+ */
21
+ runs: RunRow[];
17
22
  }
18
23
  /** Everything the server needs, injected so it can be driven without a machine. */
19
24
  export interface DashboardDeps {
@@ -31,6 +36,26 @@ export interface DashboardDeps {
31
36
  * was each working it out for itself.
32
37
  */
33
38
  stateDir(entry: Ghostrail): Promise<string>;
39
+ /**
40
+ * One ghostrail's run store. Defaults to a file store over `stateDir`, which
41
+ * is what every real caller wants; injectable so the detail routes can be
42
+ * driven without a filesystem.
43
+ *
44
+ * It goes through `stateDir` rather than working the path out for itself,
45
+ * because that is the mistake TJ-1639 fixed: two places deriving one
46
+ * directory is how the dashboard ended up reading an empty one.
47
+ */
48
+ store?(entry: Ghostrail): RunStore;
49
+ /**
50
+ * Live PR and CI status for a PR in one ghostrail's repo.
51
+ *
52
+ * It takes the entry and not just the number because the dashboard spans
53
+ * repos, and a PR number means nothing without one. Optional for the same
54
+ * reason the board's `gitHost` is: a dashboard on a machine with no `gh` and
55
+ * no credentials serves everything else unchanged and reports PR status as
56
+ * unavailable, rather than failing or refusing to start.
57
+ */
58
+ prStatus?(entry: Ghostrail, prNumber: number): Promise<PrStatus>;
34
59
  }
35
60
  export interface DashboardOptions {
36
61
  bind?: string;
@@ -47,8 +72,9 @@ export interface DashboardOptions {
47
72
  /** Assemble the view for every registered ghostrail. */
48
73
  export declare function collectViews(deps: DashboardDeps, entries: readonly Ghostrail[]): Promise<GhostrailView[]>;
49
74
  /**
50
- * The dashboard's HTTP server: one page, one listing, and one write route that
51
- * sets a ghostrail's `watch` flag and does nothing else.
75
+ * The dashboard's HTTP server: one page, one listing, two read routes for a
76
+ * single run's live PR status and its artifacts, and one write route that sets
77
+ * a ghostrail's `watch` flag and does nothing else.
52
78
  *
53
79
  * It never starts or stops anything. `start` and the service units act on the
54
80
  * flag, which keeps process control off an HTTP surface: the worst a
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAqC,MAAM,WAAW,CAAC;AAK3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAMvD,uEAAuE;AACvE,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D,kFAAkF;AAClF,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,MAAM,EAAE,eAAe,CAAC;IACxB,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5E;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC,CAAC;IACjD,aAAa,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC/C,0CAA0C;IAC1C,MAAM,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpE;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAyBD,wDAAwD;AACxD,wBAAsB,YAAY,CAChC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,SAAS,SAAS,EAAE,GAC5B,OAAO,CAAC,aAAa,EAAE,CAAC,CAwB1B;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAqEjG;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAIjE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAqC,MAAM,WAAW,CAAC;AAK3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAUjD,uEAAuE;AACvE,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D,kFAAkF;AAClF,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,MAAM,EAAE,eAAe,CAAC;IACxB,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC,CAAC;IACjD,aAAa,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC/C,0CAA0C;IAC1C,MAAM,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpE;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C;;;;;;;;OAQG;IACH,KAAK,CAAC,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;IACnC;;;;;;;;OAQG;IACH,QAAQ,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAqCD,wDAAwD;AACxD,wBAAsB,YAAY,CAChC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,SAAS,SAAS,EAAE,GAC5B,OAAO,CAAC,aAAa,EAAE,CAAC,CAkB1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CA0HjG;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAIjE"}