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.
- package/CHANGELOG.md +67 -0
- package/package.json +3 -1
- package/src/check.ts +24 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
- package/src/checks/SK023-stores-under-cap.check.ts +49 -0
- package/src/checks/SK024-identity-colors.check.ts +49 -0
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +18 -5
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli.ts +22 -2
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/attach.ts +14 -9
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +40 -18
- package/src/core/discover-checks.ts +36 -18
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/retention.ts +213 -0
- package/src/core/run-check.ts +6 -2
- package/src/core/run-ci.ts +3 -0
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +86 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +208 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +141 -0
- package/src/home/resources.ts +338 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +149 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +172 -0
- package/src/home/supervise.ts +346 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widgets.ts +102 -0
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +5 -0
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/run-log.ts +58 -23
- package/src/ledger/store.ts +228 -103
- package/src/ledger/world-cache.ts +15 -13
- package/src/plan/cadence.ts +237 -0
- package/src/plan/planner.ts +303 -0
- package/src/plan/repo-source.ts +100 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/repo-config.ts +51 -2
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +1 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +1 -1
- package/src/serve/hub-app/header-bar.tsx +44 -12
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +241 -0
- package/src/serve/hub-app/home-strip.css +70 -0
- package/src/serve/hub-app/home-strip.tsx +245 -0
- package/src/serve/hub-app/home.html +17 -0
- package/src/serve/hub-app/hub-app.tsx +201 -93
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +43 -23
- package/src/serve/hub-app/icons.tsx +14 -0
- package/src/serve/hub-app/index.html +3 -1
- package/src/serve/hub-app/keys.ts +25 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +12 -9
- package/src/serve/hub-app/machine-st.ts +270 -0
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +88 -0
- package/src/serve/hub-app/planned-st.ts +171 -0
- package/src/serve/hub-app/planned-view.tsx +109 -0
- package/src/serve/hub-app/planned.css +39 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +97 -0
- package/src/serve/hub-app/project-list.tsx +167 -0
- package/src/serve/hub-app/projects-svc.ts +139 -17
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/{hub-st.ts → repo-st.ts} +155 -84
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +10 -10
- package/src/serve/hub-app/timeline-chart.tsx +24 -12
- package/src/serve/hub-app/timeline-svc.ts +5 -4
- package/src/serve/hub-app/timeline-view.tsx +13 -13
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +542 -0
- package/src/serve/project-gutter-route.ts +21 -0
- package/src/serve/project-gutter.ts +3 -2
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +142 -44
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +114 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/project-gutter.css +0 -65
- 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 {
|
|
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:
|
|
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
|
-
|
|
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
|
+
}
|