rvlib-shipkit 1.2.0 → 1.4.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 (173) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/asks/asks-queue.ts +4 -2
  5. package/src/assets/kits/identity-kit.ts +15 -13
  6. package/src/assets/run-assets.ts +15 -10
  7. package/src/check.ts +26 -10
  8. package/src/checks/SK001-package-json-shape.check.ts +34 -5
  9. package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
  10. package/src/checks/SK004-gitignore-policy.check.ts +11 -3
  11. package/src/checks/SK005-rvlib-versions.check.ts +13 -1
  12. package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
  13. package/src/checks/SK007-github-visibility.check.ts +9 -9
  14. package/src/checks/SK008-reviewer-agent.check.ts +14 -8
  15. package/src/checks/SK009-commit-ai-tells.check.ts +103 -61
  16. package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
  17. package/src/checks/SK014-prose-ste.check.ts +16 -6
  18. package/src/checks/SK015-nested-repo.check.ts +5 -1
  19. package/src/checks/SK016-legacy-check-shapes.check.ts +11 -5
  20. package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
  21. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
  22. package/src/checks/SK024-identity-colors.check.ts +5 -0
  23. package/src/checks/SK025-release-drift.check.ts +58 -0
  24. package/src/checks/SK026-asks-autoclose.check.ts +44 -0
  25. package/src/checks/SK027-agent-wiki-links.check.ts +50 -0
  26. package/src/checks/SK028-readme.check.ts +83 -0
  27. package/src/cli/cmd_Assets.ts +1 -1
  28. package/src/cli/cmd_Ci.ts +5 -0
  29. package/src/cli/cmd_CrossRepoActivity.ts +119 -0
  30. package/src/cli/cmd_GlobalProcesses.ts +155 -0
  31. package/src/cli/cmd_Keys.ts +17 -3
  32. package/src/cli/cmd_Overview.ts +98 -0
  33. package/src/cli/cmd_Planned.ts +117 -0
  34. package/src/cli.ts +9 -1
  35. package/src/collections/checks.ts +3 -2
  36. package/src/collections/collection-types.ts +15 -0
  37. package/src/collections/providers.ts +7 -3
  38. package/src/collections/test-time.ts +10 -1
  39. package/src/contracts/apply-contracts.ts +9 -8
  40. package/src/contracts/contract-types.ts +8 -2
  41. package/src/contracts/contracts-engine.ts +34 -13
  42. package/src/contracts/manifests/identity.contract.ts +6 -5
  43. package/src/contracts/manifests/social.contract.ts +4 -2
  44. package/src/contracts/manifests/steam.contract.ts +7 -3
  45. package/src/contracts/web-surface.ts +59 -0
  46. package/src/core/agent-spawn.ts +46 -0
  47. package/src/core/archived.ts +82 -0
  48. package/src/core/budget.ts +19 -9
  49. package/src/core/canonical-scripts.ts +12 -3
  50. package/src/core/check-runner.ts +66 -17
  51. package/src/core/discover-checks.ts +10 -5
  52. package/src/core/file-stamp.ts +20 -0
  53. package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
  54. package/src/core/identity-color.ts +1 -1
  55. package/src/core/layout.ts +2 -0
  56. package/src/core/motherhome.ts +88 -66
  57. package/src/core/repo-facts.ts +14 -0
  58. package/src/core/retention.ts +14 -5
  59. package/src/core/retired-checks.ts +7 -0
  60. package/src/core/run-check.ts +8 -7
  61. package/src/core/run-ci.ts +46 -15
  62. package/src/core/run-fix.ts +7 -2
  63. package/src/core/run-fixers.ts +10 -6
  64. package/src/core/wire.ts +49 -9
  65. package/src/fleet/scan-fleet.ts +2 -1
  66. package/src/home/activity-shape.ts +138 -0
  67. package/src/home/activity.ts +256 -0
  68. package/src/home/global-units.ts +3 -2
  69. package/src/home/gui-app/ShipkitGui.swift +144 -25
  70. package/src/home/gui-app.ts +93 -30
  71. package/src/home/home-launchd.ts +10 -2
  72. package/src/home/home-routes.ts +34 -6
  73. package/src/home/home-start.ts +33 -5
  74. package/src/home/machine-stores.ts +4 -2
  75. package/src/home/open-app.ts +23 -10
  76. package/src/home/pm-config.ts +15 -11
  77. package/src/home/pm-inputs.ts +76 -0
  78. package/src/home/pm.ts +40 -7
  79. package/src/home/process-rows.ts +222 -0
  80. package/src/home/resources.ts +99 -20
  81. package/src/home/run-daemon.ts +12 -7
  82. package/src/home/run-pm.ts +30 -5
  83. package/src/home/serve-web.ts +6 -3
  84. package/src/home/supervise.ts +16 -1
  85. package/src/home/widget-text.ts +119 -0
  86. package/src/home/widgets.ts +32 -3
  87. package/src/hooks/commit-message.ts +2 -3
  88. package/src/hooks/run-hooks.ts +32 -5
  89. package/src/ledger/db.ts +1 -1
  90. package/src/ledger/gate-checks.ts +20 -6
  91. package/src/ledger/house-runs.ts +43 -0
  92. package/src/ledger/live-timeline.ts +4 -19
  93. package/src/ledger/run-log.ts +39 -10
  94. package/src/ledger/store.ts +30 -2
  95. package/src/ledger/world-cache.ts +63 -14
  96. package/src/plan/cadence.ts +21 -5
  97. package/src/plan/catch-up.ts +37 -0
  98. package/src/plan/plan-timeline.ts +99 -0
  99. package/src/plan/planner.ts +138 -13
  100. package/src/plan/repo-source.ts +120 -8
  101. package/src/release/run-release.ts +9 -2
  102. package/src/release/unit-resolve.ts +8 -12
  103. package/src/repo-config.ts +17 -4
  104. package/src/serve/house-runs.ts +53 -0
  105. package/src/serve/hub-app/activity-st.ts +168 -0
  106. package/src/serve/hub-app/activity-view.tsx +218 -0
  107. package/src/serve/hub-app/activity.css +78 -0
  108. package/src/serve/hub-app/admin-svc.ts +41 -0
  109. package/src/serve/hub-app/admin-view.tsx +42 -0
  110. package/src/serve/hub-app/chart-view.ts +12 -5
  111. package/src/serve/hub-app/check-words.ts +2 -0
  112. package/src/serve/hub-app/checks-svc.ts +2 -1
  113. package/src/serve/hub-app/col-tables-view.tsx +124 -0
  114. package/src/serve/hub-app/col-tables.ts +115 -0
  115. package/src/serve/hub-app/cols-svc.ts +28 -0
  116. package/src/serve/hub-app/controls.css +32 -0
  117. package/src/serve/hub-app/cycle-button.tsx +36 -0
  118. package/src/serve/hub-app/dock-anim.ts +25 -0
  119. package/src/serve/hub-app/dock-st.ts +329 -0
  120. package/src/serve/hub-app/dock-view.tsx +68 -0
  121. package/src/serve/hub-app/dock.css +35 -0
  122. package/src/serve/hub-app/hash-route.ts +7 -1
  123. package/src/serve/hub-app/header-bar.tsx +79 -104
  124. package/src/serve/hub-app/home-st.ts +50 -13
  125. package/src/serve/hub-app/home-strip.css +81 -23
  126. package/src/serve/hub-app/home-strip.tsx +243 -102
  127. package/src/serve/hub-app/home.html +4 -0
  128. package/src/serve/hub-app/hub-app.tsx +103 -161
  129. package/src/serve/hub-app/hub-types.ts +6 -3
  130. package/src/serve/hub-app/hub.css +101 -39
  131. package/src/serve/hub-app/icons.tsx +27 -3
  132. package/src/serve/hub-app/index.html +4 -0
  133. package/src/serve/hub-app/keys.ts +44 -9
  134. package/src/serve/hub-app/live-svc.ts +24 -64
  135. package/src/serve/hub-app/machine-st.ts +65 -13
  136. package/src/serve/hub-app/md.ts +38 -5
  137. package/src/serve/hub-app/next-run.ts +1 -1
  138. package/src/serve/hub-app/palette-st.ts +13 -9
  139. package/src/serve/hub-app/palette-view.tsx +47 -0
  140. package/src/serve/hub-app/pill-row.tsx +65 -0
  141. package/src/serve/hub-app/planned-st.ts +21 -85
  142. package/src/serve/hub-app/planned-view.tsx +12 -7
  143. package/src/serve/hub-app/planned.css +5 -2
  144. package/src/serve/hub-app/project-list.css +43 -21
  145. package/src/serve/hub-app/project-list.tsx +122 -23
  146. package/src/serve/hub-app/projects-svc.ts +120 -12
  147. package/src/serve/hub-app/readme-svc.ts +38 -0
  148. package/src/serve/hub-app/readme-view.tsx +19 -0
  149. package/src/serve/hub-app/readme.css +11 -0
  150. package/src/serve/hub-app/repo-sections.tsx +61 -0
  151. package/src/serve/hub-app/repo-st.ts +26 -7
  152. package/src/serve/hub-app/runs-tabs.ts +1 -1
  153. package/src/serve/hub-app/sidebar.tsx +84 -0
  154. package/src/serve/hub-app/tests-view.tsx +31 -4
  155. package/src/serve/hub-app/timeline-chart.tsx +31 -27
  156. package/src/serve/hub-app/timeline-geometry.ts +40 -5
  157. package/src/serve/hub-app/timeline-svc.ts +2 -2
  158. package/src/serve/hub-app/timeline-view.tsx +29 -9
  159. package/src/serve/hub-runtime.ts +17 -9
  160. package/src/serve/other-repos.ts +8 -45
  161. package/src/serve/project-gutter-route.ts +100 -20
  162. package/src/serve/project-gutter.ts +34 -13
  163. package/src/serve/readme-route.ts +57 -0
  164. package/src/serve/serve-hub.ts +13 -8
  165. package/src/serve/tracked-watch.ts +33 -1
  166. package/src/serve/warm-cell.ts +49 -16
  167. package/src/shipkit-files.ts +20 -4
  168. package/src/suite/judge-suites.ts +42 -17
  169. package/src/suite/suite-cli.ts +5 -8
  170. package/src/wiki/lint-md.ts +2 -0
  171. package/src/wiki/scan-md.ts +37 -10
  172. package/src/checks/SK023-stores-under-cap.check.ts +0 -49
  173. package/src/checks/contracts.check.ts +0 -20
@@ -1,40 +1,28 @@
1
- // The header over every page, and the live strip under it. The header is the
2
- // repo name (on the home the strip above names it), one link per core page,
3
- // and on the right the repo's own controls and the two runs that are not one
4
- // check with what is running now. The strip is the one run chart
5
- // (timeline-chart.tsx) over a rolling window that ends now: folded, a thin
6
- // bar of the last two minutes; unfolded, the full chart over ten, zoomable.
7
- // Each fold has ONE height whatever runs, so a run never moves the page.
1
+ // A repo's pieces of the top bar, beside its icon and its name: what runs now
2
+ // and the two runs that are not one check (RepoRun), then on the right its
3
+ // processes, the hub's own trouble and modernize while it is behind
4
+ // (RepoTools), and at the far right end the editor icons (RepoOpeners). On
5
+ // the home they ride in the home strip (home-strip.tsx); a hub of its own
6
+ // draws them in its own header, with its name. The repo's sections are the
7
+ // left pane's (repo-sections.tsx). What runs, drawn, is the dock at the
8
+ // bottom of the page (dock-view.tsx).
8
9
  import { observer } from 'mobx-react-lite'
9
- import { RUNS_ID } from '../../collections/collection-types.ts'
10
- import { Help } from './help.tsx'
11
- import { openWikis, WIKIS } from './wikis-view.tsx'
10
+ import { Icon, ICON } from './icons.tsx'
12
11
  import type { RepoSt } from './repo-st.ts'
13
- import { byWords, TimelineChart } from './timeline-chart.tsx'
14
- import { RepoControls } from './home-strip.tsx'
15
12
 
16
- export type CorePage = { id: string; label: string; on: boolean; go: () => void }
17
-
18
- /** the pages reached for most, in the order they are reached for. A page this
19
- * repo does not have is not a link */
20
- export function corePages(app: RepoSt): CorePage[] {
21
- const has = (col: string): boolean => app.cols.colMeta.some((c) => c.id === col)
22
- const sel = app.sel
23
- const onCol = (col: string): boolean => (sel?.t === 'col' && sel.id === col) || (sel?.t === 'item' && sel.col === col)
24
- const out: CorePage[] = []
25
- if (has('checks')) out.push({ id: 'checks', label: 'checks', on: onCol('checks'), go: () => app.showCol('checks') })
26
- // a page, never a resource: every hub keeps a run record, so it is
27
- // always here
28
- out.push({ id: 'runs', label: 'runs', on: onCol('runs'), go: () => app.cols.select('runs', RUNS_ID) })
29
- if (has('asks')) out.push({ id: 'asks', label: 'asks', on: onCol('asks'), go: () => app.showCol('asks') })
30
- const firstCat = app.media.assets[0]?.cat ?? app.cols.cachedCat ?? undefined
31
- if (firstCat != null) out.push({ id: 'media', label: 'media', on: sel?.t === 'asset' || sel?.t === 'cat', go: () => app.showCat(firstCat) })
32
- if (has(WIKIS)) out.push({ id: WIKIS, label: 'wikis', on: onCol(WIKIS), go: () => openWikis(app) })
33
- return out
13
+ /** the repo's hub has not answered on the home: its reason and its fix show
14
+ * instead of the page. A screen this browser kept shows meanwhile, until
15
+ * the machine says the hub is not up */
16
+ export const notUp = (app: RepoSt): boolean => {
17
+ const m = app.home.machine
18
+ if (m == null || app.started) return false
19
+ if (!app.cols.cached) return true
20
+ return m.fresh && m.worker(app.key)?.status !== 'up'
34
21
  }
35
22
 
36
23
  /** what is running, in one line: the hub's own run first, since it says the
37
- * most, else the newest run the record has in flight */
24
+ * most, else the newest run the record has in flight. Cut to its width, the
25
+ * whole line on hover */
38
26
  const RunState = observer(({ app }: { app: RepoSt }) => {
39
27
  const own = app.checks.strip
40
28
  const run = app.live.view?.runs[0]
@@ -49,8 +37,28 @@ const RunState = observer(({ app }: { app: RepoSt }) => {
49
37
  )
50
38
  })
51
39
 
40
+ export const HEALTH_TITLE = 'run the health pass: every check but the build steps, as shipkit check runs them (creds, probes, rules, contracts)'
41
+ export const GATE_TITLE = 'run the gate: every check shipkit ci runs, the build steps included (lint, typecheck, tests)'
42
+
43
+ /** the run state, then the two runs, compact: below a width their labels
44
+ * give way to their icons, so the bar never wraps */
45
+ export const RepoRun = observer(({ app }: { app: RepoSt }) => {
46
+ const u = app.checks
47
+ return (
48
+ <span className="hrun">
49
+ <RunState app={app} />
50
+ <button className="primary hbtn" disabled={u.busy} title={HEALTH_TITLE} aria-label="health pass" onClick={() => void u.run()}>
51
+ <Icon d={ICON.health} /><span className="lbl">health pass</span>
52
+ </button>
53
+ <button className="hbtn" disabled={u.busy} title={GATE_TITLE} aria-label="gate" onClick={() => void u.run(undefined, { gate: true, label: 'the gate' })}>
54
+ <Icon d={ICON.gate} /><span className="lbl">gate</span>
55
+ </button>
56
+ </span>
57
+ )
58
+ })
59
+
52
60
  /** what is wrong with the hub itself, one chip per problem. Nothing when all
53
- * is well: the header states trouble, never health */
61
+ * is well: the bar states trouble, never health */
54
62
  const HubStatus = observer(({ app }: { app: RepoSt }) => (
55
63
  <>
56
64
  {app.cols.hubStatus.map((p) => (
@@ -59,30 +67,49 @@ const HubStatus = observer(({ app }: { app: RepoSt }) => (
59
67
  </>
60
68
  ))
61
69
 
62
- export const HeaderBar = observer(({ app }: { app: RepoSt }) => {
63
- const u = app.checks
70
+ /** modernize, while the repo runs an older shipkit than the home */
71
+ const Modernize = observer(({ app }: { app: RepoSt }) => {
72
+ const m = app.home.machine
73
+ const behind = m?.data?.behind[app.key]
74
+ if (m == null || behind == null) return null
75
+ return <button className="primary hbtn" onClick={() => void m.modernize(app.key)} title={`this repo runs shipkit ${behind}, the home runs ${m.data?.version}`}>modernize {behind} → {m.data?.version}</button>
76
+ })
77
+
78
+ /** the repo in its editor and in its git client, one icon each, at the far
79
+ * right end of the bar. Only on the home: the machine opens them */
80
+ export const RepoOpeners = observer(({ app }: { app: RepoSt }) => {
81
+ const m = app.home.machine
82
+ if (m == null) return null
64
83
  return (
65
- <header className="hdr">
66
- {/* on the home the strip names the repo, in its colour */}
67
- {app.home.machine != null ? null : <span className="hname" style={app.name === '' ? undefined : { color: app.home.projects.identityOf(app.name).colorDark }}>{app.name !== '' ? app.name : 'hub'}</span>}
68
- <nav className="hnav">
69
- {/* the spinner's slot stays once the repo is read, so the links never move */}
70
- <span className={`spin${app.cols.loaded ? ' idle' : ''}`} title={app.cols.loaded ? undefined : app.cols.cached ? 'showing what this browser kept, reading the repo' : 'reading the repo'} />
71
- {corePages(app).map((p) => (
72
- <button key={p.id} className={p.on ? 'on' : ''} onClick={p.go}>{p.label}</button>
73
- ))}
74
- </nav>
75
- <span className="sp" />
76
- <RepoControls app={app.home} name={app.key} />
77
- <ProcessChips app={app} />
78
- <HubStatus app={app} />
79
- <RunState app={app} />
80
- <button className="primary" disabled={u.busy} onClick={() => void u.run()}>run the health pass</button>
81
- <button disabled={u.busy} onClick={() => void u.run(undefined, { gate: true, label: 'the gate' })}>run the gate</button>
82
- </header>
84
+ <>
85
+ <button className="hicon" title="open in VS Code" aria-label={`open ${app.key} in VS Code`} onClick={() => m.openInCode(app.key)}><Icon d={ICON.vscode} /></button>
86
+ <button className="hicon" title="open in Fork" aria-label={`open ${app.key} in Fork`} onClick={() => void app.home.activity.openFork(app.key)}><Icon d={ICON.fork} /></button>
87
+ </>
83
88
  )
84
89
  })
85
90
 
91
+ /** the right side of the bar for a repo, before the machine's trouble and
92
+ * the openers, which close it */
93
+ export const RepoTools = observer(({ app }: { app: RepoSt }) => (
94
+ <>
95
+ <ProcessChips app={app} />
96
+ <HubStatus app={app} />
97
+ <Modernize app={app} />
98
+ </>
99
+ ))
100
+
101
+ /** the bar of a hub of its own: its name, then the same pieces the home
102
+ * strip carries for a repo */
103
+ export const HeaderBar = observer(({ app }: { app: RepoSt }) => (
104
+ <header className="hdr">
105
+ <span className="hname" style={app.name === '' ? undefined : { color: app.home.projects.identityOf(app.name).colorDark }}>{app.name !== '' ? app.name : 'hub'}</span>
106
+ <RepoRun app={app} />
107
+ <span className="sp" />
108
+ <RepoTools app={app} />
109
+ <RepoOpeners app={app} />
110
+ </header>
111
+ ))
112
+
86
113
  /** one switch per process the repo declares (serve.processes): its dot, its
87
114
  * id, a click starts or stops it, and a link once it serves */
88
115
  export const ProcessChips = observer(({ app }: { app: RepoSt }) => {
@@ -106,55 +133,3 @@ export const ProcessChips = observer(({ app }: { app: RepoSt }) => {
106
133
  </span>
107
134
  )
108
135
  })
109
-
110
- /** the key that folds and unfolds the strip, printed on its chevron */
111
- export const LIVE_KEY = 'l'
112
-
113
- /** the strip's own row, in px: the one height the folded strip has */
114
- export const LIVE_ROW_H = 30
115
-
116
- /** what the strip is, in the (?) beside its chevron */
117
- export const LIVE_HELP = "every check shipkit runs, whoever started it: this hub, an agent in a terminal, the plan, here and in the other repos. Folded, the last two minutes: a bar grows at the right while its check runs and slides left once it ends, in its verdict colour. Unfolded, the whole record, opened on the last ten minutes: the run bands, the time axis, and over the lanes how many checks ran at once, with the cap dashed and the stretches past it in orange. It follows now: the right edge is the present and the view slides with it. The wheel zooms and a drag pans; leaving the right edge stops the view where it is so a bar can be read, and ● live, the k key or panning back to the right edge follows now again. While a tooltip is open the view stands still. fit shows every run on record. A lane at the bottom is another repo with something running; a click opens its hub when it serves. The count is the checks in flight against the runner's cap"
118
-
119
- const Chevron = ({ open }: { open: boolean }) => (
120
- <svg className={open ? 'cvt open' : 'cvt'} viewBox="0 0 24 24" width="11" height="11" aria-hidden="true">
121
- <path d="M9 5l7 7-7 7" fill="none" stroke="currentColor" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round" />
122
- </svg>
123
- )
124
-
125
- export const LiveStrip = observer(({ app }: { app: RepoSt }) => {
126
- const l = app.live
127
- const v = l.view
128
- const run = v?.runs[0]
129
- const hidden = l.chart.layout?.hidden ?? 0
130
- const waits = v == null ? '' : [
131
- v.queued > 0 ? `${v.queued} queued for a free slot` : '',
132
- ...v.blocked.map((w) => `${w.id} waits on ${w.deps.join(', ')}`),
133
- ].filter((x) => x !== '').join('\n')
134
- const who = run == null ? 'nothing running' : `${run.label}`
135
- const chart = (
136
- <TimelineChart view={l.chart} compact={!l.open} repos={v?.repos ?? []} follow zoomable={l.open}
137
- onBar={(b) => app.cols.select('checks', `check:${b.id}`)}
138
- onRepo={(r) => void l.openRepo(r.repo)} />
139
- )
140
- return (
141
- <div className={`live${l.open ? ' open' : ''}${run == null ? ' idle' : ''}`} style={l.open ? { height: LIVE_ROW_H + l.held.h } : undefined}>
142
- <div className="lrow">
143
- <button className="lfold" onClick={() => l.toggle()} title={`${l.open ? 'fold' : 'unfold'} the live timeline (${LIVE_KEY})`}>
144
- <Chevron open={l.open} /><span className="sk">{LIVE_KEY}</span>
145
- </button>
146
- <Help text={LIVE_HELP} />
147
- <span className="lwho" title={run == null ? '' : `${run.label}, started from the ${byWords(run.by)}${v != null && v.runs.length > 1 ? `\n${v.runs.length - 1} more in flight here` : ''}`}>
148
- {who}{run == null ? null : <i>{byWords(run.by)}</i>}{v != null && v.runs.length > 1 ? <b>+{v.runs.length - 1}</b> : null}
149
- </span>
150
- {l.open ? <span className="sp" /> : <div className="lchart">{chart}</div>}
151
- {l.note !== '' ? <span className="lnote">{l.note}</span> : null}
152
- {hidden > 0 ? <span className="lmore" title="check runs that did not fit in the folded strip: unfold it to see them">+{hidden}</span> : null}
153
- <span className="lbusy" title={waits === '' ? 'checks in flight in this repo, against the runner cap' : waits}>
154
- in flight <span className="dnum">{v?.busy ?? 0}</span> / cap <span className="dnum">{v?.cap ?? 0}</span>
155
- </span>
156
- </div>
157
- {l.open ? <div className="lopen">{chart}</div> : null}
158
- </div>
159
- )
160
- })
@@ -1,6 +1,6 @@
1
1
  // HomeSt: THE root of the page (build/app-state-tree), exposed as window.app.
2
- // One tree for every page the home serves, '/', '/planned' and '/r/<repo>/':
3
- // the machine, the project list, the plan, and one RepoSt per repo visited, built on
2
+ // One tree for every page the home serves, '/', '/planned', '/activity' and
3
+ // '/r/<repo>/': the machine, the project list, the plan, the activity, and one RepoSt per repo visited, built on
4
4
  // the first visit and kept for the life of the page (same name, same
5
5
  // instance), so a repo you come back to shows what it showed. Only the shown
6
6
  // repo polls. Every other repo whose hub is up is read ahead in the
@@ -11,30 +11,41 @@
11
11
  // together, and only `show` moves the page. A list click, a link, back,
12
12
  // forward and a hand edited address all land there.
13
13
  import { makeAutoObservable, observableShallow, reaction, type IReactionDisposer } from 'mobx'
14
- import { currentHash } from './hash-route.ts'
14
+ import { currentHash, hashFor } from './hash-route.ts'
15
15
  import { MachineSt, type MachineOpts } from './machine-st.ts'
16
16
  import { LayoutSt } from './layout-st.ts'
17
+ import { DockSt } from './dock-st.ts'
17
18
  import { PaletteSt } from './palette-st.ts'
18
19
  import { PlannedSt } from './planned-st.ts'
20
+ import { ActivitySt } from './activity-st.ts'
19
21
  import { ProjectsSvc } from './projects-svc.ts'
20
22
  import { parseFilter, RepoSt } from './repo-st.ts'
21
23
 
22
24
  /** the longest one repo's read ahead holds the next one back */
23
25
  export const PREFETCH_WAIT_MS = 10_000
24
26
 
25
- /** what the page shows: the machine overview, the plan, or one repo */
26
- export type Page = 'overview' | 'planned' | 'repo'
27
+ /** what the page shows: the machine overview, the plan, the activity, or one repo */
28
+ export type Page = 'overview' | 'planned' | 'activity' | 'archived' | 'repo'
29
+
30
+ /** the address of each page of the home that is no repo's */
31
+ export const PLACE_PATH = { overview: '/', planned: '/planned', activity: '/activity', archived: '/archived' } as const
27
32
 
28
33
  /** where a repo's hub answers on the home */
29
34
  export const repoBase = (name: string): string => `/r/${encodeURIComponent(name)}`
30
35
  /** the page of a repo on the home */
31
36
  export const repoPage = (name: string): string => `${repoBase(name)}/`
32
37
 
38
+ /** where a check's own page answers on the home */
39
+ export const checkAddress = (repo: string, check: string): string =>
40
+ `${repoPage(repo)}#${hashFor({ t: 'item', col: 'checks', id: `check:${check}` })}`
41
+
33
42
  /** the page an address on the home names */
34
43
  export function routeOf(pathname: string): { page: Page; repo: string | null } {
35
44
  const m = /^\/r\/([^/]+)/.exec(pathname)
36
45
  if (m != null) return { page: 'repo', repo: decodeURIComponent(m[1]!) }
37
46
  if (/^\/planned\/?$/.test(pathname)) return { page: 'planned', repo: null }
47
+ if (/^\/activity\/?$/.test(pathname)) return { page: 'activity', repo: null }
48
+ if (/^\/archived\/?$/.test(pathname)) return { page: 'archived', repo: null }
38
49
  return { page: 'overview', repo: null }
39
50
  }
40
51
 
@@ -55,13 +66,18 @@ export class HomeSt {
55
66
  projects: ProjectsSvc
56
67
  /** the plan, polled while it is the page */
57
68
  planned: PlannedSt
58
- /** the one picker, cmd+k */
69
+ /** what every repo committed and holds, polled while it is the page */
70
+ activity: ActivitySt
71
+ /** the one picker, cmd+j */
59
72
  palette: PaletteSt
60
73
  /** the widths of the project list and a repo's sidebar */
61
74
  layout: LayoutSt
75
+ /** the run chart docked under the whole page */
76
+ dock: DockSt
62
77
  page: Page
63
- /** the key of the shown repo: its name on the home, '' for the one repo
64
- * of a hub of its own. null on the overview and the plan */
78
+ /** the key of the repo picked last: its name on the home, '' for the one
79
+ * repo of a hub of its own. A global page keeps it (only `shown` says
80
+ * whether it is on screen); null until a repo is picked */
65
81
  selected: string | null = null
66
82
  /** every repo visited, by key, kept for the life of the page */
67
83
  repos = new Map<string, RepoSt>()
@@ -73,15 +89,20 @@ export class HomeSt {
73
89
  constructor(o: HomeOpts) {
74
90
  this.projects = new ProjectsSvc()
75
91
  this.planned = new PlannedSt(this)
92
+ this.activity = new ActivitySt(this)
76
93
  this.palette = new PaletteSt(this)
77
94
  this.layout = new LayoutSt()
95
+ this.dock = new DockSt(this)
78
96
  this.machine = o.home ? new MachineSt(this, o) : null
79
97
  this.page = o.home ? 'overview' : 'repo'
80
- makeAutoObservable<this, 'disposers' | 'onUrl' | 'prefetching'>(this, { machine: false, projects: false, planned: false, palette: false, layout: false, repos: observableShallow, disposers: false, onUrl: false, prefetching: false })
98
+ makeAutoObservable<this, 'disposers' | 'onUrl' | 'prefetching'>(this, { machine: false, projects: false, planned: false, activity: false, palette: false, layout: false, dock: false, repos: observableShallow, disposers: false, onUrl: false, prefetching: false })
81
99
  const m = this.machine
82
100
  if (m != null) {
83
101
  this.projects.onSelect = (name) => this.select(name)
84
- this.projects.onPlace = (p) => this.navigate(p === 'overview' ? '/' : '/planned')
102
+ this.projects.onPlace = (p) => this.navigate(PLACE_PATH[p])
103
+ this.projects.onPicker = () => this.palette.toggle()
104
+ this.projects.pickerOpen = () => this.palette.open
105
+ this.projects.onArchived = () => this.navigate(PLACE_PATH.archived)
85
106
  this.projects.dotOf = (name) => m.dotOf(name)
86
107
  this.projects.warnOf = (name) => m.warnOf(name)
87
108
  } else {
@@ -106,6 +127,8 @@ export class HomeSt {
106
127
  get title(): string {
107
128
  if (this.page === 'overview') return 'home'
108
129
  if (this.page === 'planned') return 'planned'
130
+ if (this.page === 'activity') return 'activity'
131
+ if (this.page === 'archived') return 'archived'
109
132
  const r = this.shown
110
133
  return r == null || r.name === '' ? 'hub' : `${r.name} · hub`
111
134
  }
@@ -124,6 +147,7 @@ export class HomeSt {
124
147
 
125
148
  boot(): void {
126
149
  this.machine?.boot()
150
+ this.dock.boot()
127
151
  this.disposers.push(reaction(() => this.title, (t) => { document.title = t }, { fireImmediately: true }))
128
152
  // back, forward, a pasted link and a hand edited hash all arrive
129
153
  // here; fromUrl is idempotent, so the two events firing together
@@ -140,6 +164,8 @@ export class HomeSt {
140
164
  window.removeEventListener('hashchange', this.onUrl)
141
165
  this.machine?.stop()
142
166
  this.planned.stop()
167
+ this.activity.stop()
168
+ this.dock.dispose()
143
169
  for (const r of this.repos.values()) r.dispose()
144
170
  }
145
171
 
@@ -148,6 +174,13 @@ export class HomeSt {
148
174
  this.show('repo', name, 'push')
149
175
  }
150
176
 
177
+ /** a check: its own page in its repo, in place. The selection is written
178
+ * before the repo is shown, so the one history entry already names it */
179
+ openCheck(repo: string, check: string): void {
180
+ this.repo(repo).cols.select('checks', `check:${check}`)
181
+ this.select(repo)
182
+ }
183
+
151
184
  /** a link to a page of the home: the same page, in place */
152
185
  navigate(path: string): void {
153
186
  const u = new URL(path, 'http://home.invalid')
@@ -183,16 +216,20 @@ export class HomeSt {
183
216
  if (prev != null && prev !== next) prev.pause()
184
217
  next?.markSeen()
185
218
  this.page = page
186
- this.selected = next?.key ?? null
219
+ // a global page is no repo's: the repo picked last stays selected
220
+ if (next != null) this.selected = next.key
187
221
  this.projects.selected = this.selected
188
- this.projects.place = page === 'repo' ? null : page
222
+ this.projects.away = next == null
223
+ this.projects.place = page === 'repo' || page === 'archived' ? null : page
189
224
  if (how !== 'none') {
190
- const to = next != null ? `${repoPage(next.key)}${next.search}${next.hash}` : page === 'planned' ? this.planned.address : '/'
225
+ const to = next != null ? `${repoPage(next.key)}${next.search}${next.hash}` : page === 'planned' ? this.planned.address : page === 'activity' || page === 'archived' ? PLACE_PATH[page] : '/'
191
226
  if (how === 'push') history.pushState(null, '', to)
192
227
  else history.replaceState(null, '', to)
193
228
  }
194
229
  if (page === 'planned') this.planned.start()
195
230
  else this.planned.stop()
231
+ if (page === 'activity') this.activity.start()
232
+ else this.activity.stop()
196
233
  if (next != null && next !== prev && this.machine?.data != null) void this.machine.hub(next.key, 'ensure')
197
234
  this.wake()
198
235
  }
@@ -1,21 +1,23 @@
1
1
  /* the home's strip over a hub page, and the overview. hub.css owns every
2
2
  token and the base controls: no colour is written here. */
3
- .hstrip{display:flex;align-items:center;flex-wrap:nowrap;gap:var(--s2);height:32px;flex:none;padding:0 var(--s3);border-bottom:1px solid var(--line);overflow:hidden;white-space:nowrap}
3
+ .hstrip{display:flex;align-items:center;flex-wrap:nowrap;gap:var(--s2);height:32px;flex:none;padding:0 var(--s3);border-bottom:1px solid var(--line);background:var(--bg-top);overflow:hidden;white-space:nowrap}
4
4
  .hstrip .sp{flex:1}
5
5
  .hplace{font-size:12px;font-weight:650;letter-spacing:.08em;text-transform:uppercase;color:var(--dim)}
6
- .hrepo{display:inline-flex;align-items:center;gap:var(--s2);min-width:0}
6
+ .hrepo{display:inline-flex;align-items:center;gap:var(--s2);min-width:0;flex:0 1 auto}
7
7
  .hrepo img,.hrico{flex:none;width:18px;height:18px;border-radius:4px;object-fit:contain}
8
- .hrname{font-size:13px;font-weight:650;letter-spacing:.04em}
8
+ .hrname{font-size:13px;font-weight:650;letter-spacing:.04em;min-width:0;overflow:hidden;text-overflow:ellipsis}
9
9
  .hswatch{display:inline-flex;gap:2px}
10
10
  .hswatch input{width:16px;height:16px;padding:0;border:1px solid var(--line);border-radius:3px;background:none;cursor:pointer}
11
11
  .hswatch input::-webkit-color-swatch-wrapper{padding:0}
12
12
  .hswatch input::-webkit-color-swatch{border:none;border-radius:2px}
13
13
  .halert{display:inline-flex;align-items:center;font-size:12px;color:var(--errink)}
14
- .hctl{display:inline-flex;align-items:center;gap:var(--s2)}
15
- .hstat,.hflag{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px;color:var(--dim)}
16
14
  .herr{font-size:12px;color:var(--errink);max-width:40ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
17
- .hdown{padding:var(--s2) var(--s3);font-size:13px;color:var(--errink);border-bottom:1px solid var(--red)}
18
- .hdown b{color:var(--red);margin-right:var(--s2)}
15
+ /* the daemon or pm down: toasts over the bottom right corner of the page
16
+ column (hub.css .appcol is their box), just above the dock, never taking
17
+ room, so one coming or going moves nothing */
18
+ .htoasts{position:absolute;right:var(--s3);bottom:var(--s3);z-index:25;display:flex;flex-direction:column;align-items:flex-end;gap:var(--s1);max-width:min(60ch,calc(100% - 2 * var(--s3)));pointer-events:none}
19
+ .htoast{max-width:100%;padding:var(--s1) var(--s2);background:var(--panel);border:1px solid var(--red);border-radius:4px;box-shadow:0 6px 20px var(--shadow);font-size:12px;color:var(--errink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:auto}
20
+ .htoast b{color:var(--red);margin-right:var(--s2)}
19
21
  .hdot{display:inline-block;width:8px;height:8px;border-radius:4px;margin-right:var(--s1);background:var(--line)}
20
22
  .hdot.up{background:var(--ok)}.hdot.starting,.hdot.stopping{background:var(--warn)}.hdot.crashed{background:var(--red)}
21
23
  .hres{display:inline-flex;align-items:center;padding:var(--s0) var(--s2);font-size:12px}
@@ -25,29 +27,86 @@
25
27
  can move into it to scroll */
26
28
  .hpop{z-index:20;margin:0;padding:var(--s2);width:min(80ch,80vw);max-height:40vh;overflow:auto;
27
29
  background:var(--panel);border:1px solid var(--line);border-radius:3px;box-shadow:0 6px 20px var(--shadow);font:11px/1.45 ui-monospace,Menlo,monospace;color:var(--dim);white-space:pre-wrap}
28
- .hmon{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(340px,100%),1fr));gap:var(--s3)}
29
- .hwidget{padding:var(--s2) var(--s3);border:1px solid var(--line);border-radius:3px;font-size:12px;overflow:auto}
30
- .hwidget h3{margin:0 0 var(--s2);font-size:12px;font-weight:650}
30
+ /* each widget card exactly as wide as what it holds; a widget's leading
31
+ <header> joins the title line, dim */
32
+ .hmon{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--s2)}
33
+ .hwidget{flex:0 1 auto;max-width:100%;padding:var(--s0) var(--s2);border:1px solid var(--line);border-radius:3px;font-size:12px;line-height:1.4;overflow:auto}
34
+ .hwidget h3{margin:0;font-size:12px;font-weight:650;white-space:nowrap}
35
+ .hwidget h3 .hdim{font-weight:400}
36
+ /* meters in two columns: <table class="wmeters">, label · bar · value, twice a row */
37
+ .wmeters td{padding-right:var(--s1)}
38
+ .wmeters td:nth-child(3n){padding-right:var(--s3)}
39
+ .wmeters td:last-child{padding-right:0}
40
+ .wmeters .wbar{margin-right:0}
41
+ /* a meter's value: small, its digits one width so a column never jitters */
42
+ .wmeters .wv{font-size:11px;font-variant-numeric:tabular-nums}
31
43
  .hwidget table{border-collapse:collapse}
32
44
  .hwidget td{padding:0 var(--s3) 0 0;white-space:nowrap;vertical-align:middle}
45
+ /* a widget error: in its card where the html goes, or one mark after the cards */
46
+ .hwerr{font-size:12px;color:var(--warn);white-space:nowrap;cursor:help}
47
+ .hwerr .gpwarn{margin:0 var(--s1) 0 0}
33
48
  .hbad{color:var(--errink)}
34
49
  .hwait{flex:1;min-height:0;padding:var(--s4);color:var(--dim);font-size:13px;overflow:auto}
35
50
  .hwait p{margin:0 0 var(--s2)}
36
51
  .hlog{margin:var(--s2) 0 0;padding:var(--s2);background:var(--panel);border:1px solid var(--line);border-radius:3px;
37
52
  font:11px/1.45 ui-monospace,Menlo,monospace;color:var(--dim);white-space:pre-wrap;max-height:60vh;overflow:auto}
38
53
  .hall{flex:1;min-height:0;overflow:auto;padding:var(--s4)}
39
- /* the sections one under the other: shipkit, the process cards, the monitor */
54
+ /* the sections one under the other: the monitor, then the processes */
40
55
  .hstack{display:flex;flex-direction:column;gap:var(--s4)}
41
- /* one card per global process, as many columns as fit, never a horizontal scroll */
42
- .hcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(300px,100%),1fr));gap:var(--s3)}
43
- .hcard{display:flex;flex-direction:column;gap:var(--s1);min-width:0;padding:var(--s2) var(--s3);background:var(--panel);border:1px solid var(--line);border-radius:4px;font-size:12px}
44
- .hchead{display:flex;align-items:center;gap:var(--s2)}
45
- .hcstate{font-size:12px;color:var(--dim)}
46
- .hcstate.up{color:var(--ok)}.hcstate.crashed{color:var(--red)}.hcstate.starting,.hcstate.stopping{color:var(--warn)}
47
- .hcfile{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
48
- .hcurl{color:var(--accent);text-decoration:none;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
49
- .hcurl:hover{text-decoration:underline}
50
- .hcbtns{display:flex;flex-wrap:wrap;gap:var(--s1);margin-top:var(--s1)}
56
+ .hpico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:3px;font-size:13px;line-height:1;font-style:normal;object-fit:contain}
57
+ /* the processes: the activity page's table (activity.css actable, acgrp,
58
+ acname, acrepo), a line per process tinted by its state, the group's name
59
+ at the top of its group, the label cut to its narrow column, then one
60
+ column per kind of button so they align across lines. The container query
61
+ drops the url, the version and the pid first, so a phone never scrolls
62
+ sideways */
63
+ .gpwrap{container-type:inline-size;font-size:13px}
64
+ .c-gpname{width:180px}.c-gpver{width:56px}.c-gppid{width:64px}.c-gpstate{width:110px}
65
+ .c-gpb.take{width:78px}.c-gpb.stop,.c-gpb.restart{width:24px}
66
+ .gptable td{vertical-align:middle}
67
+ .gptable td.acname{vertical-align:top}
68
+ .gpcore{color:var(--ink)}
69
+ .gptag{flex:none;padding:0 var(--s1);border:1px solid var(--line);border-radius:3px;font-size:9px;font-weight:600;line-height:13px;letter-spacing:.06em;text-transform:uppercase;color:var(--dim)}
70
+ .gpver{color:var(--dim);font-size:12px;font-variant-numeric:tabular-nums}
71
+ .gppid{color:var(--dim);font-size:12px;font-variant-numeric:tabular-nums}
72
+ .gpcopy{padding:0;border:0;background:none;color:inherit;font:inherit;cursor:pointer}
73
+ .gpcopy:hover{color:var(--ink);text-decoration:underline dotted}
74
+ .gpcopy.on{color:var(--ok)}
75
+ .gpline.up td:not(.acname){background:var(--okbg)}
76
+ .gpline.crashed td:not(.acname){background:var(--errbg)}
77
+ .gplabel{display:flex;align-items:center;gap:var(--s1);min-width:0;cursor:default}
78
+ .gplabel .hdot{flex:none;margin-right:0}
79
+ .gplabel span{overflow:hidden;text-overflow:ellipsis}
80
+ .hdot.unknown{background:var(--line)}
81
+ .gphost a{color:var(--accent);text-decoration:underline;text-underline-offset:2px;font-size:12px}
82
+ .gphost span{color:var(--dim);font-size:12px}
83
+ .gpstate{color:var(--dim);font-size:12px;font-variant-numeric:tabular-nums}
84
+ .gpstate.up{color:var(--ok)}.gpstate.crashed{color:var(--red)}.gpstate.starting{color:var(--warn)}
85
+ .gptable td.gpb{padding-left:0;padding-right:var(--s1);text-align:right;overflow:visible}
86
+ .gptable td.gpb.stop,.gptable td.gpb.restart{padding:0;text-align:center}
87
+ /* a small icon button: a process's start or stop, its restart, the open before its url */
88
+ .gpi{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:0;border-radius:3px;background:none;color:var(--dim);cursor:pointer;vertical-align:middle}
89
+ .gpi .hico{width:14px;height:14px;color:inherit}
90
+ .gpi:hover:not(:disabled){color:var(--ink);background:var(--panel2)}
91
+ .gpi.go{color:var(--ok)}
92
+ .gpi:disabled{opacity:.35;cursor:default}
93
+ .gphost .gpi{margin-right:var(--s1)}
94
+ /* the app button while its app builds: a spinner, never faded */
95
+ .gpi[aria-busy]:disabled{opacity:1;cursor:progress}
96
+ .gpb button.hsm{padding:0 var(--s1);font-size:11px}
97
+ .gpb.act{white-space:nowrap}
98
+ .gpb.act button + button{margin-left:var(--s1)}
99
+ .gphead div{display:flex;align-items:center;gap:var(--s1)}
100
+ .gphead .sp{flex:1}
101
+ .gphead button.hsm{padding:0 var(--s1);font-size:11px}
102
+ /* what a process has to say (its note, a refused take over): a mark after
103
+ its state words, the words on hover, never a line of its own */
104
+ .gpwarn{display:inline-block;vertical-align:middle;width:6px;height:6px;margin-left:var(--s1);border-radius:1px;background:var(--warn)}
105
+ .gpstate span[title]{cursor:help}
106
+ @container (max-width:620px){.c-gphost,.gphost,.c-gpver,.gpver,.c-gppid,.gppid{display:none}.gptable .c-name{width:96px}.c-gpname{width:110px}.c-gpstate{width:88px}}
107
+ /* a phone: the state words go too, the dot and the tint still say it, and
108
+ only start (stop) stays, the label taking what is left */
109
+ @container (max-width:440px){.c-gpstate,.gpstate,.c-gpb.act,.gpb.act,.c-gpb.take,.gpb.take,.c-gpb.restart,.gpb.restart{display:none}.gptable .c-name{width:64px}.c-gpname{width:auto}}
51
110
  .hsec{min-width:0}
52
111
  .hsec h2{display:flex;align-items:center;gap:var(--s2);height:28px;margin:0 0 var(--s2);padding-bottom:var(--s1);border-bottom:1px solid var(--line);
53
112
  font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);font-weight:600}
@@ -63,8 +122,7 @@
63
122
  .hnote{font-size:12px;color:var(--warn)}
64
123
  /* a filled bar any widget can draw: <span class="wbar" style="--p:51%"><i></i></span>,
65
124
  green below 70%, `mid` from 70%, `hi` from 90% (the widget picks the class) */
66
- .wbar{display:inline-block;vertical-align:middle;width:84px;height:7px;margin-right:var(--s2);border-radius:4px;background:var(--line);overflow:hidden}
125
+ .wbar{display:inline-block;vertical-align:middle;width:40px;height:7px;margin-right:var(--s2);border-radius:4px;background:var(--line);overflow:hidden}
67
126
  .wbar > i{display:block;height:100%;width:var(--p,0%);background:var(--ok)}
68
127
  .wbar.mid > i{background:var(--warn)}
69
128
  .wbar.hi > i{background:var(--red)}
70
- .hcard .hrow{border-bottom:0;padding:var(--s0) 0}