rvlib-shipkit 1.1.0 → 1.2.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 (137) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/package.json +3 -1
  3. package/src/check.ts +24 -0
  4. package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
  5. package/src/checks/SK023-stores-under-cap.check.ts +49 -0
  6. package/src/checks/SK024-identity-colors.check.ts +49 -0
  7. package/src/cli/cmd_Check.ts +26 -1
  8. package/src/cli/cmd_Ci.ts +18 -5
  9. package/src/cli/cmd_Home.ts +170 -0
  10. package/src/cli.ts +22 -2
  11. package/src/comfy/comfy-panel.ts +1 -16
  12. package/src/core/attach.ts +14 -9
  13. package/src/core/check-file-names.ts +8 -0
  14. package/src/core/check-runner.ts +40 -18
  15. package/src/core/discover-checks.ts +36 -18
  16. package/src/core/identity-color.ts +203 -0
  17. package/src/core/identity.ts +91 -0
  18. package/src/core/job-follow.ts +102 -0
  19. package/src/core/job-run.ts +27 -0
  20. package/src/core/jobs.ts +87 -27
  21. package/src/core/legacy-shapes.ts +1 -14
  22. package/src/core/machine-db.ts +83 -0
  23. package/src/core/one-lib.ts +85 -0
  24. package/src/core/port-holders.ts +43 -0
  25. package/src/core/retention.ts +213 -0
  26. package/src/core/run-check.ts +6 -2
  27. package/src/core/run-ci.ts +3 -0
  28. package/src/core/slots.ts +5 -0
  29. package/src/core/statement-end.ts +17 -0
  30. package/src/home/attach.ts +95 -0
  31. package/src/home/autostart.ts +108 -0
  32. package/src/home/global-units.ts +86 -0
  33. package/src/home/gui-app/ShipkitGui.swift +159 -0
  34. package/src/home/gui-app.ts +99 -0
  35. package/src/home/handoff.ts +51 -0
  36. package/src/home/home-launchd.ts +104 -0
  37. package/src/home/home-paths.ts +88 -0
  38. package/src/home/home-routes.ts +208 -0
  39. package/src/home/home-start.ts +105 -0
  40. package/src/home/home-state.ts +43 -0
  41. package/src/home/home-words.ts +12 -0
  42. package/src/home/hubs.ts +286 -0
  43. package/src/home/lib-watch.ts +139 -0
  44. package/src/home/machine-stores.ts +115 -0
  45. package/src/home/open-app.ts +16 -0
  46. package/src/home/pm-config.ts +37 -0
  47. package/src/home/pm.ts +141 -0
  48. package/src/home/resources.ts +338 -0
  49. package/src/home/run-daemon.ts +137 -0
  50. package/src/home/run-pm.ts +149 -0
  51. package/src/home/run-unit-action.ts +25 -0
  52. package/src/home/run-web.ts +37 -0
  53. package/src/home/serve-daemon.ts +31 -0
  54. package/src/home/serve-web.ts +172 -0
  55. package/src/home/supervise.ts +346 -0
  56. package/src/home/via-hub.ts +142 -0
  57. package/src/home/widgets.ts +102 -0
  58. package/src/ledger/check-stats.ts +19 -45
  59. package/src/ledger/db.ts +69 -17
  60. package/src/ledger/gate-checks.ts +5 -0
  61. package/src/ledger/git-scan.ts +31 -7
  62. package/src/ledger/ledger.ts +40 -14
  63. package/src/ledger/run-log.ts +58 -23
  64. package/src/ledger/store.ts +228 -103
  65. package/src/ledger/world-cache.ts +15 -13
  66. package/src/plan/cadence.ts +237 -0
  67. package/src/plan/planner.ts +303 -0
  68. package/src/plan/repo-source.ts +100 -0
  69. package/src/problems/problem-line.ts +1 -1
  70. package/src/repo-config.ts +51 -2
  71. package/src/serve/hub-app/check-view.tsx +9 -9
  72. package/src/serve/hub-app/check-words.ts +1 -1
  73. package/src/serve/hub-app/checks-svc.ts +12 -8
  74. package/src/serve/hub-app/col-card.tsx +4 -4
  75. package/src/serve/hub-app/cols-svc.ts +92 -13
  76. package/src/serve/hub-app/comfy-svc.ts +12 -6
  77. package/src/serve/hub-app/comfy-view.tsx +3 -3
  78. package/src/serve/hub-app/floating.tsx +49 -0
  79. package/src/serve/hub-app/geometry-view.tsx +4 -4
  80. package/src/serve/hub-app/hash-route.ts +1 -1
  81. package/src/serve/hub-app/header-bar.tsx +44 -12
  82. package/src/serve/hub-app/health-svc.ts +3 -3
  83. package/src/serve/hub-app/help.tsx +10 -7
  84. package/src/serve/hub-app/home-st.ts +241 -0
  85. package/src/serve/hub-app/home-strip.css +70 -0
  86. package/src/serve/hub-app/home-strip.tsx +245 -0
  87. package/src/serve/hub-app/home.html +17 -0
  88. package/src/serve/hub-app/hub-app.tsx +201 -93
  89. package/src/serve/hub-app/hub-types.ts +0 -3
  90. package/src/serve/hub-app/hub.css +43 -23
  91. package/src/serve/hub-app/icons.tsx +14 -0
  92. package/src/serve/hub-app/index.html +3 -1
  93. package/src/serve/hub-app/keys.ts +25 -0
  94. package/src/serve/hub-app/layout-st.ts +55 -0
  95. package/src/serve/hub-app/live-svc.ts +12 -9
  96. package/src/serve/hub-app/machine-st.ts +270 -0
  97. package/src/serve/hub-app/media-svc.ts +63 -44
  98. package/src/serve/hub-app/media-view.tsx +20 -20
  99. package/src/serve/hub-app/next-run.ts +24 -30
  100. package/src/serve/hub-app/palette-st.ts +88 -0
  101. package/src/serve/hub-app/planned-st.ts +171 -0
  102. package/src/serve/hub-app/planned-view.tsx +109 -0
  103. package/src/serve/hub-app/planned.css +39 -0
  104. package/src/serve/hub-app/processes-svc.ts +63 -0
  105. package/src/serve/hub-app/project-list.css +97 -0
  106. package/src/serve/hub-app/project-list.tsx +167 -0
  107. package/src/serve/hub-app/projects-svc.ts +139 -17
  108. package/src/serve/hub-app/prose-hits-st.ts +3 -3
  109. package/src/serve/hub-app/prose-hits-view.tsx +6 -6
  110. package/src/serve/hub-app/{hub-st.ts → repo-st.ts} +155 -84
  111. package/src/serve/hub-app/runs-tabs.ts +1 -1
  112. package/src/serve/hub-app/screen-cache.ts +74 -0
  113. package/src/serve/hub-app/test-view.tsx +4 -4
  114. package/src/serve/hub-app/tests-svc.ts +3 -3
  115. package/src/serve/hub-app/tests-view.tsx +10 -10
  116. package/src/serve/hub-app/timeline-chart.tsx +24 -12
  117. package/src/serve/hub-app/timeline-svc.ts +5 -4
  118. package/src/serve/hub-app/timeline-view.tsx +13 -13
  119. package/src/serve/hub-app/tree-row.tsx +2 -2
  120. package/src/serve/hub-app/tree-svc.ts +2 -2
  121. package/src/serve/hub-app/wikis-view.tsx +10 -6
  122. package/src/serve/hub-runtime.ts +542 -0
  123. package/src/serve/project-gutter-route.ts +21 -0
  124. package/src/serve/project-gutter.ts +3 -2
  125. package/src/serve/repo-processes.ts +89 -0
  126. package/src/serve/run-serve.ts +110 -402
  127. package/src/serve/serve-hub.ts +142 -44
  128. package/src/serve/stale-restart.ts +5 -0
  129. package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
  130. package/src/serve/warm-cell.ts +95 -0
  131. package/src/shipkit-files.ts +114 -0
  132. package/src/shipkit.ts +2 -1
  133. package/src/suite/kept-timings.ts +1 -1
  134. package/src/wiki/graph-positions.ts +1 -1
  135. package/src/serve/health-schedule.ts +0 -102
  136. package/src/serve/hub-app/project-gutter.css +0 -65
  137. package/src/serve/hub-app/project-gutter.tsx +0 -128
@@ -0,0 +1,55 @@
1
+ // the page's two resizable widths, a node of the root (home-st.ts): the
2
+ // project list at the far left and a repo's own sidebar. Each is dragged by
3
+ // its separator (react-resizable-panels) and kept per browser in px, so the
4
+ // next load renders at that width from its first paint. A stored value that
5
+ // is not a number, or is out of bounds, reads as the default: no runtime
6
+ // value can hand the page a list wider than LIST.max.
7
+ import { makeAutoObservable } from 'mobx'
8
+
9
+ export type Bounds = { key: string; min: number; max: number; def: number }
10
+
11
+ /** the project list: icon and name on one line */
12
+ export const LIST: Bounds = { key: 'hub:list-width', min: 140, max: 360, def: 200 }
13
+ /** a repo's sidebar: the resource tree */
14
+ export const SIDEBAR: Bounds = { key: 'hub:sidebar-width', min: 220, max: 640, def: 330 }
15
+
16
+ /** a width in its bounds, or the default for anything else */
17
+ export const clampWidth = (v: unknown, b: Bounds): number => {
18
+ const n = typeof v === 'string' ? Number(v) : typeof v === 'number' ? v : NaN
19
+ return Number.isFinite(n) && n >= b.min && n <= b.max ? Math.round(n) : b.def
20
+ }
21
+
22
+ export function loadWidth(b: Bounds): number {
23
+ try {
24
+ return clampWidth(globalThis.localStorage?.getItem(b.key), b)
25
+ } catch {
26
+ return b.def
27
+ }
28
+ }
29
+
30
+ function saveWidth(b: Bounds, px: number): void {
31
+ try {
32
+ globalThis.localStorage?.setItem(b.key, String(px))
33
+ } catch { /* private window or blocked site data: the width lasts the tab */ }
34
+ }
35
+
36
+ export class LayoutSt {
37
+ listWidth = loadWidth(LIST)
38
+ sidebarWidth = loadWidth(SIDEBAR)
39
+
40
+ constructor() {
41
+ makeAutoObservable(this)
42
+ }
43
+
44
+ /** a drag of the list's separator ended at this width */
45
+ setListWidth(px: number): void {
46
+ this.listWidth = clampWidth(Math.round(px), LIST)
47
+ saveWidth(LIST, this.listWidth)
48
+ }
49
+
50
+ /** a drag of the sidebar's separator ended at this width */
51
+ setSidebarWidth(px: number): void {
52
+ this.sidebarWidth = clampWidth(Math.round(px), SIDEBAR)
53
+ saveWidth(SIDEBAR, this.sidebarWidth)
54
+ }
55
+ }
@@ -10,7 +10,7 @@ import { makeAutoObservable, observableRef, runInAction } from 'mobx'
10
10
  import type { LiveView } from '../../ledger/live-timeline.ts'
11
11
  import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
12
12
  import { ChartView, type ChartSource } from './chart-view.ts'
13
- import type { HubSt } from './hub-st.ts'
13
+ import type { RepoSt } from './repo-st.ts'
14
14
  import { chartHeight } from './timeline-geometry.ts'
15
15
 
16
16
  /** how often the page asks. A strip a second behind the runner is still live */
@@ -75,10 +75,11 @@ export class LiveSvc {
75
75
  chart: ChartView
76
76
  private timer: ReturnType<typeof setInterval> | null = null
77
77
  private inFlight = false
78
+ private started = false
78
79
 
79
- constructor(private st: HubSt) {
80
+ constructor(private st: RepoSt) {
80
81
  this.chart = new ChartView(null, () => liveSource(this.view, this.now, this.open, this.st.timeline.data?.invocations ?? []))
81
- makeAutoObservable<this, 'st' | 'timer' | 'inFlight'>(this, { st: false, timer: false, inFlight: false, view: observableRef, chart: false })
82
+ makeAutoObservable<this, 'st' | 'timer' | 'inFlight' | 'started'>(this, { st: false, timer: false, inFlight: false, started: false, view: observableRef, chart: false })
82
83
  }
83
84
 
84
85
  /** the server clock, as of the page clock's last tick */
@@ -112,13 +113,15 @@ export class LiveSvc {
112
113
  this.held = settleHeight(this.held, Math.min(OPEN_MAX_H, chartHeight(lanes)), now)
113
114
  }
114
115
 
116
+ /** a resume keeps the chart where it was: only the first start homes it */
115
117
  start(): void {
116
118
  if (this.timer != null) return
117
- this.opened()
119
+ if (!this.started) this.opened()
120
+ this.started = true
118
121
  void this.poll()
119
122
  this.timer = setInterval(() => void this.poll(), LIVE_POLL_MS)
120
123
  }
121
- dispose(): void {
124
+ stop(): void {
122
125
  if (this.timer != null) clearInterval(this.timer)
123
126
  this.timer = null
124
127
  }
@@ -128,7 +131,7 @@ export class LiveSvc {
128
131
  if (this.inFlight) return
129
132
  this.inFlight = true
130
133
  try {
131
- const r = await fetch('/api/live.json')
134
+ const r = await fetch(this.st.url('/api/live.json'))
132
135
  if (!r.ok) throw new Error(await r.text())
133
136
  const v = (await r.json()) as LiveView
134
137
  const ended = this.running && v.runs.length === 0
@@ -142,8 +145,8 @@ export class LiveSvc {
142
145
  // wrote new numbers: the rows and the timeline read them now
143
146
  // rather than on the next ten second poll
144
147
  if (ended) {
145
- void this.st.cols.poll()
146
- if (this.st.timeline.data != null) void this.st.timeline.load()
148
+ void this.st.cols.poll(true)
149
+ if (this.st.timeline.data != null) void this.st.timeline.load(true)
147
150
  }
148
151
  } catch (e) {
149
152
  runInAction(() => { this.error = `the live runs could not be read: ${(e as Error).message}` })
@@ -159,7 +162,7 @@ export class LiveSvc {
159
162
  /** a repo lane was clicked: its hub, when it serves */
160
163
  async openRepo(repo: string): Promise<void> {
161
164
  try {
162
- const r = (await (await fetch(`/api/repo-hub?repo=${encodeURIComponent(repo)}`)).json()) as { url: string | null }
165
+ const r = (await (await fetch(this.st.url(`/api/repo-hub?repo=${encodeURIComponent(repo)}`))).json()) as { url: string | null }
163
166
  if (r.url != null) window.open(r.url, '_blank')
164
167
  else this.say(`${repo} has no hub serving`)
165
168
  } catch (e) {
@@ -0,0 +1,270 @@
1
+ // the machine, as a page on the home sees it: every repo's hub state and the
2
+ // active flags from the daemon's /api/home.json, the widgets from its
3
+ // /api/home/widgets.json, the global processes and pm's children from pm's
4
+ // /api/pm/status.json. A node of the page tree
5
+ // (home-st.ts): the root reads it for the list's dots, and says loudly when the
6
+ // daemon behind the page does not answer. A hub serving on its own has none
7
+ // of this (HomeSt.machine is null).
8
+ import { makeAutoObservable, observableRef, runInAction } from 'mobx'
9
+ import type { HomeAnswer, WidgetsAnswer } from '../../home/home-routes.ts'
10
+ import type { WorkerView } from '../../home/hubs.ts'
11
+ import type { ResourceView } from '../../home/resources.ts'
12
+ import type { PmAnswer, Role } from '../../home/pm.ts'
13
+ import type { HomeSt } from './home-st.ts'
14
+ import type { HubDot } from './projects-svc.ts'
15
+
16
+ export const MACHINE_POLL_MS = 1_500
17
+ /** past the web's own wait on the daemon (serve-web.ts MACHINE_ANSWER_MS), so
18
+ * a hung daemon reads in its words, and a hung web still reads as down */
19
+ export const MACHINE_TIMEOUT_MS = 8_000
20
+
21
+ /** where this browser keeps the machine as it last read it, painted at once
22
+ * on the next load (the overview) while the daemon and pm are asked again */
23
+ export const MACHINE_KEY = 'hub:machine'
24
+
25
+ type Kept = { data: HomeAnswer | null; pm: PmAnswer | null; widgets: WidgetsAnswer | null }
26
+
27
+ function loadKept(): Kept | null {
28
+ try {
29
+ const v = JSON.parse(globalThis.localStorage?.getItem(MACHINE_KEY) ?? 'null') as Kept | null
30
+ return v != null && typeof v === 'object' && (v.data == null || Array.isArray(v.data.workers)) ? v : null
31
+ } catch {
32
+ return null
33
+ }
34
+ }
35
+
36
+ /** the poll failed because nothing behind the page answers, not on an error of an answer */
37
+ class DownError extends Error {}
38
+
39
+ export type MachineOpts = { reload?: () => void; timeoutMs?: number }
40
+
41
+ export class MachineSt {
42
+ data: HomeAnswer | null = null
43
+ /** pm's answer: the resources and the daemon and web states */
44
+ pm: PmAnswer | null = null
45
+ /** every house repo's widgets, as the daemon last rendered them */
46
+ widgets: WidgetsAnswer | null = null
47
+ /** why pm did not answer the last poll, or '' while it does */
48
+ pmDown = ''
49
+ /** the last action that failed */
50
+ error = ''
51
+ /** why the daemon did not answer the last poll, or '' while it does */
52
+ down = ''
53
+ /** the daemon answered once since the load: until then data, pm and
54
+ * widgets are what this browser kept, shown but never acted on */
55
+ fresh = false
56
+ /** what was last kept, so an unchanged machine is not written again */
57
+ private kept = ''
58
+ private timer: ReturnType<typeof setInterval> | null = null
59
+ private wasDown = false
60
+ private polling = false
61
+ private widgetsPolling = false
62
+ /** `error` holds a poll's error, which the next good poll clears */
63
+ private pollError = false
64
+
65
+ constructor(private st: HomeSt, private o: MachineOpts = {}) {
66
+ const k = loadKept()
67
+ if (k != null) {
68
+ this.data = k.data
69
+ this.pm = k.pm
70
+ this.widgets = k.widgets
71
+ }
72
+ makeAutoObservable<this, 'st' | 'timer' | 'wasDown' | 'polling' | 'widgetsPolling' | 'pollError' | 'o' | 'kept' | 'keep'>(this, { st: false, timer: false, wasDown: false, polling: false, widgetsPolling: false, pollError: false, o: false, kept: false, keep: false, data: observableRef, pm: observableRef, widgets: observableRef })
73
+ }
74
+
75
+ /** the repo the page shows, null on the overview and the plan */
76
+ get repo(): string | null {
77
+ return this.st.page === 'repo' ? this.st.selected : null
78
+ }
79
+
80
+ worker(name: string | null): WorkerView | null {
81
+ return name == null ? null : this.data?.workers.find((w) => w.repo === name) ?? null
82
+ }
83
+
84
+ get shown(): WorkerView | null {
85
+ return this.worker(this.repo)
86
+ }
87
+
88
+ resource(id: string): ResourceView | null {
89
+ return this.pm?.resources.find((r) => r.id === id) ?? null
90
+ }
91
+
92
+ dotOf(name: string): HubDot {
93
+ const s = this.worker(name)?.status
94
+ return s === 'starting' || s === 'crashed' ? s : null
95
+ }
96
+
97
+ /** the resources this repo uses that are not up, as one line */
98
+ warnOf(name: string): string | null {
99
+ const off = (this.worker(name)?.uses ?? []).filter((id) => this.resource(id)?.status !== 'up')
100
+ return off.length === 0 ? null : `needs ${off.join(', ')}, which ${off.length === 1 ? 'is' : 'are'} off`
101
+ }
102
+
103
+ isActive(name: string): boolean {
104
+ return this.data?.active.includes(name) === true
105
+ }
106
+
107
+ boot(): void {
108
+ void this.load().then(() => {
109
+ // a repo page asks its hub to load when it is down
110
+ if (this.repo != null && this.data != null) void this.hub(this.repo, 'ensure')
111
+ })
112
+ this.timer = setInterval(() => void this.load(), MACHINE_POLL_MS)
113
+ }
114
+
115
+ stop(): void {
116
+ if (this.timer != null) clearInterval(this.timer)
117
+ }
118
+
119
+ /** pm is polled beside the daemon: either one down leaves the other shown */
120
+ private async loadPm(): Promise<void> {
121
+ try {
122
+ const r = await fetch('/api/pm/status.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
123
+ if (!r.ok) throw new Error(await r.text())
124
+ const j = (await r.json()) as PmAnswer
125
+ runInAction(() => {
126
+ this.pm = j
127
+ this.pmDown = ''
128
+ })
129
+ } catch (e) {
130
+ runInAction(() => {
131
+ this.pm = null
132
+ this.pmDown = (e as Error).message
133
+ })
134
+ }
135
+ }
136
+
137
+ /** a widget poll that fails keeps the last html shown: the daemon's own
138
+ * poll says why */
139
+ private async loadWidgets(): Promise<void> {
140
+ try {
141
+ const r = await fetch('/api/home/widgets.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
142
+ if (!r.ok) return
143
+ const j = (await r.json()) as WidgetsAnswer
144
+ runInAction(() => { this.widgets = j })
145
+ } catch {
146
+ // the daemon poll beside it reports a daemon that does not answer
147
+ }
148
+ }
149
+
150
+ async load(): Promise<void> {
151
+ // a stalled poll is not stacked on
152
+ if (this.polling) return
153
+ this.polling = true
154
+ const pm = this.loadPm()
155
+ // never held on: a widget that answers late only shows late
156
+ if (!this.widgetsPolling) {
157
+ this.widgetsPolling = true
158
+ void this.loadWidgets().finally(() => { this.widgetsPolling = false })
159
+ }
160
+ try {
161
+ let r: Response
162
+ try {
163
+ r = await fetch('/api/home.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
164
+ } catch (e) {
165
+ throw new DownError(`the home does not answer: ${(e as Error).message}`)
166
+ }
167
+ if (r.headers.get('x-shipkit-daemon') === 'down') throw new DownError(await r.text())
168
+ if (!r.ok) throw new Error(`/api/home.json answered ${r.status}: ${await r.text()}`)
169
+ const j = (await r.json()) as HomeAnswer
170
+ const back = this.wasDown
171
+ const cleared = this.pollError
172
+ this.wasDown = false
173
+ this.pollError = false
174
+ runInAction(() => {
175
+ this.data = j
176
+ this.down = ''
177
+ this.fresh = true
178
+ if (cleared) this.error = ''
179
+ })
180
+ // the shown repo starts once its hub answers
181
+ this.st.wake()
182
+ // and every other one is read ahead behind it
183
+ void this.st.prefetch()
184
+ // what the page loaded while the daemon was away failed: start over
185
+ if (back) this.reload()
186
+ } catch (e) {
187
+ const msg = (e as Error).message
188
+ if (e instanceof DownError) {
189
+ this.wasDown = true
190
+ runInAction(() => { this.down = msg })
191
+ } else {
192
+ this.pollError = true
193
+ runInAction(() => {
194
+ this.down = ''
195
+ this.error = msg
196
+ })
197
+ }
198
+ } finally {
199
+ await pm
200
+ this.polling = false
201
+ this.keep()
202
+ }
203
+ }
204
+
205
+ /** the machine as read now, kept for the next load. Only a change is written */
206
+ private keep(): void {
207
+ if (!this.fresh) return
208
+ try {
209
+ // the log tails change on every poll and only show on a hover: not kept
210
+ const pm = this.pm == null ? null : { ...this.pm, children: this.pm.children.map((c) => ({ ...c, tail: [] })), resources: this.pm.resources.map((r) => ({ ...r, tail: [] })) }
211
+ const s = JSON.stringify({ data: this.data, pm, widgets: this.widgets } satisfies Kept)
212
+ if (s === this.kept) return
213
+ globalThis.localStorage?.setItem(MACHINE_KEY, s)
214
+ this.kept = s
215
+ } catch { /* private window or blocked site data: the next load waits on the daemon */ }
216
+ }
217
+
218
+ private reload(): void {
219
+ if (this.o.reload != null) this.o.reload()
220
+ else window.location.reload()
221
+ }
222
+
223
+ private async post(path: string, body: unknown): Promise<boolean> {
224
+ try {
225
+ const r = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
226
+ if (!r.ok) throw new Error(await r.text())
227
+ this.pollError = false
228
+ runInAction(() => { this.error = '' })
229
+ void this.load()
230
+ return true
231
+ } catch (e) {
232
+ runInAction(() => { this.error = (e as Error).message })
233
+ return false
234
+ }
235
+ }
236
+
237
+ hub(name: string, act: 'ensure' | 'restart'): Promise<boolean> {
238
+ return this.post('/api/home/hub', { name, act })
239
+ }
240
+
241
+ setActive(name: string, on: boolean): Promise<boolean> {
242
+ return this.post('/api/home/active', { name, on })
243
+ }
244
+
245
+ /** a global process: start, stop, restart, take over the copy started
246
+ * elsewhere, open its url in the default browser or as an app window
247
+ * (started first when it is down), or run one of its actions */
248
+ processAct(id: string, act: 'start' | 'stop' | 'restart' | 'takeover' | 'open' | 'app' | 'action', action?: string): Promise<boolean> {
249
+ return this.post('/api/pm/resource', { id, act, action })
250
+ }
251
+
252
+ /** restart the daemon or the web through pm */
253
+ restartChild(id: Role): Promise<boolean> {
254
+ return this.post('/api/pm/child', { id, act: 'restart' })
255
+ }
256
+
257
+ modernize(name: string): Promise<boolean> {
258
+ return this.post('/api/home/modernize', { name })
259
+ }
260
+
261
+ /** one more window on this page. In a gui window (a Chrome app) a popup
262
+ * opens as its own app window */
263
+ newWindow(): void {
264
+ window.open(`${window.location.origin}${window.location.pathname}`, '_blank', 'popup,width=1400,height=900')
265
+ }
266
+
267
+ openInCode(name: string): void {
268
+ void this.st.projects.open(name)
269
+ }
270
+ }