rvlib-shipkit 1.4.0 → 1.6.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 (199) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/assets/gen/comfyts-gen.ts +12 -8
  5. package/src/assets/server/studio-server.ts +14 -2
  6. package/src/check.ts +73 -103
  7. package/src/checks/SK001-package-json-shape.check.ts +11 -6
  8. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  9. package/src/checks/SK003-claude-md-shape.check.ts +7 -3
  10. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  11. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  12. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  13. package/src/checks/SK007-github-visibility.check.ts +4 -2
  14. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  15. package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
  16. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  17. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  18. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  19. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  20. package/src/checks/SK014-prose-ste.check.ts +4 -0
  21. package/src/checks/SK015-nested-repo.check.ts +4 -2
  22. package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
  23. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  24. package/src/checks/SK018-test-files-run.check.ts +6 -2
  25. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  26. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  27. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  28. package/src/checks/SK024-identity-colors.check.ts +5 -3
  29. package/src/checks/SK025-release-drift.check.ts +4 -5
  30. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  31. package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
  32. package/src/checks/SK028-readme.check.ts +5 -1
  33. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  34. package/src/checks/SK030-cache-audit.check.ts +34 -0
  35. package/src/checks/SK031-trace-audit.check.ts +39 -0
  36. package/src/checks/SK032-ignored-inputs.check.ts +79 -0
  37. package/src/cli/cmd_Asks.ts +5 -5
  38. package/src/cli/cmd_Assets.ts +17 -13
  39. package/src/cli/cmd_Bump.ts +12 -11
  40. package/src/cli/cmd_CacheAudit.ts +43 -0
  41. package/src/cli/cmd_Check.ts +8 -7
  42. package/src/cli/cmd_Checks.ts +7 -6
  43. package/src/cli/cmd_Ci.ts +12 -11
  44. package/src/cli/cmd_Commands.ts +16 -13
  45. package/src/cli/cmd_ConfigKeys.ts +7 -6
  46. package/src/cli/cmd_Contracts.ts +3 -3
  47. package/src/cli/cmd_CrossRepoActivity.ts +8 -6
  48. package/src/cli/cmd_Fleet.ts +9 -7
  49. package/src/cli/cmd_GlobalProcesses.ts +120 -7
  50. package/src/cli/cmd_Home.ts +26 -19
  51. package/src/cli/cmd_Hooks.ts +6 -5
  52. package/src/cli/cmd_Init.ts +5 -4
  53. package/src/cli/cmd_Keys.ts +20 -8
  54. package/src/cli/cmd_Mv.ts +7 -6
  55. package/src/cli/cmd_Needs.ts +4 -3
  56. package/src/cli/cmd_Overview.ts +5 -4
  57. package/src/cli/cmd_Pb.ts +2 -2
  58. package/src/cli/cmd_Planned.ts +6 -5
  59. package/src/cli/cmd_Prose.ts +5 -5
  60. package/src/cli/cmd_Reflect.ts +7 -7
  61. package/src/cli/cmd_Release.ts +6 -5
  62. package/src/cli/cmd_Releases.ts +5 -4
  63. package/src/cli/cmd_Serve.ts +6 -4
  64. package/src/cli/cmd_Smoke.ts +5 -4
  65. package/src/cli/cmd_Start.ts +7 -6
  66. package/src/cli/cmd_Status.ts +4 -3
  67. package/src/cli/cmd_TraceAudit.ts +62 -0
  68. package/src/cli/cmd_Tui.ts +3 -2
  69. package/src/cli/cmd_When.ts +71 -0
  70. package/src/cli/cmd_Wiki.ts +4 -4
  71. package/src/cli.ts +11 -2
  72. package/src/collections/checks.ts +2 -0
  73. package/src/collections/collection-types.ts +12 -4
  74. package/src/collections/providers.ts +5 -3
  75. package/src/core/app-icon.ts +154 -0
  76. package/src/core/audit-report.ts +21 -0
  77. package/src/core/cache-audit.ts +72 -0
  78. package/src/core/canonical-scripts.ts +2 -2
  79. package/src/core/check-ctx.ts +14 -5
  80. package/src/core/check-runner.ts +43 -23
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +23 -2
  83. package/src/core/floor-resolve.ts +5 -7
  84. package/src/core/git-root.ts +13 -0
  85. package/src/core/ignored-block.ts +94 -0
  86. package/src/core/ignored-inputs.ts +47 -0
  87. package/src/core/jobs.ts +9 -2
  88. package/src/core/legacy-shapes.ts +39 -10
  89. package/src/core/one-lib.ts +59 -4
  90. package/src/core/one-way-in-policy.ts +2 -2
  91. package/src/core/outcome-words.ts +18 -2
  92. package/src/core/port-check-cli.ts +52 -0
  93. package/src/core/port-check-file.ts +295 -0
  94. package/src/core/port-check-files.ts +31 -0
  95. package/src/core/read-set.ts +97 -9
  96. package/src/core/retention.ts +0 -14
  97. package/src/core/run-check.ts +12 -9
  98. package/src/core/run-ci.ts +14 -5
  99. package/src/core/run-fix.ts +30 -23
  100. package/src/core/run-fixers.ts +48 -5
  101. package/src/core/runs-matrix.ts +179 -0
  102. package/src/core/runs.ts +242 -0
  103. package/src/core/trace-audit.ts +120 -0
  104. package/src/core/update-policy.ts +20 -0
  105. package/src/core/when-block.ts +29 -30
  106. package/src/core/when.ts +41 -103
  107. package/src/core/wire.ts +10 -14
  108. package/src/fleet/scan-fleet.ts +20 -4
  109. package/src/fleet/update-fleet.ts +6 -6
  110. package/src/home/gui-app.ts +24 -27
  111. package/src/home/home-launchd.ts +20 -4
  112. package/src/home/home-routes.ts +12 -1
  113. package/src/home/home-start.ts +11 -5
  114. package/src/home/hubs.ts +5 -5
  115. package/src/home/open-app.ts +2 -2
  116. package/src/home/pm.ts +8 -0
  117. package/src/home/run-pm.ts +1 -0
  118. package/src/home/serve-web.ts +2 -2
  119. package/src/home/supervise.ts +24 -1
  120. package/src/home/via-hub.ts +14 -0
  121. package/src/hooks/run-hooks.ts +48 -4
  122. package/src/init-repo.ts +6 -6
  123. package/src/ledger/db.ts +2 -13
  124. package/src/ledger/gate-checks.ts +133 -61
  125. package/src/ledger/git-key.ts +153 -31
  126. package/src/ledger/git-scan.ts +5 -2
  127. package/src/ledger/ledger.ts +136 -21
  128. package/src/ledger/store.ts +14 -0
  129. package/src/ledger/toolchain-identity.ts +248 -45
  130. package/src/plan/repo-source.ts +18 -39
  131. package/src/problems/extract.ts +3 -0
  132. package/src/release/run-release.ts +5 -0
  133. package/src/repo-config.ts +46 -54
  134. package/src/rules/rule.ts +2 -2
  135. package/src/scaffold/files/agent-coding.md +1 -1
  136. package/src/scaffold/files/agent-index.md +1 -1
  137. package/src/scaffold/files/verify-skill.md +2 -2
  138. package/src/serve/hub-app/activity-st.ts +5 -4
  139. package/src/serve/hub-app/activity-view.tsx +1 -1
  140. package/src/serve/hub-app/admin-svc.ts +36 -3
  141. package/src/serve/hub-app/admin-view.tsx +46 -12
  142. package/src/serve/hub-app/check-view.tsx +66 -46
  143. package/src/serve/hub-app/check-words.ts +1 -11
  144. package/src/serve/hub-app/checks-svc.ts +14 -35
  145. package/src/serve/hub-app/col-card.tsx +1 -44
  146. package/src/serve/hub-app/col-tables-view.tsx +84 -11
  147. package/src/serve/hub-app/col-tables.ts +27 -0
  148. package/src/serve/hub-app/cols-svc.ts +5 -4
  149. package/src/serve/hub-app/dock-st.ts +3 -2
  150. package/src/serve/hub-app/dock-view.tsx +36 -0
  151. package/src/serve/hub-app/dock.css +6 -0
  152. package/src/serve/hub-app/float-cards.tsx +30 -0
  153. package/src/serve/hub-app/floating.tsx +41 -4
  154. package/src/serve/hub-app/git-st.ts +42 -0
  155. package/src/serve/hub-app/git-view.tsx +61 -0
  156. package/src/serve/hub-app/hash-route.ts +45 -1
  157. package/src/serve/hub-app/header-bar.tsx +62 -72
  158. package/src/serve/hub-app/health-svc.ts +3 -2
  159. package/src/serve/hub-app/home-st.ts +14 -2
  160. package/src/serve/hub-app/home-strip.css +43 -12
  161. package/src/serve/hub-app/home-strip.tsx +115 -64
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +32 -8
  164. package/src/serve/hub-app/hub-types.ts +5 -2
  165. package/src/serve/hub-app/hub.css +89 -63
  166. package/src/serve/hub-app/icons.tsx +11 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +15 -4
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +18 -22
  171. package/src/serve/hub-app/plan-view.tsx +51 -0
  172. package/src/serve/hub-app/planned-st.ts +9 -4
  173. package/src/serve/hub-app/processes-view.tsx +45 -0
  174. package/src/serve/hub-app/project-list.css +4 -1
  175. package/src/serve/hub-app/project-list.tsx +6 -3
  176. package/src/serve/hub-app/projects-svc.ts +25 -10
  177. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  178. package/src/serve/hub-app/reach.ts +36 -0
  179. package/src/serve/hub-app/readme-svc.ts +3 -2
  180. package/src/serve/hub-app/repo-sections.ts +131 -0
  181. package/src/serve/hub-app/repo-st.ts +70 -14
  182. package/src/serve/hub-app/sidebar.tsx +17 -42
  183. package/src/serve/hub-app/timeline-svc.ts +3 -2
  184. package/src/serve/hub-app/tree-build.ts +151 -155
  185. package/src/serve/hub-app/tree-row.tsx +55 -12
  186. package/src/serve/hub-app/tree-svc.ts +97 -56
  187. package/src/serve/hub-app/when-view.tsx +89 -0
  188. package/src/serve/hub-app/when.css +17 -0
  189. package/src/serve/hub-runtime.ts +7 -18
  190. package/src/serve/project-gutter-route.ts +22 -1
  191. package/src/serve/run-serve.ts +1 -1
  192. package/src/serve/serve-hub.ts +4 -14
  193. package/src/suite/judge-suites.ts +6 -0
  194. package/src/trace/interpose.c +655 -0
  195. package/src/trace/read-trace.ts +485 -0
  196. package/src/trace/tracer.ts +282 -0
  197. package/src/serve/hub-app/processes-svc.ts +0 -63
  198. package/src/serve/hub-app/repo-sections.tsx +0 -61
  199. package/src/serve/repo-processes.ts +0 -89
@@ -38,7 +38,6 @@ a,a:visited{color:var(--accent)}
38
38
  .pjpanel,.appcol,.pside,.pmain{height:100%;min-width:0}
39
39
  .apptop,.pdock{height:100%;min-height:0}
40
40
  .pjpanel{display:flex}
41
- /* the box the machine toasts float in (home-strip.css .htoasts) */
42
41
  .appcol{position:relative;display:flex;flex-direction:column}
43
42
  /* one region per repo visited, the shown one alone displayed */
44
43
  .repo{flex:1;min-height:0;display:flex;flex-direction:column}
@@ -50,10 +49,9 @@ a,a:visited{color:var(--accent)}
50
49
  .pmain > main{flex:1}
51
50
 
52
51
  /* ── a repo's bar: on the home the strip (home-strip.css .hstrip), on a hub
53
- of its own the header (.hdr). Its name, what runs now and the two runs,
54
- then its processes, its trouble and its editor icons. One line at any
55
- width: the container queries shrink the labels to their icons, then the
56
- run state to its spinner ── */
52
+ of its own the header (.hdr). Its name, its check… button and a spinner
53
+ while a run goes, then its processes and its editor icons, on one line.
54
+ Its trouble floats under it (home-strip.css .htoasts) ── */
57
55
  /* the repo's processes (serve.processes): a dot, its id, a link once up */
58
56
  .hprocs{display:inline-flex;gap:var(--s1);align-items:center}
59
57
  .hproc{display:inline-flex;align-items:center;gap:var(--s0)}
@@ -64,7 +62,7 @@ a,a:visited{color:var(--accent)}
64
62
  .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)}
65
63
  .hdr .sp{flex:1}
66
64
  .hname{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);font-weight:600;margin-right:var(--s2);white-space:nowrap}
67
- .hdr,.hstrip{container-type:inline-size;white-space:nowrap}
65
+ .hdr,.hstrip{white-space:nowrap}
68
66
  .hrun{display:inline-flex;align-items:center;gap:var(--s1);flex:none;margin-left:var(--s2)}
69
67
  .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}
70
68
  .hbtn .hico{width:13px;height:13px}
@@ -72,40 +70,37 @@ a,a:visited{color:var(--accent)}
72
70
  .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)}
73
71
  .hicon:hover{color:var(--accent)}
74
72
  .hicon .hico{width:15px;height:15px}
75
- /* the run state holds its width whatever it says, so a run never moves the buttons beside it */
76
- .hstatus{max-width:420px;flex:0 1 auto;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
77
- .hstatus.red{color:var(--red)}
78
- .hstatus.warn{color:var(--warn)}
79
- /* its words sit against the run buttons, cut with an ellipsis */
80
- .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}
81
- .hstate.on{color:var(--ink)}
82
- .hstate .htext{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
83
- .hstate .spin.idle{visibility:hidden;animation:none}
84
- /* after .hstate, so a narrow bar wins over its width */
85
- @container (max-width:600px){.hbtn .lbl{display:none}.hbtn{padding:0 var(--s1)}}
86
- @container (max-width:520px){.hstate{width:120px;min-width:120px}}
87
- @container (max-width:420px){.hstate .htext{display:none}.hstate{width:16px;min-width:16px}}
88
73
 
89
74
  /* a row of a (?) legend: one glyph or word, then its meaning */
90
75
  .lrow{display:flex;gap:var(--s2);align-items:center;height:29px;flex:none;padding:0 var(--s3)}
91
76
 
92
- /* ── the left pane: the sections, the filter, then the tree, the one part
93
- that scrolls ── */
77
+ /* ── the left pane: the filter, then the tree, the one part that
78
+ scrolls ── */
94
79
  aside{border-right:1px solid var(--line);background:var(--panel);min-height:0;display:flex;flex-direction:column;overflow:hidden}
95
80
  aside > *{flex:none}
96
- aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
97
- /* a repo's sections: the project list's lines (project-list.css .pjpl), a
98
- little shorter, the current one lit and marked in the accent */
99
- .rsecs{display:flex;flex-direction:column;padding:var(--s1) 0;border-bottom:1px solid var(--line)}
100
- .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}
101
- .rsec:hover{background:var(--panel2);color:var(--ink);border-color:transparent}
102
- .rsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
103
- .rsec .hico{flex:none;width:16px;height:16px}
104
- .rsecl{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
105
- .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)}
81
+ /* the tree is long, so it keeps a scroll indicator, a thin one with no track:
82
+ WebKit (the macOS app) ignores scrollbar-width and draws a wide dark band */
83
+ aside > .tscroll{flex:1;min-height:0;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--line) transparent}
84
+ aside > .tscroll::-webkit-scrollbar{width:6px}
85
+ aside > .tscroll::-webkit-scrollbar-track{background:transparent}
86
+ aside > .tscroll::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}
87
+ /* a section, the tree's top row: the project list's lines (project-list.css
88
+ .pjpl), a little shorter, the current one lit and marked in the accent.
89
+ The chevron column is the tree's own, so a section's rows line up under it */
90
+ .tsec{display:grid;grid-template-columns:14px 16px minmax(0,1fr) auto auto;align-items:center;gap:var(--s2);height:26px;padding:0 var(--s1) 0 var(--s2);color:var(--dim);font-size:13px;cursor:pointer;white-space:nowrap}
91
+ .tsec:hover{background:var(--panel2);color:var(--ink)}
92
+ .tsec.empty{opacity:.4}
93
+ .tsec.noop{cursor:default}
94
+ .tsec.noop:hover{background:none;color:var(--dim)}
95
+ .tsec.on{box-shadow:inset 2px 0 0 var(--accent)}
96
+ .tsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
97
+ .tsec .hico{width:16px;height:16px}
98
+ .tsec:hover .tw{color:var(--ink)}
99
+ .rsecl{min-width:0;overflow:hidden;text-overflow:ellipsis}
100
+ .rsecn{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)}
106
101
  .rsecn.nred{background:var(--red)}
107
102
  .rsecn.nwarn{background:var(--warn)}
108
- .rsec:hover .pjkey,.rsec.cur .pjkey{opacity:1}
103
+ .tsec:hover .pjkey,.tsec.cur .pjkey{opacity:1}
109
104
  /* segmented severity filter — all · needs · red. A thin border and an
110
105
  underline on the live one: no fill, no pill */
111
106
  .seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden;flex:none}
@@ -115,9 +110,8 @@ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
115
110
  .seg button:hover{color:var(--ink)}
116
111
  .seg button.on{color:var(--accent);border-bottom-color:var(--accent)}
117
112
  .seg button .sk{margin-left:var(--s1);font-size:9px;opacity:.55}
118
- .swhen{padding:0 var(--s3) var(--s2);font-size:11px;color:var(--dim)}
119
113
  /* a narrow pane puts the severity filter under the input, never over it */
120
- .ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3)}
114
+ .ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3);border-bottom:1px solid var(--line)}
121
115
  .sfield{position:relative;flex:1 1 96px;min-width:0;display:flex}
122
116
  .ssearch input{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:4px;color:var(--ink);
123
117
  padding:var(--s0) var(--s4) var(--s0) var(--s2);font-size:12px;outline:none}
@@ -133,8 +127,8 @@ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
133
127
  no prose, no dash separators ── */
134
128
  .tree{padding:var(--s1) 0 var(--s4);outline:none}
135
129
  .tree:focus-visible{box-shadow:inset 2px 0 0 var(--accent)}
136
- .trow{display:grid;grid-template-columns:calc(var(--d,0)*12px + 18px) 14px minmax(0,1fr) 58px 46px 14px;
137
- align-items:center;gap:var(--s1);padding:0 var(--s2);height:20px;line-height:20px;cursor:pointer;white-space:nowrap}
130
+ .trow{display:grid;grid-template-columns:calc(var(--d,0)*10px + 14px) 14px minmax(0,1fr) 12px auto 14px;
131
+ align-items:center;gap:var(--s1);padding:0 var(--s1) 0 var(--s2);height:20px;line-height:20px;cursor:pointer;white-space:nowrap}
138
132
  .trow:hover{background:var(--panel2)}
139
133
  .trow.on{background:var(--sel);box-shadow:inset 2px 0 0 var(--accent)}
140
134
  /* the fold marker: an 11px chevron with the whole first column as its hit
@@ -168,21 +162,20 @@ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
168
162
  .tg.ok{color:var(--ok)}
169
163
  .tg.adv{color:var(--warn);opacity:.75}
170
164
  .tg.run{color:var(--accent);animation:pulse 1.1s ease-in-out infinite}
171
- /* right aligned, tabular figures, ONE unit (ms) and one tone table, so a
172
- column of durations compares by eye (hub-app/dur.ts owns both) */
173
- .tdur{font:10.5px ui-monospace,Menlo,monospace;font-variant-numeric:tabular-nums;text-align:right;color:var(--dim);overflow:hidden}
165
+ /* the duration: a clock tinted by the tone table (hub-app/dur.ts), the
166
+ figure in its tooltip, so the label keeps the width */
167
+ .tdur{display:flex;align-items:center;justify-content:center;color:var(--dim)}
168
+ .tdur .hico{width:11px;height:11px;color:inherit}
174
169
  .tdur.fast,.dnum.fast{color:var(--ok)}
175
170
  .tdur.plain,.dnum.plain{color:var(--ink)}
176
171
  .tdur.slow,.dnum.slow{color:var(--warn)}
177
172
  .tdur.bad,.dnum.bad{color:var(--red)}
178
173
  .tnote{font-size:10px;text-align:right;color:var(--dim);overflow:hidden;text-overflow:ellipsis}
179
174
  .tnote b{font-weight:600;color:var(--red)}
180
- .stabs{display:flex;gap:var(--s2);align-items:center;padding:var(--s1) var(--s3);border-bottom:1px solid var(--line)}
181
- .stabs .sp{flex:1}
182
- .stabs .cnt{min-width:4ch;text-align:right;font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums}
183
175
  .catgen{display:inline-flex;gap:var(--s1)}
184
176
  .catgen button{padding:var(--s0) var(--s2);font-size:11px;font-weight:700}
185
177
  .tempty{padding:var(--s2) var(--s3);font-size:12px;color:var(--dim);white-space:pre-wrap}
178
+ .thidden{cursor:pointer}.thidden:hover{color:var(--ink)}
186
179
  .twait{display:flex;align-items:center;gap:var(--s2)}
187
180
  .warnline{color:var(--red)}
188
181
  .fold{cursor:pointer;color:var(--dim);font-size:15px;display:inline-block;width:22px;text-align:center;
@@ -369,7 +362,15 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
369
362
  .assetrem .rem{color:var(--ok);border-left:2px solid var(--ok);padding-left:var(--s2);font-size:12.5px;margin:0 0 var(--s2);white-space:pre-wrap}
370
363
 
371
364
  /* ── collection item card + status card ── */
372
- .colcard{max-width:760px;width:100%;background:var(--panel);border:1px solid var(--line);border-radius:3px;padding:var(--s4);text-align:left}
365
+ /* a repo page sits on the page background, never in a box of its own: its
366
+ header and its sentence are unboxed, and each group it holds (the facts,
367
+ a titled section, the command, the admin rows, the runs list) is one
368
+ lighter box, so a box always means "these belong together" */
369
+ .colcard{max-width:760px;width:100%;padding:var(--s2) 0;text-align:left}
370
+ .colcard>.cfacts,.colcard>.csec,.colcard>.ccmd,.colcard>.md:not(.usays),.colcard>.adbox,.tlcard>.rlist{background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:var(--s2) var(--s3)}
371
+ .colcard>.cfacts,.colcard>.ccmd,.colcard>.md:not(.usays),.colcard>.adbox{margin-top:var(--s3)}
372
+ .ccmd>.cf:first-child,.cfacts>.cf:nth-child(-n+2){border-top:0}
373
+ @media (max-width:900px){.cfacts>.cf:nth-child(2){border-top:1px solid var(--line)}}
373
374
  .colcard h3{margin:0 0 var(--s2);font-size:15px;display:flex;gap:var(--s2);align-items:baseline}
374
375
  .colcard h3 .sp{flex:1}
375
376
  .colcard .truth{margin-left:auto;font:11px ui-monospace,Menlo,monospace;color:var(--dim)}
@@ -560,6 +561,15 @@ a.tok{color:var(--accent)}
560
561
  .checkcard .dim{color:var(--dim)}
561
562
  /* what is not a fact: the output and the slow suites, titled, unboxed */
562
563
  .csec{margin-top:var(--s4)}
564
+ /* where it runs: one line per context, the value then its meaning. A context
565
+ that never runs the check stays dim, so the lit lines read down the list */
566
+ .cruns{display:grid;grid-template-columns:auto auto minmax(0,1fr);column-gap:var(--s3);row-gap:var(--s0);align-items:baseline}
567
+ .crun{display:contents;color:var(--dim)}
568
+ .crv{font:11px ui-monospace,Menlo,monospace;white-space:nowrap}
569
+ .crun.on .crv{color:var(--ok)}
570
+ .crun.cache .crv{color:var(--ink)}
571
+ .crm{font-size:11.5px;color:var(--dim);opacity:.8}
572
+ .crun:not(.on) .crm{opacity:.5}
563
573
  .cstitle{display:flex;gap:var(--s1);align-items:center;font-size:11px;text-transform:uppercase;letter-spacing:.08em;
564
574
  color:var(--dim);margin-bottom:var(--s2)}
565
575
  .wline{margin-bottom:var(--s1);word-break:break-word}
@@ -624,18 +634,21 @@ a.tok{color:var(--accent)}
624
634
  padding:var(--s1) var(--s2);font-size:12.5px;outline:none}
625
635
  .wform input:focus{border-color:var(--accent)}
626
636
 
627
- /* ── collection view (one card per instance) ── */
628
- .colgrid{flex:1;min-height:0;overflow-y:auto;padding:var(--s3) var(--s4)}
629
- .cghead{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
637
+ /* ── a collection's page: its title and count, then its table ── */
638
+ .cghead{font-size:15px;font-weight:600;color:var(--ink);
630
639
  display:flex;gap:var(--s2);align-items:baseline;margin:0 0 var(--s3)}
631
- .cghead .cnt{font-size:11px;color:var(--dim);letter-spacing:0;text-transform:none}
632
- /* the checks and asks pages: one dense table each (col-tables-view.tsx), on
640
+ .ctlead{font-size:11px;color:var(--dim);margin:calc(-1 * var(--s1)) 0 var(--s2)}
641
+ .cghead .cnt{font-size:12px;font-weight:400;color:var(--dim)}
642
+ /* the checks, asks and secrets pages: one dense table each (col-tables-view.tsx), on
633
643
  the activity table (.actable), a group header per state that folds */
634
644
  .ctpage{flex:1;min-height:0;overflow-y:auto;padding:var(--s3) var(--s4);font-size:12.5px;container-type:inline-size}
645
+ /* the table sits on the panel surface, the way every card section does, so
646
+ its lines read against something lighter than the page */
647
+ .ctbox{background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:var(--s1) 0}
635
648
  .cttable{font-variant-numeric:tabular-nums}
636
649
  .cttable td{padding:var(--s0) var(--s2);vertical-align:baseline}
637
- .ctgrp td{padding:var(--s2) var(--s2) var(--s1);color:var(--dim);font-size:11.5px;border-bottom:1px solid var(--line);cursor:pointer;user-select:none}
638
- .ctgrp:hover td{color:var(--ink)}
650
+ .ctgrp td{padding:var(--s2) var(--s2) var(--s1);color:var(--ink);font-size:12.5px;font-weight:600;border-bottom:1px solid var(--line);cursor:pointer;user-select:none}
651
+ .ctgrp:hover td{color:var(--accent)}
639
652
  .ctgrp.g-fail td{color:var(--red)}
640
653
  .ctgrp.g-warn td{color:var(--warn)}
641
654
  .ctcaret{display:inline-block;width:14px}
@@ -648,17 +661,8 @@ a.tok{color:var(--accent)}
648
661
  .ctbudget,.ctplan,.ctblocks{color:var(--dim)}
649
662
  .ctprobe{font-size:11.5px}
650
663
  .cttable .c-dot{width:22px}.cttable .c-id{width:140px}.cttable .c-title{width:16%}.cttable .c-age{width:72px}.cttable .c-dur{width:152px}.cttable .c-plan{width:88px}
651
- .cttable .c-kind{width:92px}.cttable .c-blocks{width:32%}.cttable .c-probe{width:84px}
664
+ .cttable .c-name{width:30%}.cttable .c-file{width:34%}.cttable .c-kind{width:92px}.cttable .c-blocks{width:32%}.cttable .c-probe{width:84px}
652
665
  @container (max-width:760px){.cttable .c-title{width:0}.cttable .cttitle{display:none}.cttable .c-plan{width:0}.cttable .ctplan{display:none}}
653
- .cgcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:var(--s3)}
654
- .cgcard{background:var(--panel);border:1px solid var(--line);border-radius:5px;padding:var(--s2) var(--s3);cursor:pointer;min-width:0}
655
- .cgcard:hover{border-color:var(--accent)}
656
- .cgcard .cgtop{display:flex;gap:var(--s2);align-items:center;min-width:0}
657
- .cgcard .cgtop b{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
658
- .cgcard .truth{display:block;font:11px ui-monospace,Menlo,monospace;color:var(--dim);margin-top:var(--s1);
659
- white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
660
- .cgcard .cgrem{color:var(--ok);border-left:2px solid var(--ok);padding-left:var(--s2);font-size:12px;margin-top:var(--s1);
661
- white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
662
666
 
663
667
  /* ── graph item view (the graph itself fills the stage) ── */
664
668
  .graphview{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3) var(--s4) var(--s4)}
@@ -701,13 +705,10 @@ a.tok{color:var(--accent)}
701
705
  overflow-x:auto;font:11px/1.5 ui-monospace,Menlo,monospace;white-space:pre}
702
706
  .md pre code{background:none;border:0;padding:0}
703
707
  .md a{color:var(--accent)}
704
- .md.clamp{max-height:180px;overflow:hidden;margin-top:var(--s1);
705
- -webkit-mask-image:linear-gradient(var(--solid) 65%,transparent);mask-image:linear-gradient(var(--solid) 65%,transparent)}
706
708
 
707
709
  /* ── secret (cred) cards ── */
708
710
  .credname{font:600 13px ui-monospace,Menlo,monospace}
709
711
  .creddesc{font-size:12px;color:var(--dim);margin-top:var(--s1)}
710
- .credrow{display:flex;gap:var(--s2);align-items:center;margin-top:var(--s2)}
711
712
  .stchip{font-size:10px;border-radius:5px;padding:var(--s0) var(--s2);letter-spacing:.08em;text-transform:uppercase;
712
713
  border:1px solid;flex:none;font-weight:600}
713
714
  .stchip.ok{color:var(--ok);border-color:var(--ok)}
@@ -745,9 +746,34 @@ pre{background:var(--panel);border:1px solid var(--line);border-radius:3px;paddi
745
746
 
746
747
  /* ── the admin page: one row per control, its sentence under its name ── */
747
748
  .adrow{display:flex;align-items:center;gap:var(--s4);padding:var(--s3) 0;border-top:1px solid var(--line)}
749
+ .adrow:first-child{border-top:0}
748
750
  .adtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--s0);font-size:13px}
749
751
  .adtext .dim{font-size:12px}
750
752
  .adctl{flex:none;display:flex;align-items:center}
751
753
  .adflag{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px;color:var(--dim)}
752
754
  .adflag input{width:auto;margin:0}
755
+ .adid{display:flex;align-items:flex-start;gap:var(--s4)}
756
+ .adicon{flex:none;width:128px;display:flex;flex-direction:column;gap:var(--s1);font-size:12px}
757
+ .adicon img{display:block;width:128px;height:128px}
758
+ .adicon img[hidden]{display:none}
759
+ .adpick{display:flex;align-items:center;gap:var(--s2);margin-top:var(--s1);font-size:12px;cursor:pointer}
760
+ .colcard .adpick input{flex:none;min-width:0;width:28px;height:28px;padding:0;margin:0;border:1px solid var(--line);border-radius:4px;background:none;cursor:pointer}
761
+ .adpick input::-webkit-color-swatch-wrapper{padding:0}
762
+ .adpick input::-webkit-color-swatch{border:none;border-radius:3px}
763
+ .adsample{padding:var(--s0) var(--s2);border-radius:4px;font-weight:600;font-size:13px}
764
+ .adsample.dark{background:var(--bg)}
765
+ .adsample.light{background:var(--tag)}
753
766
  .adconfirm{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px}
767
+ /* a repo's plan page: one line per planned check */
768
+ .plntable{width:100%;border-collapse:collapse;font-size:12.5px}
769
+ .plntable td{padding:var(--s1) var(--s2);border-top:1px solid var(--line);vertical-align:baseline}
770
+ .plntable tr:first-child td{border-top:0}
771
+ .plnwhen{white-space:nowrap;font-variant-numeric:tabular-nums}
772
+ .plnrun{text-align:right;white-space:nowrap}
773
+ /* a repo's git page */
774
+ .gitfiles,.gitcommits{width:100%;border-collapse:collapse;font-size:12.5px}
775
+ .gitfiles td,.gitcommits td{padding:2px var(--s2);vertical-align:baseline}
776
+ .gitkind{width:1%;white-space:nowrap;color:var(--dim)}
777
+ .gitkind.staged{color:var(--ok)}
778
+ .gitkind.untracked{color:var(--warn)}
779
+ .gitpath{font:12px ui-monospace,Menlo,monospace;overflow-wrap:anywhere}
@@ -5,6 +5,11 @@
5
5
  export const ICON = {
6
6
  shipkit: 'M3 7.5 10 4l7 3.5v5L10 16l-7-3.5zM3 7.5 10 11l7-3.5M10 11v5',
7
7
  process: 'M2 10h3.5l2-5 5 10 2-5H18',
8
+ processes: 'M2 10h3.5l2-5 5 10 2-5H18',
9
+ when: 'M10 3a7 7 0 1 0 .01 0M10 6v4l2.5 2',
10
+ plan: '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',
11
+ git: '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',
12
+ reflections: 'M4 3.5h12v9H9l-4 3.5v-3.5H4zM7.5 7h5M7.5 9.5h3',
8
13
  monitor: 'M3 4h14v9H3zM7 16.5h6M10 13v3.5',
9
14
  archived: 'M2.5 4h15v3.5h-15zM4 7.5V16h12V7.5M8 10.5h4',
10
15
  home: 'M3 9.5 10 3.5l7 6M5 8v8.5h10V8M8.5 16.5v-4.5h3v4.5',
@@ -15,7 +20,11 @@ export const ICON = {
15
20
  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
21
  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
22
  // a repo page's sections, then its two runs and its editor
23
+ readme: 'M5 2.5h7l3 3v12H5zM12 2.5v3h3M7.5 9h5M7.5 12h5M7.5 15h3',
18
24
  checks: 'M4 10.5 7.5 14 16 5.5',
25
+ contracts: 'M5 2.5h10v15H5zM7.5 6h5M7.5 9h5M7.5 12h2.5',
26
+ repo: 'M2.5 5h5l1.5 1.5h8.5v9.5h-15z',
27
+ secrets: 'M7 7a3 3 0 1 0 0 6a3 3 0 1 0 0-6M10 10h7.5M15 10v2.5M17.5 10v2',
19
28
  runs: 'M3 5h8M3 10h12M3 15h6',
20
29
  asks: 'M3 4h14v9H8l-4 3.5V13H3z',
21
30
  tests: 'M8 3h4M8.5 3v5L4 16.5h12L11.5 8V3M6.2 12.5h7.6',
@@ -30,6 +39,8 @@ export const ICON = {
30
39
  // a process's own switches on the overview
31
40
  play: 'M6.5 4.5v11l9-5.5z',
32
41
  stop: 'M5.5 5.5h9v9h-9z',
42
+ link: 'M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1',
43
+ clock: 'M10 2.5a7.5 7.5 0 1 0 0 15a7.5 7.5 0 1 0 0-15M10 6v4l2.5 2',
33
44
  restart: 'M15.5 10a5.5 5.5 0 1 1-1.6-3.9M14.2 3v3.3h-3.3',
34
45
  } as const
35
46
 
@@ -9,6 +9,7 @@
9
9
  <link rel="stylesheet" href="./prose-hits.css" />
10
10
  <link rel="stylesheet" href="./home-strip.css" />
11
11
  <link rel="stylesheet" href="./planned.css" />
12
+ <link rel="stylesheet" href="./when.css" />
12
13
  <link rel="stylesheet" href="./activity.css" />
13
14
  <link rel="stylesheet" href="./readme.css" />
14
15
  <link rel="stylesheet" href="./dock.css" />
@@ -1,12 +1,12 @@
1
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
2
+ // nth repo of the project list, ⌘↑ and ⌘↓ the repo above or below, then the home row under the right hand: ⌘J
3
3
  // the picker, ⌘K the overview, ⌘L the activity, ⌘' the plan. ⌘B cycles the
4
4
  // dock at the bottom of the page (dock-st.ts): the line, slim, open.
5
5
  // A digit and the quote are read from the physical key (`code`), so an
6
6
  // AZERTY keyboard, where the key under 1 types `&`, selects the same repo. Ctrl stands in for
7
7
  // ⌘ off a Mac. The macOS app binds none of these to a menu item, so they
8
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' }
9
+ export type PageKey = { t: 'repo'; n: number } | { t: 'step'; by: -1 | 1 } | { t: 'home' } | { t: 'planned' } | { t: 'activity' } | { t: 'picker' } | { t: 'dock' }
10
10
 
11
11
  type KeyLike = { key: string; code: string; metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; altKey: boolean }
12
12
 
@@ -15,6 +15,8 @@ export function pageKey(e: KeyLike): PageKey | null {
15
15
  const digit = /^Digit([1-9])$/.exec(e.code)
16
16
  if (digit != null) return { t: 'repo', n: Number(digit[1]) }
17
17
  if (e.shiftKey) return null
18
+ if (e.key === 'ArrowUp') return { t: 'step', by: -1 }
19
+ if (e.key === 'ArrowDown') return { t: 'step', by: 1 }
18
20
  if (e.code === 'Quote' || e.key === "'") return { t: 'planned' }
19
21
  const k = e.key.toLowerCase()
20
22
  if (k === 'j') return { t: 'picker' }
@@ -28,6 +30,8 @@ export function pageKey(e: KeyLike): PageKey | null {
28
30
  export const PLACE_KEY = { overview: '⌘K', planned: "⌘'", activity: '⌘L' } as const
29
31
  /** the picker's shortcut, on the first button, before home */
30
32
  export const PICKER_KEY = '⌘J'
33
+ /** the repo above and below, hinted at the project list's foot */
34
+ export const STEP_KEYS = ['⌘↑', '⌘↓'] as const
31
35
  /** the dock's shortcut, on the dock's own header */
32
36
  export const DOCK_KEY = '⌘B'
33
37
 
@@ -35,6 +39,10 @@ export const DOCK_KEY = '⌘B'
35
39
  * badge at the input's right prints this */
36
40
  export const SEARCH_KEY = '/'
37
41
 
42
+ /** in the filter, up and down walk the tree rows the filter keeps; left
43
+ * and right stay the text caret's */
44
+ export const isTreeWalk = (key: string): boolean => key === 'ArrowUp' || key === 'ArrowDown'
45
+
38
46
  export function isSearchKey(e: Pick<KeyLike, 'key' | 'metaKey' | 'ctrlKey' | 'altKey'>): boolean {
39
47
  return e.key === SEARCH_KEY && !e.metaKey && !e.ctrlKey && !e.altKey
40
48
  }
@@ -46,8 +54,11 @@ export function isSearchKey(e: Pick<KeyLike, 'key' | 'metaKey' | 'ctrlKey' | 'al
46
54
  * plus a letter only types a character inside a text field, where the page
47
55
  * leaves the key alone. Read from the physical key (`code`): on a Mac ⌥C
48
56
  * 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' }
57
+ export type SectionId = 'readme' | 'contracts' | 'shipkit' | 'repo' | 'secrets' | 'when' | 'runs' | 'plan' | 'processes' | 'git' | 'asks' | 'reflections' | 'tests' | 'media' | 'wikis' | 'admin'
58
+ export const SECTION_LETTER: Record<SectionId, string> = { readme: 'E', contracts: 'K', shipkit: 'S', repo: 'C', secrets: 'X', when: 'O', runs: 'R', plan: 'N', processes: 'P', git: 'G', asks: 'A', reflections: 'F', tests: 'T', media: 'M', wikis: 'W', admin: 'D' }
59
+ /** a section's words on its row, where the id alone would not say it */
60
+ export const SECTION_LABEL: Partial<Record<SectionId, string>> = { shipkit: 'shipkit checks', repo: 'repo checks' }
61
+ export const sectionLabel = (id: SectionId): string => SECTION_LABEL[id] ?? id
51
62
  /** the badge at the end of a section's line */
52
63
  export const sectionKeyLabel = (id: SectionId): string => `⌥${SECTION_LETTER[id]}`
53
64
 
@@ -11,6 +11,7 @@ import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
11
11
  import { ChartView, type ChartSource } from './chart-view.ts'
12
12
  import type { RepoSt } from './repo-st.ts'
13
13
  import { chartHeight } from './timeline-geometry.ts'
14
+ import { ownTrouble, reach } from './reach.ts'
14
15
 
15
16
  /** how often the page asks. A strip a second behind the runner is still live */
16
17
  export const LIVE_POLL_MS = 1_000
@@ -107,7 +108,7 @@ export class LiveSvc {
107
108
  if (this.inFlight) return
108
109
  this.inFlight = true
109
110
  try {
110
- const r = await fetch(this.st.url('/api/live.json'))
111
+ const r = await reach(this.st.url('/api/live.json'))
111
112
  if (!r.ok) throw new Error(await r.text())
112
113
  const v = (await r.json()) as LiveView
113
114
  const ended = this.running && v.runs.length === 0
@@ -125,7 +126,7 @@ export class LiveSvc {
125
126
  if (this.st.timeline.data != null) void this.st.timeline.load(true)
126
127
  }
127
128
  } catch (e) {
128
- runInAction(() => { this.error = `the live runs could not be read: ${(e as Error).message}` })
129
+ runInAction(() => { this.error = ownTrouble(e, this.st.home.machine, (m) => `the live runs could not be read: ${m}`) })
129
130
  } finally {
130
131
  this.inFlight = false
131
132
  }
@@ -14,6 +14,7 @@ import { processGroups, type ProcessGroup } from '../../home/process-rows.ts'
14
14
  import { ClockSvc } from './clock-svc.ts'
15
15
  import type { HomeSt } from './home-st.ts'
16
16
  import type { HubDot } from './projects-svc.ts'
17
+ import { ownTrouble, reach, Unreachable } from './reach.ts'
17
18
 
18
19
  export const MACHINE_POLL_MS = 1_500
19
20
  /** past the web's own wait on the daemon (serve-web.ts MACHINE_ANSWER_MS), so
@@ -38,13 +39,9 @@ function loadKept(): Kept | null {
38
39
  }
39
40
  }
40
41
 
41
- /** the poll failed because nothing behind the page answers, not on an error of an answer */
42
- /** the web (no answer at all) or the daemon behind it (the web says so) */
43
- class DownError extends Error {
44
- constructor(msg: string, readonly who: 'web' | 'daemon') {
45
- super(msg)
46
- }
47
- }
42
+ /** the web not answering, in the words of the daemon's (serve-web.ts daemonDownText) */
43
+ export const webDownText = (why: string): string =>
44
+ `the shipkit web does not answer on ${globalThis.location?.origin ?? 'this page\'s origin'} (${why}). \`shipkit pm\` runs it; this page comes back by itself once it answers`
48
45
 
49
46
  export type MachineOpts = { reload?: () => void; timeoutMs?: number }
50
47
 
@@ -149,7 +146,7 @@ export class MachineSt {
149
146
  /** pm is polled beside the daemon: either one down leaves the other shown */
150
147
  private async loadPm(): Promise<void> {
151
148
  try {
152
- const r = await fetch('/api/pm/status.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
149
+ const r = await reach('/api/pm/status.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
153
150
  if (!r.ok) throw new Error(await r.text())
154
151
  const j = (await r.json()) as PmAnswer
155
152
  runInAction(() => {
@@ -180,19 +177,15 @@ export class MachineSt {
180
177
  if (this.polling) return
181
178
  this.polling = true
182
179
  const pm = this.loadPm()
183
- // never held on: a widget that answers late only shows late
184
- if (!this.widgetsPolling) {
180
+ // never held on: a widget that answers late only shows late. Asked only
181
+ // while the overview shows: the daemon renders a widget for a reader,
182
+ // and a render can cost another machine an ssh session
183
+ if (!this.widgetsPolling && this.st.page === 'overview' && globalThis.document?.visibilityState !== 'hidden') {
185
184
  this.widgetsPolling = true
186
185
  void this.loadWidgets().finally(() => { this.widgetsPolling = false })
187
186
  }
188
187
  try {
189
- let r: Response
190
- try {
191
- r = await fetch('/api/home.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
192
- } catch (e) {
193
- throw new DownError(`the home does not answer: ${(e as Error).message}`, 'web')
194
- }
195
- if (r.headers.get('x-shipkit-daemon') === 'down') throw new DownError(await r.text(), 'daemon')
188
+ const r = await reach('/api/home.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
196
189
  if (!r.ok) throw new Error(`/api/home.json answered ${r.status}: ${await r.text()}`)
197
190
  const j = (await r.json()) as HomeAnswer
198
191
  const back = this.wasDown
@@ -213,11 +206,12 @@ export class MachineSt {
213
206
  if (back) this.reload()
214
207
  } catch (e) {
215
208
  const msg = (e as Error).message
216
- if (e instanceof DownError) {
209
+ if (e instanceof Unreachable) {
217
210
  this.wasDown = true
211
+ const who = e.who === 'web' ? 'web' : 'daemon'
218
212
  runInAction(() => {
219
- this.down = msg
220
- this.downWho = e.who
213
+ this.down = who === 'web' ? webDownText(msg) : msg
214
+ this.downWho = who
221
215
  })
222
216
  } else {
223
217
  this.pollError = true
@@ -253,14 +247,16 @@ export class MachineSt {
253
247
 
254
248
  private async post(path: string, body: unknown): Promise<boolean> {
255
249
  try {
256
- const r = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
250
+ const r = await reach(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
257
251
  if (!r.ok) throw new Error(await r.text())
258
252
  this.pollError = false
259
253
  runInAction(() => { this.error = '' })
260
254
  void this.load()
261
255
  return true
262
256
  } catch (e) {
263
- runInAction(() => { this.error = (e as Error).message })
257
+ // the machine not answering is the down card's, which the next poll raises
258
+ runInAction(() => { this.error = ownTrouble(e, this) })
259
+ void this.load()
264
260
  return false
265
261
  }
266
262
  }
@@ -0,0 +1,51 @@
1
+ // A repo's plan page: what the machine runs here on its own time, soonest
2
+ // first, then the checks only a save runs. The rows are this repo's slice of
3
+ // the machine's plan, already carried by /api/resources.json, so the page
4
+ // reads nothing of its own. The home's planned page is the same plan for
5
+ // every repo at once.
6
+ import { observer } from 'mobx-react-lite'
7
+ import type { PlanRow } from '../../plan/planner.ts'
8
+ import { LAST_TONE, markOf, reasonWords, whenWords } from './planned-st.ts'
9
+ import type { RepoSt } from './repo-st.ts'
10
+ import { ageWords } from './time-ago.tsx'
11
+
12
+ const soonest = (a: PlanRow, b: PlanRow): number => (a.dueAt ?? Infinity) - (b.dueAt ?? Infinity) || a.check.localeCompare(b.check)
13
+
14
+ const PlanLine = observer(({ app, r }: { app: RepoSt; r: PlanRow }) => {
15
+ const now = app.clock.now
16
+ const m = markOf(r)
17
+ const running = r.state === 'running'
18
+ return (
19
+ <tr className={`pln m-${m.mark}`}>
20
+ <td className="plnwhen">{whenWords(r, now)}</td>
21
+ <td><a href="#" title={r.title ?? r.check} onClick={(e) => { e.preventDefault(); app.timeline.openCheck(r.check) }}>{r.check}</a>{r.title != null ? <span className="dim"> {r.title}</span> : null}</td>
22
+ <td className="dim" title={`rule: ${r.rule}\ninactive: ${r.inactive}`}>{r.error != null ? <span className="tl red">{r.error}</span> : reasonWords(r)}</td>
23
+ <td>{r.last == null ? <span className="dim">never ran</span> : <span className={`tl ${LAST_TONE[r.last.status]}`}>{r.last.status}, {ageWords(r.last.at, now)}</span>}</td>
24
+ <td className="plnrun"><button className="ghost" disabled={running} onClick={() => void app.home.planned.run(r)}>{running ? <><span className="spin" />running</> : 'run now'}</button></td>
25
+ </tr>
26
+ )
27
+ })
28
+
29
+ const PlanTable = observer(({ app, rows }: { app: RepoSt; rows: PlanRow[] }) => (
30
+ <table className="plntable">
31
+ <tbody>{rows.map((r) => <PlanLine key={r.check} app={app} r={r} />)}</tbody>
32
+ </table>
33
+ ))
34
+
35
+ export const PlanPage = observer(({ app }: { app: RepoSt }) => {
36
+ const all = [...(app.cols.planned ?? [])].sort(soonest)
37
+ const timed = all.filter((r) => r.dueAt != null)
38
+ const onSave = all.filter((r) => r.dueAt == null)
39
+ return (
40
+ <div className="colcard checkcard">
41
+ <div className="chead">
42
+ <span className="tid">plan</span>
43
+ <h3 className="ctitle">{app.name}</h3>
44
+ </div>
45
+ <div className="usays">what the machine runs here on its own time, soonest first. Every repo at once: the planned page (⌘')</div>
46
+ {app.cols.planned == null ? <div className="dim">no plan covers this repo</div> : null}
47
+ {timed.length > 0 ? <section className="csec"><div className="cstitle">next</div><PlanTable app={app} rows={timed} /></section> : null}
48
+ {onSave.length > 0 ? <section className="csec"><div className="cstitle">on a save only</div><PlanTable app={app} rows={onSave} /></section> : null}
49
+ </div>
50
+ )
51
+ })
@@ -8,6 +8,7 @@ import type { PlannedAnswer } from '../../home/home-routes.ts'
8
8
  import type { PlanRow } from '../../plan/planner.ts'
9
9
  import { sectionOf, timeline, titleWords, waitingRows, waitingWords } from '../../plan/plan-timeline.ts'
10
10
  import type { HomeSt } from './home-st.ts'
11
+ import { ownTrouble, reach } from './reach.ts'
11
12
 
12
13
  export const PLANNED_POLL_MS = 1_000
13
14
 
@@ -78,18 +79,22 @@ export class PlannedSt {
78
79
 
79
80
  async poll(): Promise<void> {
80
81
  try {
81
- const r = await fetch('/api/home/planned.json', { signal: AbortSignal.timeout(8_000) })
82
+ const r = await reach('/api/home/planned.json', { signal: AbortSignal.timeout(8_000) })
82
83
  if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
83
84
  const j = (await r.json()) as PlannedAnswer
84
85
  runInAction(() => { this.data = j; this.error = '' })
85
86
  } catch (e) {
86
- runInAction(() => { this.error = (e as Error).message })
87
+ runInAction(() => { this.error = ownTrouble(e, this.st.machine) })
87
88
  }
88
89
  }
89
90
 
90
91
  async run(row: PlanRow): Promise<void> {
91
- const r = await fetch('/api/home/planned/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ repo: row.repo, check: row.check }) })
92
- if (!r.ok) runInAction(() => { this.error = `run ${row.check}: ${r.status} ${r.statusText}` })
92
+ try {
93
+ const r = await reach('/api/home/planned/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ repo: row.repo, check: row.check }) })
94
+ if (!r.ok) throw new Error(`${r.status} ${r.statusText}`)
95
+ } catch (e) {
96
+ runInAction(() => { this.error = ownTrouble(e, this.st.machine, (m) => `run ${row.check}: ${m}`) })
97
+ }
93
98
  await this.poll()
94
99
  }
95
100
 
@@ -0,0 +1,45 @@
1
+ // The processes page of a repo: the overview's process table, this repo's
2
+ // lines alone, never a card per process. The process a tree row opened gets
3
+ // its description and its log under the table. pm runs them, so the page
4
+ // reads the machine, never the hub.
5
+ import { observer } from 'mobx-react-lite'
6
+ import { useEffect, useRef } from 'react'
7
+ import { Log, ProcessMenu, ProcessTable } from './home-strip.tsx'
8
+ import type { RepoSt } from './repo-st.ts'
9
+ import type { ProcessRow } from '../../home/process-rows.ts'
10
+
11
+ const noop = (): void => {}
12
+
13
+ /** the opened process: its menu (actions, then its description) and its log */
14
+ const ProcessDetail = observer(({ app, r }: { app: RepoSt; r: ProcessRow }) => {
15
+ const el = useRef<HTMLElement>(null)
16
+ useEffect(() => { el.current?.scrollIntoView({ block: 'nearest' }) }, [r.id])
17
+ const m = app.home.machine
18
+ if (m == null) return null
19
+ return (
20
+ <section ref={el} className="csec pdetail" id={`process-${r.id}`}>
21
+ <div className="cstitle">{r.label}</div>
22
+ <ProcessMenu m={m} r={r} close={noop} lastLog={r.tail.length === 0} />
23
+ {r.tail.length === 0 ? null : <><div className="cstitle">log</div><Log lines={r.tail} /></>}
24
+ </section>
25
+ )
26
+ })
27
+
28
+ export const ProcessesPage = observer(({ app, id }: { app: RepoSt; id: string | null }) => {
29
+ const rows = app.globalProcesses
30
+ const m = app.home.machine
31
+ const lit = rows.find((r) => r.id === id) ?? null
32
+ return (
33
+ <div className="colcard checkcard">
34
+ <div className="chead">
35
+ <span className="tid">processes</span>
36
+ <h3 className="ctitle">{app.name}</h3>
37
+ </div>
38
+ <div className="usays">the processes this repo declares in its <code>.shipkit.ts</code> files. pm runs them, so they run whether or not the hub answers</div>
39
+ {rows.length === 0 || m == null
40
+ ? <div className="dim">this repo declares no global process</div>
41
+ : <section className="csec"><ProcessTable app={app.home} m={m} repo={app.key} /></section>}
42
+ {lit == null ? null : <ProcessDetail app={app} r={lit} />}
43
+ </div>
44
+ )
45
+ })