rvlib-shipkit 1.1.1 → 1.3.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 (210) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +1 -1
  3. package/package.json +3 -1
  4. package/src/assets/run-assets.ts +3 -8
  5. package/src/check.ts +40 -2
  6. package/src/checks/SK001-package-json-shape.check.ts +14 -3
  7. package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
  8. package/src/checks/SK004-gitignore-policy.check.ts +11 -3
  9. package/src/checks/SK005-rvlib-versions.check.ts +13 -1
  10. package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
  11. package/src/checks/SK007-github-visibility.check.ts +9 -9
  12. package/src/checks/SK008-reviewer-agent.check.ts +14 -8
  13. package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
  14. package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
  15. package/src/checks/SK015-nested-repo.check.ts +5 -1
  16. package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
  17. package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
  18. package/src/checks/SK024-identity-colors.check.ts +54 -0
  19. package/src/checks/SK025-release-drift.check.ts +58 -0
  20. package/src/checks/SK026-asks-autoclose.check.ts +44 -0
  21. package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
  22. package/src/checks/SK028-readme.check.ts +79 -0
  23. package/src/cli/cmd_Assets.ts +1 -1
  24. package/src/cli/cmd_Check.ts +26 -1
  25. package/src/cli/cmd_Ci.ts +23 -5
  26. package/src/cli/cmd_CrossRepoActivity.ts +116 -0
  27. package/src/cli/cmd_GlobalProcesses.ts +155 -0
  28. package/src/cli/cmd_Home.ts +170 -0
  29. package/src/cli/cmd_Keys.ts +17 -3
  30. package/src/cli/cmd_Overview.ts +98 -0
  31. package/src/cli/cmd_Planned.ts +117 -0
  32. package/src/cli.ts +30 -2
  33. package/src/collections/checks.ts +3 -2
  34. package/src/collections/providers.ts +1 -1
  35. package/src/collections/test-time.ts +10 -1
  36. package/src/comfy/comfy-panel.ts +1 -16
  37. package/src/core/agent-spawn.ts +46 -0
  38. package/src/core/archived.ts +82 -0
  39. package/src/core/attach.ts +14 -9
  40. package/src/core/canonical-scripts.ts +12 -3
  41. package/src/core/check-file-names.ts +8 -0
  42. package/src/core/check-runner.ts +80 -30
  43. package/src/core/discover-checks.ts +38 -18
  44. package/src/core/file-stamp.ts +20 -0
  45. package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
  46. package/src/core/identity-color.ts +203 -0
  47. package/src/core/identity.ts +91 -0
  48. package/src/core/job-follow.ts +102 -0
  49. package/src/core/job-run.ts +27 -0
  50. package/src/core/jobs.ts +87 -27
  51. package/src/core/legacy-shapes.ts +1 -14
  52. package/src/core/machine-db.ts +83 -0
  53. package/src/core/one-lib.ts +85 -0
  54. package/src/core/port-holders.ts +43 -0
  55. package/src/core/repo-facts.ts +14 -0
  56. package/src/core/retention.ts +222 -0
  57. package/src/core/retired-checks.ts +7 -0
  58. package/src/core/run-check.ts +12 -4
  59. package/src/core/run-ci.ts +46 -12
  60. package/src/core/run-fix.ts +7 -2
  61. package/src/core/run-fixers.ts +10 -6
  62. package/src/core/slots.ts +5 -0
  63. package/src/core/statement-end.ts +17 -0
  64. package/src/core/wire.ts +11 -0
  65. package/src/fleet/scan-fleet.ts +2 -1
  66. package/src/home/activity-shape.ts +135 -0
  67. package/src/home/activity.ts +249 -0
  68. package/src/home/attach.ts +95 -0
  69. package/src/home/autostart.ts +108 -0
  70. package/src/home/global-units.ts +87 -0
  71. package/src/home/gui-app/ShipkitGui.swift +159 -0
  72. package/src/home/gui-app.ts +99 -0
  73. package/src/home/handoff.ts +51 -0
  74. package/src/home/home-launchd.ts +104 -0
  75. package/src/home/home-paths.ts +88 -0
  76. package/src/home/home-routes.ts +235 -0
  77. package/src/home/home-start.ts +105 -0
  78. package/src/home/home-state.ts +43 -0
  79. package/src/home/home-words.ts +12 -0
  80. package/src/home/hubs.ts +286 -0
  81. package/src/home/lib-watch.ts +139 -0
  82. package/src/home/machine-stores.ts +115 -0
  83. package/src/home/open-app.ts +16 -0
  84. package/src/home/pm-config.ts +37 -0
  85. package/src/home/pm.ts +174 -0
  86. package/src/home/process-rows.ts +205 -0
  87. package/src/home/resources.ts +365 -0
  88. package/src/home/run-daemon.ts +137 -0
  89. package/src/home/run-pm.ts +150 -0
  90. package/src/home/run-unit-action.ts +25 -0
  91. package/src/home/run-web.ts +37 -0
  92. package/src/home/serve-daemon.ts +31 -0
  93. package/src/home/serve-web.ts +175 -0
  94. package/src/home/supervise.ts +361 -0
  95. package/src/home/via-hub.ts +142 -0
  96. package/src/home/widget-text.ts +119 -0
  97. package/src/home/widgets.ts +102 -0
  98. package/src/hooks/run-hooks.ts +20 -3
  99. package/src/ledger/check-stats.ts +19 -45
  100. package/src/ledger/db.ts +69 -17
  101. package/src/ledger/gate-checks.ts +22 -3
  102. package/src/ledger/git-scan.ts +31 -7
  103. package/src/ledger/house-runs.ts +43 -0
  104. package/src/ledger/ledger.ts +40 -14
  105. package/src/ledger/live-timeline.ts +4 -19
  106. package/src/ledger/run-log.ts +92 -28
  107. package/src/ledger/store.ts +245 -103
  108. package/src/ledger/world-cache.ts +64 -15
  109. package/src/plan/cadence.ts +253 -0
  110. package/src/plan/catch-up.ts +37 -0
  111. package/src/plan/plan-timeline.ts +99 -0
  112. package/src/plan/planner.ts +428 -0
  113. package/src/plan/repo-source.ts +212 -0
  114. package/src/problems/problem-line.ts +1 -1
  115. package/src/release/run-release.ts +9 -2
  116. package/src/release/unit-resolve.ts +8 -12
  117. package/src/repo-config.ts +61 -4
  118. package/src/serve/house-runs.ts +53 -0
  119. package/src/serve/hub-app/activity-st.ts +168 -0
  120. package/src/serve/hub-app/activity-view.tsx +218 -0
  121. package/src/serve/hub-app/activity.css +78 -0
  122. package/src/serve/hub-app/admin-svc.ts +41 -0
  123. package/src/serve/hub-app/admin-view.tsx +42 -0
  124. package/src/serve/hub-app/chart-view.ts +12 -5
  125. package/src/serve/hub-app/check-view.tsx +9 -9
  126. package/src/serve/hub-app/check-words.ts +3 -1
  127. package/src/serve/hub-app/checks-svc.ts +12 -8
  128. package/src/serve/hub-app/col-card.tsx +4 -4
  129. package/src/serve/hub-app/cols-svc.ts +92 -13
  130. package/src/serve/hub-app/comfy-svc.ts +12 -6
  131. package/src/serve/hub-app/comfy-view.tsx +3 -3
  132. package/src/serve/hub-app/controls.css +32 -0
  133. package/src/serve/hub-app/cycle-button.tsx +36 -0
  134. package/src/serve/hub-app/dock-anim.ts +25 -0
  135. package/src/serve/hub-app/dock-st.ts +329 -0
  136. package/src/serve/hub-app/dock-view.tsx +68 -0
  137. package/src/serve/hub-app/dock.css +35 -0
  138. package/src/serve/hub-app/floating.tsx +49 -0
  139. package/src/serve/hub-app/geometry-view.tsx +4 -4
  140. package/src/serve/hub-app/hash-route.ts +8 -2
  141. package/src/serve/hub-app/header-bar.tsx +102 -95
  142. package/src/serve/hub-app/health-svc.ts +3 -3
  143. package/src/serve/hub-app/help.tsx +10 -7
  144. package/src/serve/hub-app/home-st.ts +278 -0
  145. package/src/serve/hub-app/home-strip.css +113 -0
  146. package/src/serve/hub-app/home-strip.tsx +376 -0
  147. package/src/serve/hub-app/home.html +21 -0
  148. package/src/serve/hub-app/hub-app.tsx +221 -177
  149. package/src/serve/hub-app/hub-types.ts +0 -3
  150. package/src/serve/hub-app/hub.css +112 -52
  151. package/src/serve/hub-app/icons.tsx +34 -0
  152. package/src/serve/hub-app/index.html +7 -1
  153. package/src/serve/hub-app/keys.ts +60 -0
  154. package/src/serve/hub-app/layout-st.ts +55 -0
  155. package/src/serve/hub-app/live-svc.ts +35 -72
  156. package/src/serve/hub-app/machine-st.ts +333 -0
  157. package/src/serve/hub-app/md.ts +38 -5
  158. package/src/serve/hub-app/media-svc.ts +63 -44
  159. package/src/serve/hub-app/media-view.tsx +20 -20
  160. package/src/serve/hub-app/next-run.ts +24 -30
  161. package/src/serve/hub-app/palette-st.ts +92 -0
  162. package/src/serve/hub-app/palette-view.tsx +47 -0
  163. package/src/serve/hub-app/pill-row.tsx +65 -0
  164. package/src/serve/hub-app/planned-st.ts +107 -0
  165. package/src/serve/hub-app/planned-view.tsx +114 -0
  166. package/src/serve/hub-app/planned.css +42 -0
  167. package/src/serve/hub-app/processes-svc.ts +63 -0
  168. package/src/serve/hub-app/project-list.css +119 -0
  169. package/src/serve/hub-app/project-list.tsx +266 -0
  170. package/src/serve/hub-app/projects-svc.ts +253 -23
  171. package/src/serve/hub-app/prose-hits-st.ts +3 -3
  172. package/src/serve/hub-app/prose-hits-view.tsx +6 -6
  173. package/src/serve/hub-app/readme-svc.ts +38 -0
  174. package/src/serve/hub-app/readme-view.tsx +19 -0
  175. package/src/serve/hub-app/readme.css +11 -0
  176. package/src/serve/hub-app/repo-sections.tsx +61 -0
  177. package/src/serve/hub-app/repo-st.ts +340 -0
  178. package/src/serve/hub-app/runs-tabs.ts +1 -1
  179. package/src/serve/hub-app/screen-cache.ts +74 -0
  180. package/src/serve/hub-app/sidebar.tsx +84 -0
  181. package/src/serve/hub-app/test-view.tsx +4 -4
  182. package/src/serve/hub-app/tests-svc.ts +3 -3
  183. package/src/serve/hub-app/tests-view.tsx +41 -14
  184. package/src/serve/hub-app/timeline-chart.tsx +55 -39
  185. package/src/serve/hub-app/timeline-geometry.ts +40 -5
  186. package/src/serve/hub-app/timeline-svc.ts +7 -6
  187. package/src/serve/hub-app/timeline-view.tsx +33 -16
  188. package/src/serve/hub-app/tree-row.tsx +2 -2
  189. package/src/serve/hub-app/tree-svc.ts +2 -2
  190. package/src/serve/hub-app/wikis-view.tsx +10 -6
  191. package/src/serve/hub-runtime.ts +550 -0
  192. package/src/serve/other-repos.ts +8 -45
  193. package/src/serve/project-gutter-route.ts +121 -20
  194. package/src/serve/project-gutter.ts +33 -11
  195. package/src/serve/readme-route.ts +57 -0
  196. package/src/serve/repo-processes.ts +89 -0
  197. package/src/serve/run-serve.ts +110 -402
  198. package/src/serve/serve-hub.ts +155 -52
  199. package/src/serve/stale-restart.ts +5 -0
  200. package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
  201. package/src/serve/warm-cell.ts +95 -0
  202. package/src/shipkit-files.ts +130 -0
  203. package/src/shipkit.ts +2 -1
  204. package/src/suite/kept-timings.ts +1 -1
  205. package/src/wiki/graph-positions.ts +1 -1
  206. package/src/checks/contracts.check.ts +0 -20
  207. package/src/serve/health-schedule.ts +0 -102
  208. package/src/serve/hub-app/hub-st.ts +0 -250
  209. package/src/serve/hub-app/project-gutter.css +0 -65
  210. package/src/serve/hub-app/project-gutter.tsx +0 -128
@@ -4,7 +4,11 @@
4
4
  action, and red/orange/green reserved for STATE. No rule outside this
5
5
  block writes a colour, and tests/hub-css-invariants.test.ts fails one
6
6
  that does. */
7
- :root{--bg:#0d0e10;--panel:#141619;--panel2:#1b1e22;--line:#282c31;--ink:#e2e4e7;--dim:#8d9299;
7
+ :root{--bg:#0d0e10;--panel:#141619;
8
+ /* the surfaces of the regions, each a small step from the page so the rail,
9
+ the header and the dock read apart at a glance (no light theme: the page
10
+ is dark only, and colorDark is what it draws) */
11
+ --bg-side:#131519;--bg-top:#171a1f;--bg-dock:#10151b;--panel2:#1b1e22;--line:#282c31;--ink:#e2e4e7;--dim:#8d9299;
8
12
  --accent:#6b8fb3;--sel:#1a2430;--chk1:#16181b;--chk2:#121417;
9
13
  /* state, and nothing else: red broken · orange taste pending · green measured passing */
10
14
  --red:#d2605c;--warn:#cf9a4e;--ok:#6faa86;--blue:#6f9bd1;
@@ -16,58 +20,91 @@
16
20
  --k1:#8ca8c7;--k2:#a892c9;--k3:#6fb0b8;--k4:#c792b4;--k5:#9198a1;--k6:#8f95cf;
17
21
  --wash:rgba(107,143,179,.15);--wash2:rgba(107,143,179,.11);--hair:rgba(107,143,179,.5);
18
22
  --errbg:#2a1a1a;--errline:#4a2c2c;--errink:#e3bebe;
23
+ /* a running thing's wash, the green twin of errbg and errline */
24
+ --okbg:#15251c;--okline:#2f5a40;
19
25
  /* the ONE spacing scale: every padding, margin and gap in this sheet is a
20
26
  value from it or 0, and tests/hub-css-invariants.test.ts fails the rest */
21
27
  --s0:2px;--s1:4px;--s2:8px;--s3:12px;--s4:16px}
22
28
  *{box-sizing:border-box}html,body,#root{height:100%}
23
29
  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans-serif;overflow:hidden}
24
- /* the page is a flex ROW: a column mounted before .appcol sits left of the
25
- header and everything under it, and .appcol stacks the header, the live
26
- strip and the body */
27
- .app{display:flex;height:100vh}
28
- .appcol{flex:1;min-width:0;display:flex;flex-direction:column}
29
- .appbody{flex:1;min-height:0;display:grid;grid-template-columns:330px 1fr}
30
+ /* a link with no class of its own: the browser default is dark blue on this dark page */
31
+ a,a:visited{color:var(--accent)}
32
+ /* the page is the window's height, the dock under it (hub-app.tsx): over
33
+ the dock two panels, the project list, then .appcol, which stacks the
34
+ strip, the header and the body */
35
+ .appv{height:100vh}
36
+ .app{height:100%}
37
+ /* a panel's class lands on the div inside the panel (react-resizable-panels) */
38
+ .pjpanel,.appcol,.pside,.pmain{height:100%;min-width:0}
39
+ .apptop,.pdock{height:100%;min-height:0}
40
+ .pjpanel{display:flex}
41
+ .appcol{display:flex;flex-direction:column}
42
+ /* one region per repo visited, the shown one alone displayed */
43
+ .repo{flex:1;min-height:0;display:flex;flex-direction:column}
44
+ .repo[hidden]{display:none}
45
+ .appbody{flex:1;min-height:0}
46
+ .pside{display:flex;flex-direction:column}
47
+ .pside > aside{flex:1}
48
+ .pmain{display:flex;flex-direction:column}
49
+ .pmain > main{flex:1}
30
50
 
31
- /* ── the header: repo name, the core pages, the two runs and the run state ── */
32
- .hdr{display:flex;gap:var(--s2);align-items:center;height:36px;flex:none;padding:0 var(--s3);border-bottom:1px solid var(--line);background:var(--panel)}
51
+ /* ── a repo's bar: on the home the strip (home-strip.css .hstrip), on a hub
52
+ of its own the header (.hdr). Its name, what runs now and the two runs,
53
+ then its processes, its trouble and its editor icons. One line at any
54
+ width: the container queries shrink the labels to their icons, then the
55
+ run state to its spinner ── */
56
+ /* the repo's processes (serve.processes): a dot, its id, a link once up */
57
+ .hprocs{display:inline-flex;gap:var(--s1);align-items:center}
58
+ .hproc{display:inline-flex;align-items:center;gap:var(--s0)}
59
+ .hproc button{padding:var(--s0) var(--s2);font-size:12px}
60
+ .hproc a{color:var(--accent);text-decoration:none;font-size:12px}
61
+ .pdot{display:inline-block;width:7px;height:7px;border-radius:4px;margin-right:var(--s1);background:var(--line)}
62
+ .pdot.up{background:var(--ok)}.pdot.starting,.pdot.stopping{background:var(--warn)}.pdot.crashed{background:var(--red)}
63
+ .hdr{display:flex;gap:var(--s2);align-items:center;height:36px;flex:none;padding:0 var(--s3);border-bottom:1px solid var(--line);background:var(--bg-top)}
33
64
  .hdr .sp{flex:1}
34
65
  .hname{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);font-weight:600;margin-right:var(--s2);white-space:nowrap}
35
- .hnav{display:flex;gap:var(--s1);overflow:hidden}
36
- .hnav button{border-color:transparent;color:var(--dim);border-radius:0;border-bottom:1px solid transparent;white-space:nowrap}
37
- .hnav button:hover{color:var(--ink);border-color:transparent;border-bottom-color:var(--line)}
38
- .hnav button.on{color:var(--accent);border-bottom-color:var(--accent)}
66
+ .hdr,.hstrip{container-type:inline-size;white-space:nowrap}
67
+ .hrun{display:inline-flex;align-items:center;gap:var(--s1);flex:none;margin-left:var(--s2)}
68
+ .hbtn{display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s2);font-size:11.5px;white-space:nowrap;flex:none}
69
+ .hbtn .hico{width:13px;height:13px}
70
+ /* the editor icons: bare, dim, lit on hover, as the activity page's Fork icon */
71
+ .hicon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:24px;height:22px;padding:0;border:0;background:none;color:var(--dim)}
72
+ .hicon:hover{color:var(--accent)}
73
+ .hicon .hico{width:15px;height:15px}
39
74
  /* the run state holds its width whatever it says, so a run never moves the buttons beside it */
40
75
  .hstatus{max-width:420px;flex:0 1 auto;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
41
76
  .hstatus.red{color:var(--red)}
42
77
  .hstatus.warn{color:var(--warn)}
43
- .hstate{display:inline-flex;gap:var(--s1);align-items:center;width:260px;min-width:260px;flex:none;justify-content:flex-end;font-size:11.5px;color:var(--dim);overflow:hidden}
78
+ /* its words sit against the run buttons, cut with an ellipsis */
79
+ .hstate{display:inline-flex;gap:var(--s1);align-items:center;width:200px;min-width:200px;flex:none;justify-content:flex-end;font-size:11.5px;color:var(--dim);overflow:hidden}
44
80
  .hstate.on{color:var(--ink)}
45
- .hstate .htext{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
81
+ .hstate .htext{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
82
+ .hstate .spin.idle{visibility:hidden;animation:none}
83
+ /* after .hstate, so a narrow bar wins over its width */
84
+ @container (max-width:600px){.hbtn .lbl{display:none}.hbtn{padding:0 var(--s1)}}
85
+ @container (max-width:520px){.hstate{width:120px;min-width:120px}}
86
+ @container (max-width:420px){.hstate .htext{display:none}.hstate{width:16px;min-width:16px}}
46
87
 
47
- /* ── the live strip: the one run chart. Folded, one thin row of ONE height
48
- whatever runs; unfolded, the chart at the height its lanes hold, set
49
- inline from a held value that grows at once and shrinks only after a
50
- while, so it never jumps every second. The strip clips nothing itself, so
51
- its (?) opens below it over the page; the chart box clips the chart ── */
52
- .live{display:flex;flex-direction:column;height:30px;flex:none;border-bottom:1px solid var(--line);background:var(--bg);font-size:11px;color:var(--dim)}
53
- .live .hbub{top:calc(100% + 6px);bottom:auto;z-index:30}
88
+ /* a row of a (?) legend: one glyph or word, then its meaning */
54
89
  .lrow{display:flex;gap:var(--s2);align-items:center;height:29px;flex:none;padding:0 var(--s3)}
55
- .lrow .sp{flex:1}
56
- .lfold{display:inline-flex;gap:var(--s1);align-items:center;border-color:transparent;color:var(--dim);padding:var(--s0) var(--s1)}
57
- .lfold .sk{font-size:9px;opacity:.55}
58
- .lwho{width:180px;flex:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink)}
59
- .live.idle .lwho{color:var(--dim)}
60
- .lwho i{font-style:normal;color:var(--dim);margin-left:var(--s1)}
61
- .lwho b{font-weight:500;color:var(--accent);margin-left:var(--s1)}
62
- .lchart{flex:1;min-width:0;height:21px;border:1px solid var(--line);border-radius:3px;overflow:hidden}
63
- .lopen{flex:1;min-height:0;overflow-y:auto;padding:0 var(--s3);scrollbar-width:thin;scrollbar-color:var(--line) transparent}
64
- .lnote,.lmore{flex:none;color:var(--warn);white-space:nowrap}
65
- .lbusy{flex:none;white-space:nowrap}
66
90
 
67
- /* ── sidebar ── */
68
- aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto;min-height:0}
69
- .shead{position:sticky;top:0;background:var(--panel);padding:var(--s3);border-bottom:1px solid var(--line);
70
- font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);z-index:2;display:flex;justify-content:space-between;align-items:center}
91
+ /* ── the left pane: the sections, the filter, then the tree, the one part
92
+ that scrolls ── */
93
+ aside{border-right:1px solid var(--line);background:var(--panel);min-height:0;display:flex;flex-direction:column;overflow:hidden}
94
+ aside > *{flex:none}
95
+ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
96
+ /* a repo's sections: the project list's lines (project-list.css .pjpl), a
97
+ little shorter, the current one lit and marked in the accent */
98
+ .rsecs{display:flex;flex-direction:column;padding:var(--s1) 0;border-bottom:1px solid var(--line)}
99
+ .rsec{display:flex;align-items:center;gap:var(--s2);width:100%;height:26px;padding:0 var(--s3);background:transparent;border:0;border-radius:0;text-align:left;color:var(--dim);font-size:13px;cursor:pointer}
100
+ .rsec:hover{background:var(--panel2);color:var(--ink);border-color:transparent}
101
+ .rsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
102
+ .rsec .hico{flex:none;width:16px;height:16px}
103
+ .rsecl{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
104
+ .rsecn{flex:none;min-width:16px;height:16px;padding:0 var(--s1);border-radius:8px;font-size:10px;font-weight:700;line-height:16px;text-align:center;color:var(--bg)}
105
+ .rsecn.nred{background:var(--red)}
106
+ .rsecn.nwarn{background:var(--warn)}
107
+ .rsec:hover .pjkey,.rsec.cur .pjkey{opacity:1}
71
108
  /* segmented severity filter — all · needs · red. A thin border and an
72
109
  underline on the live one: no fill, no pill */
73
110
  .seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden;flex:none}
@@ -78,10 +115,16 @@ aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto
78
115
  .seg button.on{color:var(--accent);border-bottom-color:var(--accent)}
79
116
  .seg button .sk{margin-left:var(--s1);font-size:9px;opacity:.55}
80
117
  .swhen{padding:0 var(--s3) var(--s2);font-size:11px;color:var(--dim)}
81
- .ssearch{padding:var(--s2) var(--s3)}
118
+ /* a narrow pane puts the severity filter under the input, never over it */
119
+ .ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3)}
120
+ .sfield{position:relative;flex:1 1 96px;min-width:0;display:flex}
82
121
  .ssearch input{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:4px;color:var(--ink);
83
- padding:var(--s1) var(--s2);font-size:12.5px;outline:none}
122
+ padding:var(--s0) var(--s4) var(--s0) var(--s2);font-size:12px;outline:none}
123
+ .ssearch input::placeholder{color:var(--dim);opacity:.6;font-size:11.5px}
84
124
  .ssearch input:focus{border-color:var(--accent)}
125
+ /* its key, the way the project list prints its keys (.pjkey) */
126
+ .skey{position:absolute;right:var(--s2);top:50%;transform:translateY(-50%);pointer-events:none;font:10px/1 ui-monospace,Menlo,monospace;color:var(--dim);opacity:.6;background:none;border:0;padding:0}
127
+ .sfield input:focus + .skey{opacity:0}
85
128
  /* ── the resource tree: ONE row shape, a fixed grid. The indent rides in
86
129
  the FIRST column and the label eats the slack, so every right side slot
87
130
  lines up down the whole column whatever the depth. Colour is on the
@@ -135,10 +178,11 @@ aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto
135
178
  .tnote b{font-weight:600;color:var(--red)}
136
179
  .stabs{display:flex;gap:var(--s2);align-items:center;padding:var(--s1) var(--s3);border-bottom:1px solid var(--line)}
137
180
  .stabs .sp{flex:1}
138
- .stabs .cnt{font-size:11px;color:var(--dim)}
181
+ .stabs .cnt{min-width:4ch;text-align:right;font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums}
139
182
  .catgen{display:inline-flex;gap:var(--s1)}
140
183
  .catgen button{padding:var(--s0) var(--s2);font-size:11px;font-weight:700}
141
184
  .tempty{padding:var(--s2) var(--s3);font-size:12px;color:var(--dim);white-space:pre-wrap}
185
+ .twait{display:flex;align-items:center;gap:var(--s2)}
142
186
  .warnline{color:var(--red)}
143
187
  .fold{cursor:pointer;color:var(--dim);font-size:15px;display:inline-block;width:22px;text-align:center;
144
188
  padding:var(--s0);border-radius:3px;user-select:none}
@@ -373,7 +417,6 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
373
417
  .tlb.bad,.tlb.red{fill:var(--red)}
374
418
  .tlb.rep{fill:var(--line)}
375
419
  .tlb.run{fill:var(--accent);animation:pulse 1.6s ease-in-out infinite}
376
- .tlb.repo{fill:var(--k6)}
377
420
  .tlband.run{stroke:var(--accent)}
378
421
  .tlconc{fill:var(--hair)}
379
422
  .tlconc.over{fill:var(--warn)}
@@ -388,9 +431,9 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
388
431
  .tlgap.live text{fill:var(--ink)}
389
432
  .tlgap text{font-size:10px;pointer-events:none}
390
433
  .tlnow{stroke:var(--accent)}
391
- /* fixed to the viewport, placed from the pointer: every box of the strip
392
- clips its overflow, and a tooltip inside them is cut to a sliver */
393
- .tlaxtip{position:fixed;z-index:30;display:flex;flex-direction:column;gap:0;padding:var(--s1) var(--s2);pointer-events:none;
434
+ /* placed by floating-ui under the pointer, in a portal: every box of the
435
+ strip clips its overflow, and a tooltip inside them is cut to a sliver */
436
+ .tlaxtip{z-index:30;display:flex;flex-direction:column;gap:0;padding:var(--s1) var(--s2);pointer-events:none;
394
437
  background:var(--panel);border:1px solid var(--line);border-radius:3px;box-shadow:0 2px 12px var(--shadow);font-size:11px;white-space:nowrap}
395
438
  .tlaxtip .dim{color:var(--dim)}
396
439
  /* the runs page: one tab bar, then one list. Every line on a grid of fixed
@@ -422,6 +465,8 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
422
465
  per test check (the tests page), the failing files with their failure line */
423
466
  .rtime{grid-template-columns:minmax(0,1fr) 88px 72px 128px 72px 40px 88px}
424
467
  .rcheckt{grid-template-columns:152px 88px 48px 56px minmax(0,1fr) 72px}
468
+ /* every test file: file, check, duration, status, age */
469
+ .rall{grid-template-columns:minmax(0,1fr) 152px 88px 52px 72px}
425
470
  .rhead .tleft{text-align:left}
426
471
  .tshare{display:grid;grid-template-columns:minmax(0,1fr) 36px;gap:var(--s1);align-items:center}
427
472
  .ttrack{display:block;height:6px;border-radius:1px;overflow:hidden;background:var(--panel2)}
@@ -542,17 +587,18 @@ a.tok{color:var(--accent)}
542
587
  height, so a streaming run never jumps the card it sits in */
543
588
  .wlog{margin:0;min-height:120px;max-height:320px;overflow-y:auto;background:var(--panel2);border:1px solid var(--line);
544
589
  border-radius:3px;padding:var(--s2);font:11.5px/1.5 ui-monospace,Menlo,monospace;color:var(--ink);white-space:pre-wrap}
545
- /* THE HELP: a `(?)` that says its one sentence at once, on hover and on
546
- focus. No delay, no library, no state */
590
+ /* THE HELP: a `(?)` that says its one sentence on hover and on focus, the
591
+ bubble placed by floating-ui (floating.tsx) */
547
592
  .hlp{display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;flex:none;
548
593
  border:1px solid var(--line);border-radius:50%;color:var(--dim);font-size:9px;line-height:1;cursor:help;
549
- position:relative;outline:none;user-select:none}
594
+ outline:none;user-select:none}
550
595
  .hlp:hover,.hlp:focus{color:var(--accent);border-color:var(--accent)}
551
- .hbub{display:none;position:absolute;left:50%;bottom:calc(100% + 6px);transform:translateX(-50%);width:max-content;
596
+ /* out of sight, still read by a screen reader and found by a page search */
597
+ .vh{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
598
+ .hbub,.ftip{z-index:40;width:max-content;
552
599
  max-width:260px;background:var(--panel2);border:1px solid var(--line);border-radius:3px;box-shadow:0 2px 10px var(--shadow);
553
600
  padding:var(--s1) var(--s2);font:11.5px/1.4 system-ui,sans-serif;text-transform:none;letter-spacing:0;
554
- color:var(--ink);white-space:normal;z-index:9}
555
- .hlp:hover .hbub,.hlp:focus .hbub{display:block}
601
+ color:var(--ink);white-space:normal;pointer-events:none}
556
602
  /* the type legend: every glyph of the tree with its word, in one bubble */
557
603
  .legend{display:flex;flex-direction:column;gap:var(--s1)}
558
604
  .legend .lrow{display:flex;gap:var(--s2);align-items:baseline;white-space:normal}
@@ -596,7 +642,9 @@ a.tok{color:var(--accent)}
596
642
  .graphview{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3) var(--s4) var(--s4)}
597
643
  .gvhead{display:flex;gap:var(--s2);align-items:center;font-size:13px;min-width:0}
598
644
  .gvhead .gvtext{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
599
- .graphview iframe{flex:1;min-height:0;width:100%;border:1px solid var(--line);border-radius:3px;background:var(--bg)}
645
+ .gvframe{flex:1;min-height:0;position:relative;display:flex;border:1px solid var(--line);border-radius:3px;background:var(--bg)}
646
+ .gvwait{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:var(--s2);font-size:12px;color:var(--dim)}
647
+ .graphview iframe{position:relative;flex:1;min-height:0;width:100%;border:0;background:transparent}
600
648
 
601
649
  /* ── comfy item view (comfy-ts's own panel fills the stage) ── */
602
650
  .comfyview{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3) var(--s4) var(--s4)}
@@ -646,7 +694,7 @@ a.mint{color:var(--accent);font-size:12px;text-decoration:none}
646
694
  a.mint:hover{text-decoration:underline}
647
695
  .colcard h3 .stchip,.colcard h3 a.mint{margin-left:var(--s2)}
648
696
 
649
- /* ── cmd+j palette ── */
697
+ /* ── the cmd+k picker ── */
650
698
  .palov{position:fixed;inset:0;background:var(--scrim);z-index:100;display:flex;justify-content:center;align-items:flex-start;
651
699
  padding-top:14vh}
652
700
  .pal{width:min(560px,90vw);background:var(--panel);border:1px solid var(--line);border-radius:3px;
@@ -658,6 +706,9 @@ a.mint:hover{text-decoration:underline}
658
706
  .prow.on{background:var(--panel2)}
659
707
  .prow.none{color:var(--dim);cursor:default}
660
708
  .prow .plabel{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
709
+ /* the entry's icon: a place's button icon, a repo's own (project-list.tsx Face) */
710
+ .prow .pface{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;color:var(--dim)}
711
+ .prow .pface img,.prow .pface .pgi{width:16px;height:16px;border-radius:4px;object-fit:contain}
661
712
  .prow .pchip{font-size:10px;color:var(--dim);background:var(--bg);border:1px solid var(--line);border-radius:5px;
662
713
  padding:var(--s0) var(--s1);text-transform:uppercase;letter-spacing:.08em;flex:none}
663
714
 
@@ -669,3 +720,12 @@ a.mint:hover{text-decoration:underline}
669
720
  pre{background:var(--panel);border:1px solid var(--line);border-radius:3px;padding:var(--s4);overflow-x:auto;
670
721
  font:12.5px/1.55 ui-monospace,Menlo,monospace;white-space:pre-wrap}
671
722
  .ok{color:var(--ok)}.warn{color:var(--warn)}.fail{color:var(--red)}.dim{color:var(--dim)}
723
+
724
+ /* ── the admin page: one row per control, its sentence under its name ── */
725
+ .adrow{display:flex;align-items:center;gap:var(--s4);padding:var(--s3) 0;border-top:1px solid var(--line)}
726
+ .adtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--s0);font-size:13px}
727
+ .adtext .dim{font-size:12px}
728
+ .adctl{flex:none;display:flex;align-items:center}
729
+ .adflag{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px;color:var(--dim)}
730
+ .adflag input{width:auto;margin:0}
731
+ .adconfirm{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px}
@@ -0,0 +1,34 @@
1
+ // the few line icons of the home (the overview's section headers, the places
2
+ // on top of the project list, a repo page's sections and its top bar): one 20
3
+ // unit path each, stroked in the text colour, so the page's tokens colour them
4
+ // and nothing else does.
5
+ export const ICON = {
6
+ shipkit: 'M3 7.5 10 4l7 3.5v5L10 16l-7-3.5zM3 7.5 10 11l7-3.5M10 11v5',
7
+ process: 'M2 10h3.5l2-5 5 10 2-5H18',
8
+ monitor: 'M3 4h14v9H3zM7 16.5h6M10 13v3.5',
9
+ archived: 'M2.5 4h15v3.5h-15zM4 7.5V16h12V7.5M8 10.5h4',
10
+ home: 'M3 9.5 10 3.5l7 6M5 8v8.5h10V8M8.5 16.5v-4.5h3v4.5',
11
+ planned: 'M5.5 3h9M5.5 17h9M6.5 3c0 4 7 4 7 7s-7 3-7 7M13.5 3c0 4-7 4-7 7s7 3 7 7',
12
+ browser: 'M10 2.5a7.5 7.5 0 1 0 0 15a7.5 7.5 0 1 0 0-15M2.5 10h15M10 2.5c-3 3-3 12 0 15M10 2.5c3 3 3 12 0 15',
13
+ window: 'M2.5 4h15v12h-15zM2.5 7.5h15M5 5.75h.01M7 5.75h.01',
14
+ activity: 'M6 3v14M6 6.5a2 2 0 1 0 0 .01M6 13.5a2 2 0 1 0 0 .01M10 6.5h7M10 13.5h7M10 10h5',
15
+ fork: 'M6 5v10M6 3a2 2 0 1 0 0 .01M6 17a2 2 0 1 0 0 .01M14 5a2 2 0 1 0 0 .01M14 7c0 4.5-8 3.5-8 7',
16
+ search: 'M8.5 3a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0-11M12.5 12.5 17 17',
17
+ // a repo page's sections, then its two runs and its editor
18
+ checks: 'M4 10.5 7.5 14 16 5.5',
19
+ runs: 'M3 5h8M3 10h12M3 15h6',
20
+ asks: 'M3 4h14v9H8l-4 3.5V13H3z',
21
+ tests: 'M8 3h4M8.5 3v5L4 16.5h12L11.5 8V3M6.2 12.5h7.6',
22
+ media: 'M3 4h14v12H3zM3 13l4-4 4 4 2-2 4 4M13 7.5h.01',
23
+ wikis: 'M10 5.5C8.5 4 6 3.5 3 4v11c3-.5 5.5 0 7 1.5c1.5-1.5 4-2 7-1.5V4c-3-.5-5.5 0-7 1.5zM10 5.5v11',
24
+ admin: 'M4 5h12M4 10h12M4 15h12M7 3.5v3M13 8.5v3M9 13.5v3',
25
+ health: 'M2 10h3.5l2-5 5 10 2-5H18',
26
+ gate: 'M10 2.5 16 5v4.5c0 4-2.7 6.5-6 8c-3.3-1.5-6-4-6-8V5zM7.5 10l2 2 3.5-3.5',
27
+ // the VS Code mark, its ribbon drawn as one outline
28
+ vscode: 'M14 2.5l3.5 1.7v11.6L14 17.5zM14 2.5 4.5 12.3 2.5 11V9l2-1.3L14 17.5M14 6.8 10.2 10l3.8 3.2',
29
+ sort: 'M6 3.5v13M3 13.5l3 3 3-3M14 16.5v-13M11 6.5l3-3 3 3',
30
+ } as const
31
+
32
+ export const Icon = ({ d }: { d: string }) => (
33
+ <svg className="hico" viewBox="0 0 20 20" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d={d} /></svg>
34
+ )
@@ -5,8 +5,14 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>hub</title>
7
7
  <link rel="stylesheet" href="./hub.css" />
8
- <link rel="stylesheet" href="./project-gutter.css" />
8
+ <link rel="stylesheet" href="./project-list.css" />
9
9
  <link rel="stylesheet" href="./prose-hits.css" />
10
+ <link rel="stylesheet" href="./home-strip.css" />
11
+ <link rel="stylesheet" href="./planned.css" />
12
+ <link rel="stylesheet" href="./activity.css" />
13
+ <link rel="stylesheet" href="./readme.css" />
14
+ <link rel="stylesheet" href="./dock.css" />
15
+ <link rel="stylesheet" href="./controls.css" />
10
16
  </head>
11
17
  <body>
12
18
  <div id="root"></div>
@@ -0,0 +1,60 @@
1
+ // the page's command keys, read the same on every layout: ⌘1 to ⌘9 select the
2
+ // nth repo of the project list, then the home row under the right hand: ⌘J
3
+ // the overview, ⌘K the picker, ⌘L the activity, ⌘' the plan. ⌘B cycles the
4
+ // dock at the bottom of the page (dock-st.ts): the line, slim, open.
5
+ // A digit and the quote are read from the physical key (`code`), so an
6
+ // AZERTY keyboard, where the key under 1 types `&`, selects the same repo. Ctrl stands in for
7
+ // ⌘ off a Mac. The macOS app binds none of these to a menu item, so they
8
+ // reach the page (tests/home-strip.test.tsx reads ShipkitGui.swift).
9
+ export type PageKey = { t: 'repo'; n: number } | { t: 'home' } | { t: 'planned' } | { t: 'activity' } | { t: 'picker' } | { t: 'dock' }
10
+
11
+ type KeyLike = { key: string; code: string; metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; altKey: boolean }
12
+
13
+ export function pageKey(e: KeyLike): PageKey | null {
14
+ if (!(e.metaKey || e.ctrlKey) || e.altKey) return null
15
+ const digit = /^Digit([1-9])$/.exec(e.code)
16
+ if (digit != null) return { t: 'repo', n: Number(digit[1]) }
17
+ if (e.shiftKey) return null
18
+ if (e.code === 'Quote' || e.key === "'") return { t: 'planned' }
19
+ const k = e.key.toLowerCase()
20
+ if (k === 'j') return { t: 'home' }
21
+ if (k === 'k') return { t: 'picker' }
22
+ if (k === 'l') return { t: 'activity' }
23
+ if (k === 'b') return { t: 'dock' }
24
+ return null
25
+ }
26
+
27
+ /** the shortcut each place of the home carries on its button */
28
+ export const PLACE_KEY = { overview: '⌘J', planned: "⌘'", activity: '⌘L' } as const
29
+ /** the picker's shortcut, on the second button, after home */
30
+ export const PICKER_KEY = '⌘K'
31
+ /** the dock's shortcut, on the dock's own header */
32
+ export const DOCK_KEY = '⌘B'
33
+
34
+ /** the tree's filter, on a repo page: a plain `/` focuses it, and its
35
+ * badge at the input's right prints this */
36
+ export const SEARCH_KEY = '/'
37
+
38
+ export function isSearchKey(e: Pick<KeyLike, 'key' | 'metaKey' | 'ctrlKey' | 'altKey'>): boolean {
39
+ return e.key === SEARCH_KEY && !e.metaKey && !e.ctrlKey && !e.altKey
40
+ }
41
+
42
+ /** a repo page's sections, each on ⌥ and its letter. ⌥ because every other
43
+ * layer is spoken for: the plain letters are the severity filter (a n r)
44
+ * and the chart's follow key, ⌘ with these letters is copy, reload, select
45
+ * all, new tab, minimize and close in the browser and the macOS app, and ⌥
46
+ * plus a letter only types a character inside a text field, where the page
47
+ * leaves the key alone. Read from the physical key (`code`): on a Mac ⌥C
48
+ * types ç, and AZERTY moves the letters */
49
+ export type SectionId = 'checks' | 'runs' | 'asks' | 'tests' | 'media' | 'wikis' | 'admin'
50
+ export const SECTION_LETTER: Record<SectionId, string> = { checks: 'C', runs: 'R', asks: 'A', tests: 'T', media: 'M', wikis: 'W', admin: 'D' }
51
+ /** the badge at the end of a section's line */
52
+ export const sectionKeyLabel = (id: SectionId): string => `⌥${SECTION_LETTER[id]}`
53
+
54
+ export function sectionKey(e: KeyLike): SectionId | null {
55
+ if (!e.altKey || e.metaKey || e.ctrlKey || e.shiftKey) return null
56
+ const m = /^Key([A-Z])$/.exec(e.code)
57
+ if (m == null) return null
58
+ const hit = Object.entries(SECTION_LETTER).find(([, l]) => l === m[1])
59
+ return hit == null ? null : (hit[0] as SectionId)
60
+ }
@@ -0,0 +1,55 @@
1
+ // the page's two resizable widths, a node of the root (home-st.ts): the
2
+ // project list at the far left and a repo's own sidebar. Each is dragged by
3
+ // its separator (react-resizable-panels) and kept per browser in px, so the
4
+ // next load renders at that width from its first paint. A stored value that
5
+ // is not a number, or is out of bounds, reads as the default: no runtime
6
+ // value can hand the page a list wider than LIST.max.
7
+ import { makeAutoObservable } from 'mobx'
8
+
9
+ export type Bounds = { key: string; min: number; max: number; def: number }
10
+
11
+ /** the project list: icon and name on one line */
12
+ export const LIST: Bounds = { key: 'hub:list-width', min: 140, max: 360, def: 200 }
13
+ /** a repo's sidebar: the resource tree */
14
+ export const SIDEBAR: Bounds = { key: 'hub:sidebar-width', min: 220, max: 640, def: 330 }
15
+
16
+ /** a width in its bounds, or the default for anything else */
17
+ export const clampWidth = (v: unknown, b: Bounds): number => {
18
+ const n = typeof v === 'string' ? Number(v) : typeof v === 'number' ? v : NaN
19
+ return Number.isFinite(n) && n >= b.min && n <= b.max ? Math.round(n) : b.def
20
+ }
21
+
22
+ export function loadWidth(b: Bounds): number {
23
+ try {
24
+ return clampWidth(globalThis.localStorage?.getItem(b.key), b)
25
+ } catch {
26
+ return b.def
27
+ }
28
+ }
29
+
30
+ function saveWidth(b: Bounds, px: number): void {
31
+ try {
32
+ globalThis.localStorage?.setItem(b.key, String(px))
33
+ } catch { /* private window or blocked site data: the width lasts the tab */ }
34
+ }
35
+
36
+ export class LayoutSt {
37
+ listWidth = loadWidth(LIST)
38
+ sidebarWidth = loadWidth(SIDEBAR)
39
+
40
+ constructor() {
41
+ makeAutoObservable(this)
42
+ }
43
+
44
+ /** a drag of the list's separator ended at this width */
45
+ setListWidth(px: number): void {
46
+ this.listWidth = clampWidth(Math.round(px), LIST)
47
+ saveWidth(LIST, this.listWidth)
48
+ }
49
+
50
+ /** a drag of the sidebar's separator ended at this width */
51
+ setSidebarWidth(px: number): void {
52
+ this.sidebarWidth = clampWidth(Math.round(px), SIDEBAR)
53
+ saveWidth(SIDEBAR, this.sidebarWidth)
54
+ }
55
+ }
@@ -1,30 +1,28 @@
1
- // The live strip under the header, as the page holds it: `/api/live.json`
2
- // read every second (the runs of the last ten minutes, the ones in flight,
3
- // here and in the other house repos), whether the strip is folded, and the
4
- // chart it draws. Folded, the chart is the thin rolling window of the last
5
- // two minutes; unfolded, the whole record (the recent runs of /api/live.json
6
- // over the older ones of /api/timeline.json), opened on its last ten minutes
7
- // and fit to all of it on demand. It is the hub's one run chart
8
- // (chart-view.ts, timeline-chart.tsx).
1
+ // A repo's live runs, as the page holds them: `/api/live.json` read every
2
+ // second while the repo shows (the runs of the last ten minutes and the ones
3
+ // in flight), which the header's run state reads, and the chart of the runs
4
+ // page over it: the whole record (the recent runs of /api/live.json over the
5
+ // older ones of /api/timeline.json), opened on its last ten minutes and fit
6
+ // to all of it on demand (chart-view.ts, timeline-chart.tsx). What runs in
7
+ // every repo at once is the dock's (dock-st.ts).
9
8
  import { makeAutoObservable, observableRef, runInAction } from 'mobx'
10
9
  import type { LiveView } from '../../ledger/live-timeline.ts'
11
10
  import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
12
11
  import { ChartView, type ChartSource } from './chart-view.ts'
13
- import type { HubSt } from './hub-st.ts'
12
+ import type { RepoSt } from './repo-st.ts'
14
13
  import { chartHeight } from './timeline-geometry.ts'
15
14
 
16
15
  /** how often the page asks. A strip a second behind the runner is still live */
17
16
  export const LIVE_POLL_MS = 1_000
18
- /** the folded strip's window, and the span the unfolded one opens on */
19
- export const FOLDED_WINDOW_MS = 120_000
17
+ /** the span the chart opens on */
20
18
  export const OPEN_WINDOW_MS = 600_000
21
- /** the most the unfolded chart grows to: past it the lanes scroll inside it */
19
+ /** the most the chart grows to: past it the lanes scroll inside it */
22
20
  export const OPEN_MAX_H = 320
23
- /** how long the unfolded chart keeps a height its lanes no longer fill,
21
+ /** how long the chart keeps a height its lanes no longer fill,
24
22
  * so it grows at once and never shrinks under the eye every second */
25
23
  export const HEIGHT_HOLD_MS = 10_000
26
24
 
27
- /** the height the unfolded chart shows: up at once, down only once the
25
+ /** the height the chart shows: up at once, down only once the
28
26
  * lanes have wanted less for HEIGHT_HOLD_MS */
29
27
  export type HeldHeight = { h: number; lowSince: number | null }
30
28
  export function settleHeight(prev: HeldHeight, wanted: number, now: number, holdMs = HEIGHT_HOLD_MS): HeldHeight {
@@ -33,32 +31,20 @@ export function settleHeight(prev: HeldHeight, wanted: number, now: number, hold
33
31
  return now - prev.lowSince >= holdMs ? { h: wanted, lowSince: null } : prev
34
32
  }
35
33
 
36
- /** the whole record the unfolded chart draws: every run of the live view,
34
+ /** the whole record the chart draws: every run of the live view,
37
35
  * then the older runs of the record it does not carry, newest first */
38
36
  export function mergeRuns(recent: TimelineInvocation[], record: TimelineInvocation[]): TimelineInvocation[] {
39
37
  const seen = new Set(recent.map((i) => i.runId))
40
38
  return [...recent, ...record.filter((i) => !seen.has(i.runId))].sort((a, b) => b.started - a.started)
41
39
  }
42
- /** the lanes the folded strip has room for */
43
- export const FOLDED_LANES = 5
44
-
45
- /** where the fold is remembered. localStorage is per origin and every
46
- * repo's hub has its own port, so this is per repo already */
47
- export const OPEN_KEY = 'hub:liveOpen'
48
-
49
- const readOpen = (): boolean => {
50
- try { return globalThis.localStorage?.getItem(OPEN_KEY) === '1' } catch { return false }
51
- }
52
-
53
40
  /** the server clock now: the view's stamp plus what the page clock has
54
41
  * counted since it arrived, since the page may be another machine */
55
- export const serverNow = (view: LiveView, receivedAt: number, clientNow: number): number => view.now + Math.max(0, clientNow - receivedAt)
42
+ export const serverNow = (view: { now: number }, receivedAt: number, clientNow: number): number => view.now + Math.max(0, clientNow - receivedAt)
56
43
 
57
- /** what the chart draws: folded, the rolling two minutes; open, the whole
58
- * record, opening on its last ten */
59
- export function liveSource(view: LiveView | null, now: number, open: boolean, record: TimelineInvocation[] = []): ChartSource | null {
44
+ /** what the runs page's chart draws: the whole record, opening on its last
45
+ * ten minutes */
46
+ export function liveSource(view: LiveView | null, now: number, record: TimelineInvocation[] = []): ChartSource | null {
60
47
  if (view == null) return null
61
- if (!open) return { invocations: view.invocations, now, windowMs: FOLDED_WINDOW_MS, laneCap: Math.max(1, FOLDED_LANES - view.repos.length), zoomable: false, cap: view.cap }
62
48
  return { invocations: mergeRuns(view.invocations, record), now, windowMs: null, zoomable: true, cap: view.cap, homeSpanMs: OPEN_WINDOW_MS }
63
49
  }
64
50
 
@@ -67,18 +53,16 @@ export class LiveSvc {
67
53
  /** the page clock when `view` arrived */
68
54
  receivedAt = 0
69
55
  error = ''
70
- /** a short answer to a click (a repo whose hub is not serving) */
71
- note = ''
72
- open = readOpen()
73
- /** the unfolded chart's height, held so it does not jump every second */
56
+ /** the chart's height, held so it does not jump every second */
74
57
  held: HeldHeight = { h: 0, lowSince: null }
75
58
  chart: ChartView
76
59
  private timer: ReturnType<typeof setInterval> | null = null
77
60
  private inFlight = false
61
+ private started = false
78
62
 
79
- constructor(private st: HubSt) {
80
- this.chart = new ChartView(null, () => liveSource(this.view, this.now, this.open, this.st.timeline.data?.invocations ?? []))
81
- makeAutoObservable<this, 'st' | 'timer' | 'inFlight'>(this, { st: false, timer: false, inFlight: false, view: observableRef, chart: false })
63
+ constructor(private st: RepoSt) {
64
+ this.chart = new ChartView(null, () => liveSource(this.view, this.now, this.st.timeline.data?.invocations ?? []))
65
+ makeAutoObservable<this, 'st' | 'timer' | 'inFlight' | 'started'>(this, { st: false, timer: false, inFlight: false, started: false, view: observableRef, chart: false })
82
66
  }
83
67
 
84
68
  /** the server clock, as of the page clock's last tick */
@@ -86,18 +70,9 @@ export class LiveSvc {
86
70
  return this.view == null ? this.st.clock.now : serverNow(this.view, this.receivedAt, this.st.clock.now)
87
71
  }
88
72
 
89
- toggle(): void {
90
- this.open = !this.open
91
- try { globalThis.localStorage?.setItem(OPEN_KEY, this.open ? '1' : '0') } catch { /* private window: the fold lasts the tab */ }
92
- // either fold starts live: the folded strip is a trail, nothing else
93
- this.chart.attach()
94
- this.opened()
95
- }
96
-
97
- /** unfolded: back to the last ten minutes, the older record fetched, and
98
- * a height from the lanes as they are */
73
+ /** back to the last ten minutes, the older record fetched, and a height
74
+ * from the lanes as they are */
99
75
  private opened(): void {
100
- if (!this.open) return
101
76
  this.chart.home()
102
77
  this.chart.attach()
103
78
  this.held = { h: 0, lowSince: null }
@@ -107,18 +82,22 @@ export class LiveSvc {
107
82
 
108
83
  /** the height the lanes want now, as the held rule lets it show */
109
84
  settle(now: number): void {
85
+ // no runs page on screen, no chart to size: the record is not laid out
86
+ if (this.chart.viewW === 0) return
110
87
  const l = this.chart.layout
111
- const lanes = (l?.lanes ?? 0) + (this.view?.repos.length ?? 0)
88
+ const lanes = l?.lanes ?? 0
112
89
  this.held = settleHeight(this.held, Math.min(OPEN_MAX_H, chartHeight(lanes)), now)
113
90
  }
114
91
 
92
+ /** a resume keeps the chart where it was: only the first start homes it */
115
93
  start(): void {
116
94
  if (this.timer != null) return
117
- this.opened()
95
+ if (!this.started) this.opened()
96
+ this.started = true
118
97
  void this.poll()
119
98
  this.timer = setInterval(() => void this.poll(), LIVE_POLL_MS)
120
99
  }
121
- dispose(): void {
100
+ stop(): void {
122
101
  if (this.timer != null) clearInterval(this.timer)
123
102
  this.timer = null
124
103
  }
@@ -128,7 +107,7 @@ export class LiveSvc {
128
107
  if (this.inFlight) return
129
108
  this.inFlight = true
130
109
  try {
131
- const r = await fetch('/api/live.json')
110
+ const r = await fetch(this.st.url('/api/live.json'))
132
111
  if (!r.ok) throw new Error(await r.text())
133
112
  const v = (await r.json()) as LiveView
134
113
  const ended = this.running && v.runs.length === 0
@@ -136,14 +115,14 @@ export class LiveSvc {
136
115
  this.view = v
137
116
  this.receivedAt = Date.now()
138
117
  this.error = ''
139
- if (this.open) this.settle(Date.now())
118
+ this.settle(Date.now())
140
119
  })
141
120
  // a run that ended, from a terminal as much as from a button,
142
121
  // wrote new numbers: the rows and the timeline read them now
143
122
  // rather than on the next ten second poll
144
123
  if (ended) {
145
- void this.st.cols.poll()
146
- if (this.st.timeline.data != null) void this.st.timeline.load()
124
+ void this.st.cols.poll(true)
125
+ if (this.st.timeline.data != null) void this.st.timeline.load(true)
147
126
  }
148
127
  } catch (e) {
149
128
  runInAction(() => { this.error = `the live runs could not be read: ${(e as Error).message}` })
@@ -155,20 +134,4 @@ export class LiveSvc {
155
134
  get running(): boolean {
156
135
  return (this.view?.runs.length ?? 0) > 0
157
136
  }
158
-
159
- /** a repo lane was clicked: its hub, when it serves */
160
- async openRepo(repo: string): Promise<void> {
161
- try {
162
- const r = (await (await fetch(`/api/repo-hub?repo=${encodeURIComponent(repo)}`)).json()) as { url: string | null }
163
- if (r.url != null) window.open(r.url, '_blank')
164
- else this.say(`${repo} has no hub serving`)
165
- } catch (e) {
166
- this.say(`${repo}: ${(e as Error).message}`)
167
- }
168
- }
169
-
170
- private say(text: string): void {
171
- runInAction(() => { this.note = text })
172
- setTimeout(() => runInAction(() => { if (this.note === text) this.note = '' }), 4_000)
173
- }
174
137
  }