rvlib-shipkit 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/assets/run-assets.ts +3 -8
  5. package/src/check.ts +20 -6
  6. package/src/checks/SK001-package-json-shape.check.ts +14 -3
  7. package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
  8. package/src/checks/SK004-gitignore-policy.check.ts +11 -3
  9. package/src/checks/SK005-rvlib-versions.check.ts +13 -1
  10. package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
  11. package/src/checks/SK007-github-visibility.check.ts +9 -9
  12. package/src/checks/SK008-reviewer-agent.check.ts +14 -8
  13. package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
  14. package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
  15. package/src/checks/SK015-nested-repo.check.ts +5 -1
  16. package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
  17. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
  18. package/src/checks/SK024-identity-colors.check.ts +5 -0
  19. package/src/checks/SK025-release-drift.check.ts +58 -0
  20. package/src/checks/SK026-asks-autoclose.check.ts +44 -0
  21. package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
  22. package/src/checks/SK028-readme.check.ts +79 -0
  23. package/src/cli/cmd_Assets.ts +1 -1
  24. package/src/cli/cmd_Ci.ts +5 -0
  25. package/src/cli/cmd_CrossRepoActivity.ts +116 -0
  26. package/src/cli/cmd_GlobalProcesses.ts +155 -0
  27. package/src/cli/cmd_Keys.ts +17 -3
  28. package/src/cli/cmd_Overview.ts +98 -0
  29. package/src/cli/cmd_Planned.ts +117 -0
  30. package/src/cli.ts +9 -1
  31. package/src/collections/checks.ts +3 -2
  32. package/src/collections/providers.ts +1 -1
  33. package/src/collections/test-time.ts +10 -1
  34. package/src/core/agent-spawn.ts +46 -0
  35. package/src/core/archived.ts +82 -0
  36. package/src/core/canonical-scripts.ts +12 -3
  37. package/src/core/check-runner.ts +40 -12
  38. package/src/core/discover-checks.ts +2 -0
  39. package/src/core/file-stamp.ts +20 -0
  40. package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
  41. package/src/core/identity-color.ts +1 -1
  42. package/src/core/repo-facts.ts +14 -0
  43. package/src/core/retention.ts +14 -5
  44. package/src/core/retired-checks.ts +7 -0
  45. package/src/core/run-check.ts +6 -2
  46. package/src/core/run-ci.ts +43 -12
  47. package/src/core/run-fix.ts +7 -2
  48. package/src/core/run-fixers.ts +10 -6
  49. package/src/core/wire.ts +11 -0
  50. package/src/fleet/scan-fleet.ts +2 -1
  51. package/src/home/activity-shape.ts +135 -0
  52. package/src/home/activity.ts +249 -0
  53. package/src/home/global-units.ts +3 -2
  54. package/src/home/home-routes.ts +32 -5
  55. package/src/home/pm.ts +37 -4
  56. package/src/home/process-rows.ts +205 -0
  57. package/src/home/resources.ts +35 -8
  58. package/src/home/run-daemon.ts +6 -6
  59. package/src/home/run-pm.ts +1 -0
  60. package/src/home/serve-web.ts +6 -3
  61. package/src/home/supervise.ts +16 -1
  62. package/src/home/widget-text.ts +119 -0
  63. package/src/hooks/run-hooks.ts +20 -3
  64. package/src/ledger/db.ts +1 -1
  65. package/src/ledger/gate-checks.ts +17 -3
  66. package/src/ledger/house-runs.ts +43 -0
  67. package/src/ledger/live-timeline.ts +4 -19
  68. package/src/ledger/run-log.ts +39 -10
  69. package/src/ledger/store.ts +19 -2
  70. package/src/ledger/world-cache.ts +51 -4
  71. package/src/plan/cadence.ts +21 -5
  72. package/src/plan/catch-up.ts +37 -0
  73. package/src/plan/plan-timeline.ts +99 -0
  74. package/src/plan/planner.ts +138 -13
  75. package/src/plan/repo-source.ts +120 -8
  76. package/src/release/run-release.ts +9 -2
  77. package/src/release/unit-resolve.ts +8 -12
  78. package/src/repo-config.ts +11 -3
  79. package/src/serve/house-runs.ts +53 -0
  80. package/src/serve/hub-app/activity-st.ts +168 -0
  81. package/src/serve/hub-app/activity-view.tsx +218 -0
  82. package/src/serve/hub-app/activity.css +78 -0
  83. package/src/serve/hub-app/admin-svc.ts +41 -0
  84. package/src/serve/hub-app/admin-view.tsx +42 -0
  85. package/src/serve/hub-app/chart-view.ts +12 -5
  86. package/src/serve/hub-app/check-words.ts +2 -0
  87. package/src/serve/hub-app/controls.css +32 -0
  88. package/src/serve/hub-app/cycle-button.tsx +36 -0
  89. package/src/serve/hub-app/dock-anim.ts +25 -0
  90. package/src/serve/hub-app/dock-st.ts +329 -0
  91. package/src/serve/hub-app/dock-view.tsx +68 -0
  92. package/src/serve/hub-app/dock.css +35 -0
  93. package/src/serve/hub-app/hash-route.ts +7 -1
  94. package/src/serve/hub-app/header-bar.tsx +79 -104
  95. package/src/serve/hub-app/home-st.ts +49 -12
  96. package/src/serve/hub-app/home-strip.css +65 -22
  97. package/src/serve/hub-app/home-strip.tsx +228 -97
  98. package/src/serve/hub-app/home.html +4 -0
  99. package/src/serve/hub-app/hub-app.tsx +96 -160
  100. package/src/serve/hub-app/hub.css +77 -37
  101. package/src/serve/hub-app/icons.tsx +23 -3
  102. package/src/serve/hub-app/index.html +4 -0
  103. package/src/serve/hub-app/keys.ts +42 -7
  104. package/src/serve/hub-app/live-svc.ts +24 -64
  105. package/src/serve/hub-app/machine-st.ts +64 -1
  106. package/src/serve/hub-app/md.ts +38 -5
  107. package/src/serve/hub-app/next-run.ts +1 -1
  108. package/src/serve/hub-app/palette-st.ts +11 -7
  109. package/src/serve/hub-app/palette-view.tsx +47 -0
  110. package/src/serve/hub-app/pill-row.tsx +65 -0
  111. package/src/serve/hub-app/planned-st.ts +21 -85
  112. package/src/serve/hub-app/planned-view.tsx +12 -7
  113. package/src/serve/hub-app/planned.css +5 -2
  114. package/src/serve/hub-app/project-list.css +43 -21
  115. package/src/serve/hub-app/project-list.tsx +122 -23
  116. package/src/serve/hub-app/projects-svc.ts +120 -12
  117. package/src/serve/hub-app/readme-svc.ts +38 -0
  118. package/src/serve/hub-app/readme-view.tsx +19 -0
  119. package/src/serve/hub-app/readme.css +11 -0
  120. package/src/serve/hub-app/repo-sections.tsx +61 -0
  121. package/src/serve/hub-app/repo-st.ts +25 -6
  122. package/src/serve/hub-app/runs-tabs.ts +1 -1
  123. package/src/serve/hub-app/sidebar.tsx +84 -0
  124. package/src/serve/hub-app/tests-view.tsx +31 -4
  125. package/src/serve/hub-app/timeline-chart.tsx +31 -27
  126. package/src/serve/hub-app/timeline-geometry.ts +40 -5
  127. package/src/serve/hub-app/timeline-svc.ts +2 -2
  128. package/src/serve/hub-app/timeline-view.tsx +20 -3
  129. package/src/serve/hub-runtime.ts +17 -9
  130. package/src/serve/other-repos.ts +8 -45
  131. package/src/serve/project-gutter-route.ts +100 -20
  132. package/src/serve/project-gutter.ts +33 -11
  133. package/src/serve/readme-route.ts +57 -0
  134. package/src/serve/serve-hub.ts +13 -8
  135. package/src/serve/tracked-watch.ts +33 -1
  136. package/src/shipkit-files.ts +20 -4
  137. package/src/checks/SK023-stores-under-cap.check.ts +0 -49
  138. package/src/checks/contracts.check.ts +0 -20
@@ -1,23 +1,32 @@
1
- // the home's strip over every page it serves: one quiet line. On a repo page
2
- // the shown repo (its icon, its name in its own colour, the two swatches that
3
- // set those colours); on the overview and the plan, the page's name. On the
4
- // right, only machine wide trouble, and only while it lasts (pm or the daemon
5
- // down, a child or a global process crashed). The places themselves are the
6
- // two buttons at the top of the project list (⌘J, ⌘P) and the first two
7
- // entries of the picker.
8
- // What belongs to one repo (its hub state, restart, the active flag, VS
9
- // Code, modernize) is RepoControls, in that repo's own header. NotUp stands
10
- // in for the hub page while its hub cannot answer, with the fix. Every link
11
- // of the home is a page of the same app: a plain click moves it in place.
1
+ // the home's strip over every page it serves: one line that never wraps. On a
2
+ // repo page the shown repo (its icon, its name in its own colour, the two
3
+ // swatches that set those colours), what it runs now and its two runs, then
4
+ // on the right its processes, its hub's trouble and its editor icons
5
+ // (header-bar.tsx); on the overview and the plan, the page's name. At the
6
+ // far right, only machine wide trouble, and only while it lasts (pm or the
7
+ // daemon down, a child or a global process crashed). The places themselves
8
+ // are the place buttons at the top of the project list (⌘J, ⌘L, ⌘', the
9
+ // picker's ⌘K between the first two) and the first three entries of the
10
+ // picker. What administers one repo (reload its hub, keep it active, archive
11
+ // it) is its admin page (admin-view.tsx). NotUp stands in for the hub page
12
+ // while its hub cannot answer, with the fix. Every link of the home is a
13
+ // page of the same app: a plain click moves it in place. The daemon or pm
14
+ // down is said in MachineBar, a reserved line the strip brings with it,
15
+ // drawn last in the page column (its flex order).
12
16
  import { observer } from 'mobx-react-lite'
13
17
  import type { ReactNode } from 'react'
14
- import type { ResourceView } from '../../home/resources.ts'
18
+ import type { ProcessIcon, ResourceView } from '../../home/resources.ts'
15
19
  import type { WorkerView } from '../../home/hubs.ts'
16
20
  import { NOT_INSTALLED } from '../../home/home-words.ts'
17
- import type { MachineSt } from './machine-st.ts'
18
- import { plainClick, type HomeSt } from './home-st.ts'
21
+ import { splitWidgetHead } from '../../home/widget-text.ts'
22
+ import type { MachineSt, OpenHow } from './machine-st.ts'
23
+ import type { Role } from '../../home/pm.ts'
24
+ import { processIcon, stateWords, type ProcessRow } from '../../home/process-rows.ts'
25
+ import { plainClick, repoPage, type HomeSt } from './home-st.ts'
26
+ import { stampWords } from './time-ago.tsx'
19
27
  import { Icon, ICON } from './icons.tsx'
20
28
  import { Tip } from './floating.tsx'
29
+ import { notUp, RepoOpeners, RepoRun, RepoTools } from './header-bar.tsx'
21
30
 
22
31
  const STATUS_WORD: Record<WorkerView['status'], string> = { up: 'up', starting: 'starting', crashed: 'crashed', down: 'down', stopping: 'stopping' }
23
32
 
@@ -51,40 +60,134 @@ export const ResourceChip = observer(({ m, r }: { m: MachineSt; r: ResourceView
51
60
  )
52
61
  })
53
62
 
54
- /** a global process on the overview, as one card: its switch (its log
55
- * floats on hover), its state in words, the repo and file that declare it,
56
- * its url, then its buttons: restart, take over when a copy started
57
- * elsewhere runs, open in the browser, open as an app window (app: true
58
- * only), its declared actions */
59
- const ProcessCard = observer(({ m, r }: { m: MachineSt; r: ResourceView }) => {
60
- const up = r.status === 'up'
63
+ /** what a card or a group header wears: an emoji, an image, or an empty box of the same size */
64
+ const Mark = ({ i }: { i: ProcessIcon | null }) =>
65
+ i == null ? <i className="hpico" /> : 'text' in i ? <span className="hpico">{i.text}</span> : <img className="hpico" src={i.src} alt="" width={16} height={16} />
66
+
67
+ /** a global process's state in words, on the one clock: `up 2h`, `crashed 3m ago` */
68
+ const StateWords = observer(({ m, r }: { m: MachineSt; r: ProcessRow }) => (
69
+ <span title={r.since == null ? undefined : `since ${stampWords(r.since)}`}>{stateWords(r, m.clock.now)}</span>
70
+ ))
71
+
72
+ /** the button columns a table holds: a kind no line needs takes none */
73
+ type BtnCols = { act: string | null; take: boolean }
74
+
75
+ /** one process as one line, like a commit on the activity page: its dot,
76
+ * icon and label (cut to its column, whole with its log on hover), its url
77
+ * as a link, its version, its pid (a click copies it), its state, then one
78
+ * cell per kind of button, so every start sits under every start: its
79
+ * declared actions, take over, restart, start (stop while it runs), open.
80
+ * One of shipkit's own wears a core tag and has its stop disabled: it is
81
+ * always on, restarted through pm (pm itself by launchd). A running line is
82
+ * tinted green, a crashed one red */
83
+ const ProcessLine = observer(({ m, r, repoIcon, head, cols, span }: { m: MachineSt; r: ProcessRow; repoIcon: string | null; head: ReactNode; cols: BtnCols; span: number }) => {
84
+ const on = r.status === 'up' || r.status === 'starting'
85
+ const about = [r.label, ...(r.core ? [r.description] : [`${r.repo}/${r.file}`, r.description, r.where != null ? `on ${r.where}` : null])].filter((x) => x != null).join(' · ')
86
+ const log = r.tail.length > 0 ? `${about}\n\n${r.tail.join('\n')}` : null
87
+ const via = m.openHow === 'app' ? 'as a shipkit app window' : 'in the default browser'
88
+ const note = r.takeOver?.ok === false ? `cannot take over: ${r.takeOver.why}` : r.note
89
+ const stopTitle = r.core ? `always on: ${r.id === 'pm' ? 'pm is the LaunchAgent com.shipkit.pm, launchd starts it again when it dies' : `restart ${r.label} instead`}`
90
+ : on && !r.canStop ? `${r.label} was started outside pm, so pm cannot stop it` : on ? `stop ${r.label}` : `start ${r.label}`
91
+ const copied = r.pid != null && m.copiedPid === r.pid
61
92
  return (
62
- <section className="hcard">
63
- <div className="hchead">
64
- <ResourceChip m={m} r={r} />
65
- <span className={`hcstate ${r.status}`}>{STATUS_WORD[r.status]}</span>
66
- </div>
67
- <div className="hdim hcfile">{r.repo}/{r.file}</div>
68
- {r.url != null ? (
69
- <a className="hcurl" href={r.url} title={`open ${r.url} in the default browser`} onClick={(e) => {
70
- // the default browser, through pm, never inside the shipkit window
71
- e.preventDefault()
72
- void m.processAct(r.id, 'open')
73
- }}>{r.url.replace(/^https?:\/\//, '')}</a>
74
- ) : null}
75
- {r.description != null || r.where != null ? <div className="hdim">{[r.description, r.where != null ? `on ${r.where}` : null].filter((x) => x != null).join(' · ')}</div> : null}
76
- <div className="hcbtns">
77
- {up && r.canStop ? <button className="ghost" onClick={() => void m.processAct(r.id, 'restart')} title={`stop ${r.label} and start it again`}>restart</button> : null}
78
- {r.takeOver?.ok === true ? <button className="ghost" onClick={() => void m.processAct(r.id, 'takeover')} title={`stop the copy of ${r.label} started elsewhere (the process group listening on its port) and start one pm supervises, in one go`}>take over</button> : null}
79
- {r.url != null ? <button className="ghost" onClick={() => void m.processAct(r.id, 'open')} title={up ? `open ${r.url} in the default browser` : `start ${r.label}, then open ${r.url} in the default browser`}>{up ? 'open' : 'start and open'}</button> : null}
80
- {r.app && r.url != null ? <button className="ghost" onClick={() => void m.processAct(r.id, 'app')} title={up ? `open ${r.url} as a shipkit app window` : `start ${r.label}, then open ${r.url} as a shipkit app window`}>open as app</button> : null}
81
- {r.actions.map((a) => (
82
- <button key={a.id} className="ghost" disabled={r.running != null} onClick={() => void m.processAct(r.id, 'action', a.id)}>{r.running === a.id ? <span className="spin" /> : null}{a.label}</button>
83
- ))}
84
- </div>
85
- {r.takeOver?.ok === false ? <div className="hnote">cannot take over: {r.takeOver.why}</div> : null}
86
- {r.note != null ? <div className="hnote">{r.note}</div> : null}
87
- </section>
93
+ <>
94
+ <tr className={`gpline ${r.dot}`}>
95
+ {head}
96
+ <td className="gpname">
97
+ <Tip className="gplabel" tipClass="hpop" placement="bottom-start" interactive content={log} data-log={log == null ? undefined : 'yes'}>
98
+ <i className={`hdot ${r.dot}`} /><Mark i={processIcon(r, repoIcon)} /><span title={log != null ? undefined : about}>{r.label}</span>
99
+ {r.core ? <b className="gptag" title="built into shipkit: always on">core</b> : null}
100
+ </Tip>
101
+ </td>
102
+ <td className="gphost">
103
+ {r.url == null ? null : r.core ? <span>{r.host}</span> : (
104
+ <a href={r.url} title={`open ${r.url} ${via}`} onClick={(e) => {
105
+ if (!plainClick(e)) return
106
+ e.preventDefault()
107
+ void m.openProcess(r.id)
108
+ }}>{r.host}</a>
109
+ )}
110
+ </td>
111
+ <td className="gpver">{r.version ?? ''}</td>
112
+ <td className="gppid">{r.pid == null ? null : <button className={copied ? 'gpcopy on' : 'gpcopy'} title={`copy pid ${r.pid}`} onClick={() => void m.copyPid(r.pid!)}>{copied ? 'copied' : r.pid}</button>}</td>
113
+ <td className={`gpstate ${r.dot}`}><StateWords m={m} r={r} /></td>
114
+ {cols.act == null ? null : <td className="gpb act">{r.actions.map((a) => (
115
+ <button key={a.id} className="ghost hsm" disabled={r.running != null} onClick={() => void m.processAct(r.id, 'action', a.id)}>{r.running === a.id ? <span className="spin" /> : null}{a.label}</button>
116
+ ))}</td>}
117
+ {!cols.take ? null : <td className="gpb take">{r.takeOver?.ok === true ? <button className="ghost hsm" onClick={() => void m.processAct(r.id, 'takeover')} title={`stop the copy of ${r.label} started elsewhere (the process group listening on its port) and start one pm supervises, in one go`}>take over</button> : null}</td>}
118
+ <td className="gpb restart">{r.canRestart ? <button className="ghost hsm" onClick={() => void (r.core ? m.restartChild(r.id as Role) : m.processAct(r.id, 'restart'))} title={r.core ? `stop the ${r.label} and start it again, through pm` : `stop ${r.label} and start it again`}>restart</button> : null}</td>
119
+ <td className="gpb stop"><button className={on || r.core ? 'ghost' : 'primary'} disabled={r.core || r.status === 'stopping' || r.status === 'unknown' || (on && !r.canStop)}
120
+ title={stopTitle} onClick={() => void m.processAct(r.id, on ? 'stop' : 'start')}>{on || r.core ? 'stop' : 'start'}</button></td>
121
+ <td className="gpb open">{r.url != null && !r.core ? <button className="ghost" onClick={() => void m.openProcess(r.id)} title={r.status === 'up' ? `open ${r.url} ${via}` : `start ${r.label}, then open ${r.url} ${via}`}>open</button> : null}</td>
122
+ </tr>
123
+ {note == null ? null : <tr className={`gpline ${r.dot}`}><td colSpan={span} className="gpnote hnote" title={note}>{note}</td></tr>}
124
+ </>
125
+ )
126
+ })
127
+
128
+ const OPENERS: { how: OpenHow; d: string; title: string }[] = [
129
+ { how: 'browser', d: ICON.browser, title: 'open opens the default browser' },
130
+ { how: 'app', d: ICON.window, title: 'open opens a shipkit app window' },
131
+ ]
132
+
133
+ /** how every open button and url of the table opens, kept per browser */
134
+ const OpenHowPick = observer(({ m }: { m: MachineSt }) => (
135
+ <span className="seg hopen" role="group" aria-label="how open opens">
136
+ {OPENERS.map((o) => (
137
+ <button key={o.how} className={m.openHow === o.how ? 'on' : undefined} aria-pressed={m.openHow === o.how} title={o.title} onClick={() => m.setOpenHow(o.how)}><Icon d={o.d} /></button>
138
+ ))}
139
+ </span>
140
+ ))
141
+
142
+ /** the repo's icon and plain name, as on the activity page; a plain click shows it in place */
143
+ const ProcessRepo = observer(({ app, name, icon }: { app: HomeSt; name: string; icon: string | null }) => (
144
+ <a className="acrepo" href={repoPage(name)} onClick={(e) => {
145
+ if (!plainClick(e)) return
146
+ e.preventDefault()
147
+ app.select(name)
148
+ }}>
149
+ {icon == null ? null : <img src={icon} alt="" width={14} height={14} draggable={false} />}
150
+ <span>{name}</span>
151
+ </a>
152
+ ))
153
+
154
+ /** the processes as one table in the activity page's style: shipkit's own
155
+ * first, then a row group per declaring repo in the project list's order,
156
+ * its name in the first column, at the top of its group, then one line per
157
+ * process. The header row ends with the new window button and the open
158
+ * choice */
159
+ const ProcessTable = observer(({ app, m }: { app: HomeSt; m: MachineSt }) => {
160
+ const groups = m.processGroups
161
+ const rows = groups.flatMap((g) => g.rows)
162
+ // wide enough for the longest set of declared actions, in the button font
163
+ const act = Math.max(0, ...rows.map((r) => r.actions.reduce((n, a) => n + a.label.length + 3, 0)))
164
+ const cols: BtnCols = { act: act === 0 ? null : `${act}ch`, take: rows.some((r) => r.takeOver?.ok === true) }
165
+ const btns = 3 + (cols.act == null ? 0 : 1) + (cols.take ? 1 : 0)
166
+ return (
167
+ <div className="gpwrap">
168
+ <table className="actable gptable">
169
+ <colgroup>
170
+ <col className="c-name" /><col className="c-gpname" /><col className="c-gphost" /><col className="c-gpver" /><col className="c-gppid" /><col className="c-gpstate" />
171
+ {cols.act == null ? null : <col className="c-gpb act" style={{ width: cols.act }} />}
172
+ {cols.take ? <col className="c-gpb take" /> : null}
173
+ <col className="c-gpb restart" /><col className="c-gpb stop" /><col className="c-gpb open" />
174
+ </colgroup>
175
+ <thead><tr>
176
+ <th>repo</th><th>process</th><th className="gphost">url</th><th className="gpver">version</th><th className="gppid">pid</th><th className="gpstate">state</th>
177
+ <th colSpan={btns} className="gphead"><div><button className="ghost hsm" title="open one more window on this page" onClick={() => m.newWindow()}>new window</button><OpenHowPick m={m} /></div></th>
178
+ </tr></thead>
179
+ {groups.map((g) => {
180
+ const icon = g.core ? null : app.projects.list.find((p) => p.name === g.repo)?.icon ?? null
181
+ const lines = g.rows.length + g.rows.filter((r) => r.note != null || r.takeOver?.ok === false).length
182
+ const head = <td rowSpan={lines} className="acname">{g.core ? <span className="acrepo gpcore"><Icon d={ICON.shipkit} /><span>{g.repo}</span></span> : <ProcessRepo app={app} name={g.repo} icon={icon} />}</td>
183
+ return (
184
+ <tbody key={g.core ? ':core' : g.repo} className="acgrp">
185
+ {g.rows.map((r, i) => <ProcessLine key={r.id} m={m} r={r} repoIcon={icon} head={i === 0 ? head : null} cols={cols} span={5 + btns} />)}
186
+ </tbody>
187
+ )
188
+ })}
189
+ </table>
190
+ </div>
88
191
  )
89
192
  })
90
193
 
@@ -94,15 +197,20 @@ const Monitor = observer(({ m }: { m: MachineSt }) => {
94
197
  if (w == null || (w.widgets.length === 0 && w.errors.length === 0)) return null
95
198
  return (
96
199
  <section className="hsec">
97
- <h2><Icon d={ICON.monitor} />monitor</h2>
98
200
  <div className="hmon">
99
- {w.widgets.map((x) => (
100
- <section key={`${x.repo}/${x.id}`} className="hwidget" title={`${x.repo}/${x.file}`}>
101
- <h3>{x.title}<span className="hdim"> · {x.repo}</span></h3>
102
- {x.error != null ? <p className="hnote">{x.error}</p> : null}
103
- {x.html == null ? <p className="twait"><span className="spin" /></p> : <div dangerouslySetInnerHTML={{ __html: x.html }} />}
104
- </section>
105
- ))}
201
+ {w.widgets.map((x) => {
202
+ const { head, body } = splitWidgetHead(x.html ?? '')
203
+ return (
204
+ <section key={`${x.repo}/${x.id}`} className="hwidget" title={`${x.repo}/${x.file}`}>
205
+ <h3>
206
+ {x.title}
207
+ {head == null ? null : <span className="hdim"> · <span dangerouslySetInnerHTML={{ __html: head }} /></span>}
208
+ </h3>
209
+ {x.error != null ? <p className="hnote">{x.error}</p> : null}
210
+ {x.html == null ? <p className="twait"><span className="spin" /></p> : <div dangerouslySetInnerHTML={{ __html: body }} />}
211
+ </section>
212
+ )
213
+ })}
106
214
  </div>
107
215
  {w.errors.map((e) => <p key={e} className="hnote">{e}</p>)}
108
216
  </section>
@@ -152,36 +260,48 @@ export const HomeStrip = observer(({ app }: { app: HomeSt }) => {
152
260
  const m = app.machine
153
261
  if (m == null) return null
154
262
  const name = m.repo
263
+ // the repo's own pieces only while its hub answers: NotUp says why not
264
+ const shown = name == null ? null : app.shown
265
+ const live = shown != null && !notUp(shown) ? shown : null
155
266
  return (
156
267
  <>
157
- {m.down !== '' ? <div className="hdown" role="alert"><b>daemon down</b> {m.down}</div> : null}
158
- {m.pmDown !== '' ? <div className="hdown" role="alert"><b>pm down</b> {m.pmDown}</div> : null}
159
268
  <div className="hstrip">
160
- {name == null ? <span className="hplace">{app.page === 'planned' ? 'planned' : 'overview'}</span> : <ShownRepo app={app} name={name} />}
269
+ {name == null ? <span className="hplace">{app.page === 'overview' ? 'overview' : app.page}</span> : <ShownRepo app={app} name={name} />}
270
+ {live == null ? null : <RepoRun app={live} />}
161
271
  <span className="sp" />
272
+ {live == null ? null : <RepoTools app={live} />}
162
273
  <MachineAlerts m={m} />
274
+ {live == null ? null : <RepoOpeners app={live} />}
163
275
  </div>
276
+ <MachineBar app={app} />
164
277
  </>
165
278
  )
166
279
  })
167
280
 
168
- /** one repo's own controls on the home, in that repo's header: its hub
169
- * state, restart, the active flag, VS Code, and modernize when it is behind */
170
- export const RepoControls = observer(({ app, name }: { app: HomeSt; name: string }) => {
281
+ /** the daemon or pm not answering, on one line at the bottom of the window.
282
+ * The line is always there, empty while all is well, so an alert never
283
+ * moves the page. Only a failed probe sets down or pmDown: before the first
284
+ * answer the line stays empty */
285
+ export const MachineBar = observer(({ app }: { app: HomeSt }) => {
171
286
  const m = app.machine
172
287
  if (m == null) return null
173
- const w = m.worker(name)
174
- const behind = m.data?.behind[name]
175
288
  return (
176
- <span className="hctl">
177
- {w == null || w.status === 'up' ? null : <span className="hstat"><Dot s={w.status} />{STATUS_WORD[w.status]}</span>}
178
- <button className="ghost" onClick={() => void m.hub(name, 'restart')} title="load this repo's hub again, its config read fresh">restart</button>
179
- <label className="hflag" title="an active repo runs the checks the plan holds for it, and gets its pings, even with no agent in it">
180
- <input type="checkbox" checked={m.isActive(name)} onChange={(e) => void m.setActive(name, e.currentTarget.checked)} />active
181
- </label>
182
- <button className="ghost" onClick={() => m.openInCode(name)} title="open the folder in VS Code (or double click its icon)">VS Code</button>
183
- {behind == null ? null : <button className="primary" onClick={() => void m.modernize(name)} title={`this repo runs shipkit ${behind}, the home runs ${m.data?.version}`}>modernize {behind} → {m.data?.version}</button>}
184
- </span>
289
+ <div className="hbar">
290
+ {m.down !== '' ? <span className="hdown" role="alert" title={m.down}><b>daemon down</b> {m.down}</span> : null}
291
+ {m.pmDown !== '' ? <span className="hdown" role="alert" title={m.pmDown}><b>pm down</b> {m.pmDown}</span> : null}
292
+ </div>
293
+ )
294
+ })
295
+
296
+ /** archive the repo or bring it back: `archived` in its repo.config.ts, and
297
+ * only that key. Archived, it stays listed and nothing runs there */
298
+ export const ArchiveButton = observer(({ app, name }: { app: HomeSt; name: string }) => {
299
+ const on = app.projects.isArchived(name)
300
+ return (
301
+ <button className="ghost" onClick={() => void app.projects.setArchived(name, !on)}
302
+ title={on ? 'bring it back: it returns to the project list, and its hub, watch, plan rows, processes and widgets run again' : 'archive it: it leaves the project list for the archived page, and nothing runs there (no hub, no watch, no plan rows, no processes, no widgets). Written to repo.config.ts archived'}>
303
+ {on ? 'unarchive' : 'archive'}
304
+ </button>
185
305
  )
186
306
  })
187
307
 
@@ -192,6 +312,13 @@ export const NotUp = observer(({ app }: { app: HomeSt }) => {
192
312
  if (m == null || name == null) return null
193
313
  const w = m.shown
194
314
  const mod = m.data?.modernize[name]
315
+ if (app.projects.isArchived(name))
316
+ return (
317
+ <div className="hwait">
318
+ <p>{name} is archived: nothing runs there, and only the <HomeLink app={app} to="/archived">archived page</HomeLink> lists it.</p>
319
+ <ArchiveButton app={app} name={name} />
320
+ </div>
321
+ )
195
322
  if (w == null) return <div className="hwait"><p className="twait"><span className="spin" />{m.down !== '' ? `waiting for the daemon to load ${name}…` : `looking for ${name}…`}</p></div>
196
323
  return (
197
324
  <div className="hwait">
@@ -204,41 +331,45 @@ export const NotUp = observer(({ app }: { app: HomeSt }) => {
204
331
  )
205
332
  })
206
333
 
207
- /** the overview at `/`: shipkit's own processes, a card per global process,
208
- * then the widgets. The repos are the project list's, not a section here */
334
+ /** the archived page at `/archived`: the one place an archived repo is
335
+ * listed, each with its way back */
336
+ export const ArchivedView = observer(({ app }: { app: HomeSt }) => {
337
+ const list = app.projects.archived
338
+ return (
339
+ <div className="hall">
340
+ <section className="hsec">
341
+ <h2><Icon d={ICON.archived} />archived</h2>
342
+ {list.length === 0 ? <p className="hdim">no archived repo: a repo is archived from its admin page, or `archived: true` in its repo.config.ts</p> : null}
343
+ {list.map((p) => (
344
+ <div key={p.name} className="hrow">
345
+ <span className="hlink">{p.icon != null ? <img className="hpico" src={p.icon} alt="" width={16} height={16} /> : <i className="hpico" />}{p.name}</span>
346
+ <span className="sp" />
347
+ <button className="ghost" onClick={() => void app.projects.setArchived(p.name, false)} title={`bring ${p.name} back: it returns to the project list, and its hub loads again`}>unarchive</button>
348
+ </div>
349
+ ))}
350
+ </section>
351
+ </div>
352
+ )
353
+ })
354
+
355
+ /** the overview at `/`: the monitor widgets, then one processes table,
356
+ * shipkit's own (pm, the daemon, the web) its first group. The repos are
357
+ * the project list's, not a section here */
209
358
  export const Overview = observer(({ app }: { app: HomeSt }) => {
210
359
  const m = app.machine
211
360
  if (m == null) return null
212
361
  if (m.data == null) return <div className="hwait"><p className="twait"><span className="spin" />{m.down !== '' ? 'the daemon does not answer yet' : 'loading…'}</p></div>
213
- const procs = m.pm?.resources ?? []
214
362
  return (
215
363
  <div className="hall">
216
364
  <div className="hstack">
365
+ <Monitor m={m} />
217
366
  <section className="hsec">
218
- <h2><Icon d={ICON.shipkit} />shipkit<span className="sp" /><button className="ghost" title="open one more window on this page" onClick={() => m.newWindow()}>new window</button></h2>
219
- {m.pm == null ? <p className="hdim">pm does not answer, so its children and the global processes are unknown</p> : (
220
- <div className="hcards"><section className="hcard">
221
- <div className="hrow"><span className="hlink"><Dot s="up" />pm</span><span className="hdim">pid {m.pm.pid} · shipkit {m.pm.version}</span></div>
222
- {m.pm.children.map((c) => (
223
- <div key={c.id} className="hrow">
224
- <span className="hlink"><Dot s={c.status} />{c.id}</span>
225
- <span className="hdim">{STATUS_WORD[c.status]}{c.pid != null ? ` · pid ${c.pid}` : ''}{c.restarts > 0 ? ` · ${c.restarts} restart(s)` : ''}</span>
226
- {c.note != null ? <span className="hnote">{c.note}</span> : null}
227
- <span className="sp" />
228
- <button className="ghost" onClick={() => void m.restartChild(c.id as 'daemon' | 'web')} title={`stop the ${c.id} and start it again, through pm`}>restart</button>
229
- {c.status === 'crashed' ? <Log lines={c.tail} /> : null}
230
- </div>
231
- ))}
232
- </section></div>
233
- )}
234
- </section>
235
- <section className="hsec">
236
- <h2><Icon d={ICON.process} />global processes</h2>
237
- {procs.length === 0 ? <p className="hdim">{m.pm == null ? 'unknown while pm does not answer' : 'none declared: a repo declares one in a <name>.shipkit.ts file, as export default globalProcess({...})'}</p> : null}
238
- <div className="hcards">{procs.map((r) => <ProcessCard key={r.id} m={m} r={r} />)}</div>
367
+ <ProcessTable app={app} m={m} />
368
+ {m.pm != null && m.pm.resources.length === 0 ? <p className="hdim">no global process declared: a repo declares one in a &lt;name&gt;.shipkit.ts file, as export default globalProcess(&#123;...&#125;)</p> : null}
239
369
  {(m.pm?.unitErrors ?? []).map((e) => <p key={e} className="hnote">{e}</p>)}
240
370
  </section>
241
- <Monitor m={m} />
371
+ {app.projects.archived.length === 0 ? null : <p className="hdim"><HomeLink app={app} to="/archived">{app.projects.archived.length} archived repo{app.projects.archived.length === 1 ? '' : 's'}</HomeLink>, listed nowhere else</p>}
372
+ <p className="hdim">in the console: shipkit overview</p>
242
373
  </div>
243
374
  </div>
244
375
  )
@@ -9,6 +9,10 @@
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="./activity.css" />
13
+ <link rel="stylesheet" href="./readme.css" />
14
+ <link rel="stylesheet" href="./dock.css" />
15
+ <link rel="stylesheet" href="./controls.css" />
12
16
  </head>
13
17
  <body data-home>
14
18
  <div id="root"></div>