rvlib-shipkit 1.1.1 → 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.
- package/CHANGELOG.md +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- 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/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -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 +235 -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 +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -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 +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -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/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- 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 +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- 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 +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- 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 +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- 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/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- 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/sidebar.tsx +84 -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 +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- 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 +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -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/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// THE ADDRESS of what is on screen, both ways, and nothing else. Pure on
|
|
2
|
-
// purpose: the page's selection rules are worth pinning, and
|
|
2
|
+
// purpose: the page's selection rules are worth pinning, and RepoSt cannot be
|
|
3
3
|
// instantiated without a browser.
|
|
4
4
|
//
|
|
5
5
|
// The media forms are older than the rest and stay byte for byte what they
|
|
@@ -20,9 +20,11 @@ export type HashSel =
|
|
|
20
20
|
/** where the test checks' time goes: every one of them (check null), or
|
|
21
21
|
* one test check (`test`, `test:<unit>`) */
|
|
22
22
|
| { t: 'tests'; check: string | null }
|
|
23
|
+
/** what administers the repo: archive, reload its hub, keep it active */
|
|
24
|
+
| { t: 'admin' }
|
|
23
25
|
|
|
24
26
|
export type ParsedHash = {
|
|
25
|
-
kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'none'
|
|
27
|
+
kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'admin' | 'none'
|
|
26
28
|
/** the collection, for an item. null for everything else */
|
|
27
29
|
col: string | null
|
|
28
30
|
id: string
|
|
@@ -37,6 +39,7 @@ const CAT = 'cat-'
|
|
|
37
39
|
/** prefixed, so no asset id can shadow it */
|
|
38
40
|
const STATUS = 'page:status'
|
|
39
41
|
const TESTS = 'page:tests'
|
|
42
|
+
const ADMIN = 'page:admin'
|
|
40
43
|
|
|
41
44
|
/** what this selection puts in the url. null only for no selection at all,
|
|
42
45
|
* and then the hash is left alone rather than wiped */
|
|
@@ -49,6 +52,7 @@ export function hashFor(sel: HashSel | null): string | null {
|
|
|
49
52
|
case 'item': return `${ITEM}${sel.col}:${sel.id}`
|
|
50
53
|
case 'status': return STATUS
|
|
51
54
|
case 'tests': return sel.check == null ? TESTS : `${TESTS}:${sel.check}`
|
|
55
|
+
case 'admin': return ADMIN
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
|
|
@@ -57,6 +61,7 @@ export function parseHash(raw: string): ParsedHash {
|
|
|
57
61
|
if (raw === '') return none
|
|
58
62
|
if (raw === STATUS) return { kind: 'status', col: null, id: '', tools: null }
|
|
59
63
|
if (raw === TESTS) return { kind: 'tests', col: null, id: '', tools: null }
|
|
64
|
+
if (raw === ADMIN) return { kind: 'admin', col: null, id: '', tools: null }
|
|
60
65
|
// a check id carries colons of its own (`test:rvlib-shipkit`)
|
|
61
66
|
if (raw.startsWith(`${TESTS}:`)) {
|
|
62
67
|
const check = raw.slice(TESTS.length + 1)
|
|
@@ -98,6 +103,7 @@ export function selOf(p: ParsedHash): HashSel | null {
|
|
|
98
103
|
case 'item': return { t: 'item', col: p.col!, id: p.id }
|
|
99
104
|
case 'status': return { t: 'status' }
|
|
100
105
|
case 'tests': return { t: 'tests', check: p.id === '' ? null : p.id }
|
|
106
|
+
case 'admin': return { t: 'admin' }
|
|
101
107
|
}
|
|
102
108
|
}
|
|
103
109
|
|
|
@@ -1,54 +1,65 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// (
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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).
|
|
7
9
|
import { observer } from 'mobx-react-lite'
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { openWikis, WIKIS } from './wikis-view.tsx'
|
|
11
|
-
import type { HubSt } from './hub-st.ts'
|
|
12
|
-
import { byWords, TimelineChart } from './timeline-chart.tsx'
|
|
10
|
+
import { Icon, ICON } from './icons.tsx'
|
|
11
|
+
import type { RepoSt } from './repo-st.ts'
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const out: CorePage[] = []
|
|
23
|
-
if (has('checks')) out.push({ id: 'checks', label: 'checks', on: onCol('checks'), go: () => app.showCol('checks') })
|
|
24
|
-
// a page, never a resource: every hub keeps a run record, so it is
|
|
25
|
-
// always here
|
|
26
|
-
out.push({ id: 'runs', label: 'runs', on: onCol('runs'), go: () => app.cols.select('runs', RUNS_ID) })
|
|
27
|
-
if (has('asks')) out.push({ id: 'asks', label: 'asks', on: onCol('asks'), go: () => app.showCol('asks') })
|
|
28
|
-
const firstCat = app.media.assets[0]?.cat
|
|
29
|
-
if (firstCat != null) out.push({ id: 'media', label: 'media', on: sel?.t === 'asset' || sel?.t === 'cat', go: () => app.showCat(firstCat) })
|
|
30
|
-
if (has(WIKIS)) out.push({ id: WIKIS, label: 'wikis', on: onCol(WIKIS), go: () => openWikis(app) })
|
|
31
|
-
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'
|
|
32
21
|
}
|
|
33
22
|
|
|
34
23
|
/** what is running, in one line: the hub's own run first, since it says the
|
|
35
|
-
* most, else the newest run the record has in flight
|
|
36
|
-
|
|
24
|
+
* most, else the newest run the record has in flight. Cut to its width, the
|
|
25
|
+
* whole line on hover */
|
|
26
|
+
const RunState = observer(({ app }: { app: RepoSt }) => {
|
|
37
27
|
const own = app.checks.strip
|
|
38
28
|
const run = app.live.view?.runs[0]
|
|
39
29
|
const text = own !== '' ? own : run != null ? `${run.label} · ${run.done} of ${run.total}${run.reds > 0 ? ` · ${run.reds} red so far` : ''}` : 'idle'
|
|
40
30
|
const going = app.checks.busy || run != null
|
|
41
31
|
return (
|
|
42
32
|
<span className={`hstate${going ? ' on' : ''}`} title={text}>
|
|
43
|
-
{
|
|
33
|
+
{/* the spinner's slot is always there, so the text never moves */}
|
|
34
|
+
<span className={`spin${going ? '' : ' idle'}`} />
|
|
44
35
|
<span className="htext">{text}</span>
|
|
45
36
|
</span>
|
|
46
37
|
)
|
|
47
38
|
})
|
|
48
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
|
+
|
|
49
60
|
/** what is wrong with the hub itself, one chip per problem. Nothing when all
|
|
50
|
-
* is well: the
|
|
51
|
-
const HubStatus = observer(({ app }: { app:
|
|
61
|
+
* is well: the bar states trouble, never health */
|
|
62
|
+
const HubStatus = observer(({ app }: { app: RepoSt }) => (
|
|
52
63
|
<>
|
|
53
64
|
{app.cols.hubStatus.map((p) => (
|
|
54
65
|
<span key={p.id} className={`hstatus ${p.severity === 'red' ? 'red' : 'warn'}`} title={p.text}>{p.text}</span>
|
|
@@ -56,73 +67,69 @@ const HubStatus = observer(({ app }: { app: HubSt }) => (
|
|
|
56
67
|
</>
|
|
57
68
|
))
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
83
|
return (
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
<button key={p.id} className={p.on ? 'on' : ''} onClick={p.go}>{p.label}</button>
|
|
67
|
-
))}
|
|
68
|
-
</nav>
|
|
69
|
-
<span className="sp" />
|
|
70
|
-
<HubStatus app={app} />
|
|
71
|
-
<RunState app={app} />
|
|
72
|
-
<button className="primary" disabled={u.busy} onClick={() => void u.run()}>run the health pass</button>
|
|
73
|
-
<button disabled={u.busy} onClick={() => void u.run(undefined, { gate: true, label: 'the gate' })}>run the gate</button>
|
|
74
|
-
</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
|
+
</>
|
|
75
88
|
)
|
|
76
89
|
})
|
|
77
90
|
|
|
78
|
-
/** the
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
))
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
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
|
+
))
|
|
92
112
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
const waits = v == null ? '' : [
|
|
99
|
-
v.queued > 0 ? `${v.queued} queued for a free slot` : '',
|
|
100
|
-
...v.blocked.map((w) => `${w.id} waits on ${w.deps.join(', ')}`),
|
|
101
|
-
].filter((x) => x !== '').join('\n')
|
|
102
|
-
const who = run == null ? 'nothing running' : `${run.label}`
|
|
103
|
-
const chart = (
|
|
104
|
-
<TimelineChart view={l.chart} compact={!l.open} repos={v?.repos ?? []} follow zoomable={l.open}
|
|
105
|
-
onBar={(b) => app.cols.select('checks', `check:${b.id}`)}
|
|
106
|
-
onRepo={(r) => void l.openRepo(r.repo)} />
|
|
107
|
-
)
|
|
113
|
+
/** one switch per process the repo declares (serve.processes): its dot, its
|
|
114
|
+
* id, a click starts or stops it, and a link once it serves */
|
|
115
|
+
export const ProcessChips = observer(({ app }: { app: RepoSt }) => {
|
|
116
|
+
const s = app.processes
|
|
117
|
+
if (s.list.length === 0) return null
|
|
108
118
|
return (
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
</div>
|
|
125
|
-
{l.open ? <div className="lopen">{chart}</div> : null}
|
|
126
|
-
</div>
|
|
119
|
+
<span className="hprocs">
|
|
120
|
+
{s.list.map((p) => {
|
|
121
|
+
const on = p.status === 'up' || p.status === 'starting'
|
|
122
|
+
const title = [p.cmd, p.note, p.external ? 'started outside the hub' : null, p.tail.at(-1)].filter((x) => x != null && x !== '').join('\n')
|
|
123
|
+
return (
|
|
124
|
+
<span key={p.id} className="hproc">
|
|
125
|
+
<button className={`ghost ${p.status}`} title={title} disabled={p.status === 'stopping' || (on && p.external)}
|
|
126
|
+
onClick={() => void s.act(p.id, on ? 'stop' : 'start')}>
|
|
127
|
+
<i className={`pdot ${p.status}`} />{p.id}
|
|
128
|
+
</button>
|
|
129
|
+
{p.url != null && p.status === 'up' ? <a href={p.url} target="_blank" rel="noreferrer" title={p.url}>↗</a> : null}
|
|
130
|
+
</span>
|
|
131
|
+
)
|
|
132
|
+
})}
|
|
133
|
+
</span>
|
|
127
134
|
)
|
|
128
135
|
})
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
// every check has its own page (checks-svc.ts owns the run itself). This card
|
|
6
6
|
// exists so the whole text of a run stays reachable.
|
|
7
7
|
import { makeAutoObservable, runInAction } from 'mobx'
|
|
8
|
-
import type {
|
|
8
|
+
import type { RepoSt } from './repo-st.ts'
|
|
9
9
|
|
|
10
10
|
export class HealthSvc {
|
|
11
11
|
statusMd: string | null = null
|
|
12
12
|
/** the lines of the run in flight, newest last */
|
|
13
13
|
live: string[] = []
|
|
14
14
|
|
|
15
|
-
constructor(private st:
|
|
15
|
+
constructor(private st: RepoSt) {
|
|
16
16
|
makeAutoObservable<this, 'st'>(this, { st: false })
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -25,7 +25,7 @@ export class HealthSvc {
|
|
|
25
25
|
|
|
26
26
|
async load(): Promise<void> {
|
|
27
27
|
try {
|
|
28
|
-
const s = (await (await fetch('/api/status.json')).json()) as { statusMd: string | null }
|
|
28
|
+
const s = (await (await fetch(this.st.url('/api/status.json'))).json()) as { statusMd: string | null }
|
|
29
29
|
runInAction(() => { this.statusMd = s.statusMd })
|
|
30
30
|
} catch (e) {
|
|
31
31
|
runInAction(() => { this.statusMd = `FAIL status fetch broke: ${(e as Error).message}` })
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// THE help affordance, once, for every surface of the hub: a small `(?)` that
|
|
2
|
-
// shows one sentence
|
|
3
|
-
//
|
|
2
|
+
// shows one sentence when it is hovered or focused, placed by floating-ui
|
|
3
|
+
// (floating.tsx) above it, or wherever it fits.
|
|
4
4
|
//
|
|
5
5
|
// A term lives in TERMS rather than in the component that shows it, so the
|
|
6
6
|
// same word is explained the same way wherever it appears.
|
|
7
7
|
|
|
8
|
+
import { Tip } from './floating.tsx'
|
|
9
|
+
|
|
8
10
|
export const TERMS: Record<string, string> = {
|
|
9
11
|
always: 'it runs on every gate and every health pass',
|
|
10
12
|
release: 'it runs on the release path only: the full gate and the health pass run it, the quick gate does not',
|
|
@@ -23,8 +25,9 @@ export const TERMS: Record<string, string> = {
|
|
|
23
25
|
|
|
24
26
|
/** one sentence, either given here or named by its term. `text` takes a node
|
|
25
27
|
* so a legend can ride in the same bubble as a sentence */
|
|
26
|
-
export const Help = ({ text, term }: { text?: React.ReactNode; term?: string }): React.ReactElement =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
export const Help = ({ text, term }: { text?: React.ReactNode; term?: string }): React.ReactElement => {
|
|
29
|
+
const c = text ?? (term != null ? TERMS[term] ?? term : '')
|
|
30
|
+
// the sentence also rides inside the glyph, out of sight, so a screen
|
|
31
|
+
// reader and a search of the page find it without a hover
|
|
32
|
+
return <Tip className="hlp" tabIndex={0} tipClass="hbub" content={c}>?<span className="vh">{c}</span></Tip>
|
|
33
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
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', '/activity' and
|
|
3
|
+
// '/r/<repo>/': the machine, the project list, the plan, the activity, and one RepoSt per repo visited, built on
|
|
4
|
+
// the first visit and kept for the life of the page (same name, same
|
|
5
|
+
// instance), so a repo you come back to shows what it showed. Only the shown
|
|
6
|
+
// repo polls. Every other repo whose hub is up is read ahead in the
|
|
7
|
+
// background, one at a time, so its first visit has its data at once. A hub serving on its own is the same tree with no machine and
|
|
8
|
+
// one repo at base ''.
|
|
9
|
+
//
|
|
10
|
+
// the one constraint a reader breaks first: the url and the tree move
|
|
11
|
+
// together, and only `show` moves the page. A list click, a link, back,
|
|
12
|
+
// forward and a hand edited address all land there.
|
|
13
|
+
import { makeAutoObservable, observableShallow, reaction, type IReactionDisposer } from 'mobx'
|
|
14
|
+
import { currentHash, hashFor } from './hash-route.ts'
|
|
15
|
+
import { MachineSt, type MachineOpts } from './machine-st.ts'
|
|
16
|
+
import { LayoutSt } from './layout-st.ts'
|
|
17
|
+
import { DockSt } from './dock-st.ts'
|
|
18
|
+
import { PaletteSt } from './palette-st.ts'
|
|
19
|
+
import { PlannedSt } from './planned-st.ts'
|
|
20
|
+
import { ActivitySt } from './activity-st.ts'
|
|
21
|
+
import { ProjectsSvc } from './projects-svc.ts'
|
|
22
|
+
import { parseFilter, RepoSt } from './repo-st.ts'
|
|
23
|
+
|
|
24
|
+
/** the longest one repo's read ahead holds the next one back */
|
|
25
|
+
export const PREFETCH_WAIT_MS = 10_000
|
|
26
|
+
|
|
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
|
|
32
|
+
|
|
33
|
+
/** where a repo's hub answers on the home */
|
|
34
|
+
export const repoBase = (name: string): string => `/r/${encodeURIComponent(name)}`
|
|
35
|
+
/** the page of a repo on the home */
|
|
36
|
+
export const repoPage = (name: string): string => `${repoBase(name)}/`
|
|
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
|
+
|
|
42
|
+
/** the page an address on the home names */
|
|
43
|
+
export function routeOf(pathname: string): { page: Page; repo: string | null } {
|
|
44
|
+
const m = /^\/r\/([^/]+)/.exec(pathname)
|
|
45
|
+
if (m != null) return { page: 'repo', repo: decodeURIComponent(m[1]!) }
|
|
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 }
|
|
49
|
+
return { page: 'overview', repo: null }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** a plain left click: anything else keeps the browser's own behaviour
|
|
53
|
+
* (a new tab, a new window, a download) */
|
|
54
|
+
export const plainClick = (e: { button: number; metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; altKey: boolean }): boolean =>
|
|
55
|
+
e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey
|
|
56
|
+
|
|
57
|
+
export type HomeOpts = MachineOpts & {
|
|
58
|
+
/** true on the home's web; false on a hub serving on its own */
|
|
59
|
+
home: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class HomeSt {
|
|
63
|
+
/** the machine, on the home; null on a hub of its own */
|
|
64
|
+
machine: MachineSt | null
|
|
65
|
+
/** the list of every repo on this disk */
|
|
66
|
+
projects: ProjectsSvc
|
|
67
|
+
/** the plan, polled while it is the page */
|
|
68
|
+
planned: PlannedSt
|
|
69
|
+
/** what every repo committed and holds, polled while it is the page */
|
|
70
|
+
activity: ActivitySt
|
|
71
|
+
/** the one picker, cmd+k */
|
|
72
|
+
palette: PaletteSt
|
|
73
|
+
/** the widths of the project list and a repo's sidebar */
|
|
74
|
+
layout: LayoutSt
|
|
75
|
+
/** the run chart docked under the whole page */
|
|
76
|
+
dock: DockSt
|
|
77
|
+
page: Page
|
|
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 */
|
|
81
|
+
selected: string | null = null
|
|
82
|
+
/** every repo visited, by key, kept for the life of the page */
|
|
83
|
+
repos = new Map<string, RepoSt>()
|
|
84
|
+
private disposers: IReactionDisposer[] = []
|
|
85
|
+
/** a read ahead is going: one repo at a time, never two loops */
|
|
86
|
+
private prefetching = false
|
|
87
|
+
private readonly onUrl = (): void => this.fromUrl()
|
|
88
|
+
|
|
89
|
+
constructor(o: HomeOpts) {
|
|
90
|
+
this.projects = new ProjectsSvc()
|
|
91
|
+
this.planned = new PlannedSt(this)
|
|
92
|
+
this.activity = new ActivitySt(this)
|
|
93
|
+
this.palette = new PaletteSt(this)
|
|
94
|
+
this.layout = new LayoutSt()
|
|
95
|
+
this.dock = new DockSt(this)
|
|
96
|
+
this.machine = o.home ? new MachineSt(this, o) : null
|
|
97
|
+
this.page = o.home ? 'overview' : 'repo'
|
|
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 })
|
|
99
|
+
const m = this.machine
|
|
100
|
+
if (m != null) {
|
|
101
|
+
this.projects.onSelect = (name) => this.select(name)
|
|
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)
|
|
106
|
+
this.projects.dotOf = (name) => m.dotOf(name)
|
|
107
|
+
this.projects.warnOf = (name) => m.warnOf(name)
|
|
108
|
+
} else {
|
|
109
|
+
this.projects.onSelect = (name) => void this.openInHome(name)
|
|
110
|
+
const own = this.repo('')
|
|
111
|
+
own.markSeen()
|
|
112
|
+
this.selected = own.key
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** the repo on screen, or null */
|
|
117
|
+
get shown(): RepoSt | null {
|
|
118
|
+
return this.page !== 'repo' || this.selected == null ? null : this.repos.get(this.selected) ?? null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** every repo shown once, in visit order: each keeps its own mounted
|
|
122
|
+
* region. A repo only read ahead has no region yet */
|
|
123
|
+
get visited(): RepoSt[] {
|
|
124
|
+
return [...this.repos.values()].filter((r) => r.seen)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get title(): string {
|
|
128
|
+
if (this.page === 'overview') return 'home'
|
|
129
|
+
if (this.page === 'planned') return 'planned'
|
|
130
|
+
if (this.page === 'activity') return 'activity'
|
|
131
|
+
if (this.page === 'archived') return 'archived'
|
|
132
|
+
const r = this.shown
|
|
133
|
+
return r == null || r.name === '' ? 'hub' : `${r.name} · hub`
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** the repo of this key, built on its first visit and never again */
|
|
137
|
+
repo(key: string): RepoSt {
|
|
138
|
+
let r = this.repos.get(key)
|
|
139
|
+
if (r == null) {
|
|
140
|
+
r = new RepoSt(this, key, this.machine == null ? '' : repoBase(key))
|
|
141
|
+
this.repos.set(key, r)
|
|
142
|
+
// what this browser kept of it paints before its hub answers
|
|
143
|
+
void r.cols.restore()
|
|
144
|
+
}
|
|
145
|
+
return r
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
boot(): void {
|
|
149
|
+
this.machine?.boot()
|
|
150
|
+
this.dock.boot()
|
|
151
|
+
this.disposers.push(reaction(() => this.title, (t) => { document.title = t }, { fireImmediately: true }))
|
|
152
|
+
// back, forward, a pasted link and a hand edited hash all arrive
|
|
153
|
+
// here; fromUrl is idempotent, so the two events firing together
|
|
154
|
+
// for one move cost nothing
|
|
155
|
+
window.addEventListener('popstate', this.onUrl)
|
|
156
|
+
window.addEventListener('hashchange', this.onUrl)
|
|
157
|
+
this.fromUrl()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
dispose(): void {
|
|
161
|
+
for (const d of this.disposers) d()
|
|
162
|
+
this.disposers = []
|
|
163
|
+
window.removeEventListener('popstate', this.onUrl)
|
|
164
|
+
window.removeEventListener('hashchange', this.onUrl)
|
|
165
|
+
this.machine?.stop()
|
|
166
|
+
this.planned.stop()
|
|
167
|
+
this.activity.stop()
|
|
168
|
+
this.dock.dispose()
|
|
169
|
+
for (const r of this.repos.values()) r.dispose()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** a list click, a repo row: that repo, in place, as a history entry */
|
|
173
|
+
select(name: string): void {
|
|
174
|
+
this.show('repo', name, 'push')
|
|
175
|
+
}
|
|
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
|
+
|
|
184
|
+
/** a link to a page of the home: the same page, in place */
|
|
185
|
+
navigate(path: string): void {
|
|
186
|
+
const u = new URL(path, 'http://home.invalid')
|
|
187
|
+
const r = routeOf(u.pathname)
|
|
188
|
+
if (r.page === 'planned') this.planned.query = u.searchParams.get('q') ?? ''
|
|
189
|
+
this.show(r.page, r.repo, 'push')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** the url decides the page: the first load, back, forward and a hand
|
|
193
|
+
* edited address all land here */
|
|
194
|
+
fromUrl(): void {
|
|
195
|
+
if (this.machine == null) {
|
|
196
|
+
this.shown?.applyHash()
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
const r = routeOf(location.pathname)
|
|
200
|
+
// an address from before repo pages had their own path (`/#<repo>`)
|
|
201
|
+
const legacy = r.page === 'overview' ? currentHash() : ''
|
|
202
|
+
if (legacy !== '') return this.show('repo', legacy, 'replace')
|
|
203
|
+
if (r.page === 'planned') this.planned.query = new URLSearchParams(location.search).get('q') ?? ''
|
|
204
|
+
if (r.page === 'repo') this.repo(r.repo!).filter = parseFilter(location.search)
|
|
205
|
+
this.show(r.page, r.repo, 'none')
|
|
206
|
+
this.shown?.applyHash()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** the one place the page changes. A repo left behind pauses and keeps
|
|
210
|
+
* everything; the one shown resumes once its hub answers */
|
|
211
|
+
private show(page: Page, repo: string | null, how: 'push' | 'replace' | 'none'): void {
|
|
212
|
+
const prev = this.shown
|
|
213
|
+
const next = page === 'repo' && repo != null ? this.repo(repo) : null
|
|
214
|
+
// the page already shown is never a second history entry
|
|
215
|
+
if (how === 'push' && page === this.page && next === prev && page !== 'planned') how = 'none'
|
|
216
|
+
if (prev != null && prev !== next) prev.pause()
|
|
217
|
+
next?.markSeen()
|
|
218
|
+
this.page = page
|
|
219
|
+
// a global page is no repo's: the repo picked last stays selected
|
|
220
|
+
if (next != null) this.selected = next.key
|
|
221
|
+
this.projects.selected = this.selected
|
|
222
|
+
this.projects.away = next == null
|
|
223
|
+
this.projects.place = page === 'repo' || page === 'archived' ? null : page
|
|
224
|
+
if (how !== 'none') {
|
|
225
|
+
const to = next != null ? `${repoPage(next.key)}${next.search}${next.hash}` : page === 'planned' ? this.planned.address : page === 'activity' || page === 'archived' ? PLACE_PATH[page] : '/'
|
|
226
|
+
if (how === 'push') history.pushState(null, '', to)
|
|
227
|
+
else history.replaceState(null, '', to)
|
|
228
|
+
}
|
|
229
|
+
if (page === 'planned') this.planned.start()
|
|
230
|
+
else this.planned.stop()
|
|
231
|
+
if (page === 'activity') this.activity.start()
|
|
232
|
+
else this.activity.stop()
|
|
233
|
+
if (next != null && next !== prev && this.machine?.data != null) void this.machine.hub(next.key, 'ensure')
|
|
234
|
+
this.wake()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** the shown repo polls once its hub answers, or at once on a hub of its
|
|
238
|
+
* own. One that started keeps going when its hub goes away, and says so */
|
|
239
|
+
wake(): void {
|
|
240
|
+
const r = this.shown
|
|
241
|
+
if (r == null) return
|
|
242
|
+
if (this.machine == null || r.started || (this.machine.fresh && this.machine.worker(r.key)?.status === 'up')) r.resume()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** read ahead the first screen of every repo whose hub is up, in list
|
|
246
|
+
* order, one at a time, once the repo shown has its own. A repo read
|
|
247
|
+
* once is not read again: it polls when it shows */
|
|
248
|
+
async prefetch(): Promise<void> {
|
|
249
|
+
const m = this.machine
|
|
250
|
+
if (m?.data == null || !m.fresh || this.prefetching) return
|
|
251
|
+
const shown = this.shown
|
|
252
|
+
if (shown != null && !shown.cols.loaded) return
|
|
253
|
+
this.prefetching = true
|
|
254
|
+
try {
|
|
255
|
+
for (const p of this.projects.list) {
|
|
256
|
+
// a repo shown meanwhile has its own first screen to read:
|
|
257
|
+
// it goes first, and the next machine poll resumes this loop
|
|
258
|
+
const now = this.shown
|
|
259
|
+
if (now != null && !now.cols.loaded) return
|
|
260
|
+
if (m.worker(p.name)?.status !== 'up') continue
|
|
261
|
+
const r = this.repo(p.name)
|
|
262
|
+
if (r.started) continue
|
|
263
|
+
// a hub that hangs holds the loop back this long, never forever
|
|
264
|
+
await Promise.race([r.prefetch(), new Promise((ok) => setTimeout(ok, PREFETCH_WAIT_MS))])
|
|
265
|
+
}
|
|
266
|
+
} finally {
|
|
267
|
+
this.prefetching = false
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** a list click on a hub serving on its own: that repo's page on the
|
|
272
|
+
* home when one serves, else the folder in VS Code */
|
|
273
|
+
async openInHome(name: string): Promise<void> {
|
|
274
|
+
const home = await fetch('/api/home-url').then((r) => r.json() as Promise<{ port: number; up: boolean }>).catch(() => null)
|
|
275
|
+
if (home?.up !== true) return this.projects.open(name)
|
|
276
|
+
window.location.href = `${window.location.protocol}//${window.location.hostname}:${home.port}${repoPage(name)}`
|
|
277
|
+
}
|
|
278
|
+
}
|