rvlib-shipkit 1.3.0 → 1.5.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 +105 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +12 -2
- package/src/check.ts +73 -98
- package/src/checks/SK001-package-json-shape.check.ts +26 -4
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +20 -6
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
- package/src/checks/SK028-readme.check.ts +10 -2
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +10 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +4 -3
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +24 -4
- package/src/collections/providers.ts +6 -2
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +63 -25
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +31 -7
- package/src/core/layout.ts +2 -0
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/motherhome.ts +88 -66
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +14 -14
- package/src/core/run-ci.ts +11 -7
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +34 -9
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +30 -6
- package/src/home/home-routes.ts +14 -2
- package/src/home/home-start.ts +33 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +11 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +60 -6
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +115 -61
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/store.ts +11 -0
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/ledger/world-cache.ts +17 -15
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +36 -56
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +4 -4
- package/src/serve/hub-app/admin-view.tsx +13 -11
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +16 -36
- package/src/serve/hub-app/col-tables-view.tsx +131 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +33 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +7 -1
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +15 -3
- package/src/serve/hub-app/home-strip.css +69 -19
- package/src/serve/hub-app/home-strip.tsx +192 -118
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +21 -5
- package/src/serve/hub-app/hub-types.ts +11 -5
- package/src/serve/hub-app/hub.css +88 -44
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +11 -8
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +34 -49
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +38 -0
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/projects-svc.ts +15 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +117 -0
- package/src/serve/hub-app/repo-st.ts +47 -10
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +3 -16
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/warm-cell.ts +49 -16
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
|
@@ -22,6 +22,7 @@ import { serverNow } from './live-svc.ts'
|
|
|
22
22
|
import { ROWS, type RowSize } from './timeline-geometry.ts'
|
|
23
23
|
import { ageWords } from './time-ago.tsx'
|
|
24
24
|
import type { HomeSt } from './home-st.ts'
|
|
25
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
25
26
|
|
|
26
27
|
export type DockSize = 'line' | 'slim' | 'open'
|
|
27
28
|
|
|
@@ -296,7 +297,7 @@ export class DockSt {
|
|
|
296
297
|
if (this.inFlight) return
|
|
297
298
|
this.inFlight = true
|
|
298
299
|
try {
|
|
299
|
-
const r = await
|
|
300
|
+
const r = await reach('/api/house-runs.json')
|
|
300
301
|
if (!r.ok) throw new Error(await r.text())
|
|
301
302
|
const v = (await r.json()) as HouseView
|
|
302
303
|
runInAction(() => {
|
|
@@ -306,7 +307,7 @@ export class DockSt {
|
|
|
306
307
|
this.error = ''
|
|
307
308
|
})
|
|
308
309
|
} catch (e) {
|
|
309
|
-
runInAction(() => { this.error = `the runs could not be read: ${
|
|
310
|
+
runInAction(() => { this.error = ownTrouble(e, this.home.machine, (m) => `the runs could not be read: ${m}`) })
|
|
310
311
|
} finally {
|
|
311
312
|
this.inFlight = false
|
|
312
313
|
}
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// then the status as small pills that fold into +N rather than wrap. The
|
|
5
5
|
// state is dock-st.ts; the height is the page's panel (hub-app.tsx).
|
|
6
6
|
import { observer } from 'mobx-react-lite'
|
|
7
|
+
import { useEffect, useState } from 'react'
|
|
8
|
+
import { Tip } from './floating.tsx'
|
|
9
|
+
import { Icon, ICON } from './icons.tsx'
|
|
7
10
|
import type { HomeSt } from './home-st.ts'
|
|
8
11
|
import { CycleButton } from './cycle-button.tsx'
|
|
9
12
|
import { DOCK_KEY } from './keys.ts'
|
|
@@ -15,6 +18,38 @@ import type { PlacedGroup } from './timeline-geometry.ts'
|
|
|
15
18
|
/** what the dock is, on its size button's hover */
|
|
16
19
|
export const DOCK_HELP = `every check shipkit runs in every repo, whoever started it: a page, an agent in a terminal, the plan. The line, the pills alone; slim, the last two minutes, one group of lanes per repo; open, the last ten, zoomed by the wheel and panned by a drag, its height dragged at the top edge`
|
|
17
20
|
|
|
21
|
+
const COPIED_MS = 1500
|
|
22
|
+
|
|
23
|
+
/** the page's own address, to paste somewhere: the app window has no url bar */
|
|
24
|
+
export const CopyUrl = () => {
|
|
25
|
+
const [href, setHref] = useState(() => globalThis.location?.href ?? '')
|
|
26
|
+
const [said, setSaid] = useState<string | null>(null)
|
|
27
|
+
// the router moves the address with pushState and replaceState, which no
|
|
28
|
+
// event reports: read it when the pointer or the focus arrives
|
|
29
|
+
const sync = () => setHref(location.href)
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (said == null) return
|
|
32
|
+
const t = setTimeout(() => setSaid(null), COPIED_MS)
|
|
33
|
+
return () => clearTimeout(t)
|
|
34
|
+
}, [said])
|
|
35
|
+
const click = async () => {
|
|
36
|
+
try {
|
|
37
|
+
if (globalThis.navigator?.clipboard == null) throw new Error('this page has no clipboard')
|
|
38
|
+
await navigator.clipboard.writeText(location.href)
|
|
39
|
+
setSaid('copied')
|
|
40
|
+
} catch (e) {
|
|
41
|
+
setSaid(`could not copy: ${(e as Error).message}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
<Tip className="dcopy" instant placement="top-end" content={<><div>{said ?? 'click to copy this page\'s url'}</div><code>{href}</code></>}>
|
|
46
|
+
<button className={`ghost dcopyb${said === 'copied' ? ' ok' : ''}`} aria-label="copy this page's url" onMouseEnter={sync} onFocus={sync} onClick={() => void click()}>
|
|
47
|
+
<Icon d={ICON.link} />
|
|
48
|
+
</button>
|
|
49
|
+
</Tip>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
18
53
|
/** a group's label: the repo's icon and its name in its colour */
|
|
19
54
|
const GroupLabel = observer(({ app, g }: { app: HomeSt; g: PlacedGroup }) => {
|
|
20
55
|
const p = app.projects.list.find((x) => x.name === g.key)
|
|
@@ -55,6 +90,7 @@ export const Dock = observer(({ app }: { app: HomeSt }) => {
|
|
|
55
90
|
<CycleButton options={DOCK_SIZES} value={d.size} onChange={(s) => d.setSize(s)} shortcut={DOCK_KEY} title={DOCK_HELP} />
|
|
56
91
|
<PillRow items={d.pills.map((p) => ({ key: p.key, title: p.title, node: <PillFace app={app} p={p} /> }))} />
|
|
57
92
|
{d.error !== '' ? <span className="warnline">{d.error}</span> : null}
|
|
93
|
+
<CopyUrl />
|
|
58
94
|
</div>
|
|
59
95
|
{d.size === 'line' ? null : <div className="dbody">
|
|
60
96
|
{v == null ? null : groups.length === 0
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
.dhead{display:flex;gap:var(--s2);align-items:center;height:22px;flex:none;padding:0 var(--s3)}
|
|
7
7
|
/* the status pills take the room left, and fold rather than wrap */
|
|
8
8
|
.dhead .pchips{flex:1}
|
|
9
|
+
/* the page's url to the clipboard, at the bottom right corner */
|
|
10
|
+
.dcopy{flex:none;display:flex;margin-left:auto}
|
|
11
|
+
.dcopyb{display:flex;align-items:center;padding:0 var(--s0);color:var(--dim)}
|
|
12
|
+
.dcopyb:hover{color:var(--ink)}
|
|
13
|
+
.dcopyb.ok{color:var(--ok)}
|
|
14
|
+
.dcopyb .hico{width:13px;height:13px;color:inherit}
|
|
9
15
|
.dbody{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:0 var(--s3) var(--s1);scrollbar-width:thin;scrollbar-color:var(--line) transparent}
|
|
10
16
|
.dempty{padding:var(--s1) 0;color:var(--dim)}
|
|
11
17
|
.dock .tlax.compact{height:auto;padding-top:0}
|
|
@@ -31,5 +37,5 @@
|
|
|
31
37
|
.dockanim>[data-panel]{transition:flex-grow 180ms ease-out}
|
|
32
38
|
@media (prefers-reduced-motion:reduce){.dockanim>[data-panel]{transition:none}}
|
|
33
39
|
/* the runs page's record chart: its (?) in its top left corner */
|
|
34
|
-
.rchart{position:relative;flex:none;overflow:hidden;margin-
|
|
40
|
+
.rchart{position:relative;flex:none;overflow:hidden;margin-top:var(--s3)}
|
|
35
41
|
.rchelp{position:absolute;top:0;left:0;z-index:4}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// THE way the page says trouble: cards floating at the top right, just under
|
|
2
|
+
// the bar, taking no layout room, so one coming or going moves nothing, and
|
|
3
|
+
// nothing drawn while all is well. A bar never carries trouble text. Styles:
|
|
4
|
+
// home-strip.css .htoasts.
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
|
|
7
|
+
export type Card = {
|
|
8
|
+
key: string
|
|
9
|
+
tone: 'red' | 'warn'
|
|
10
|
+
/** a bold lead word or two ("web crashed"), before the words */
|
|
11
|
+
head?: ReactNode
|
|
12
|
+
body?: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** the cards, in a zero height box placed right after the bar, so they
|
|
16
|
+
* float just under it whatever the bar's height */
|
|
17
|
+
export const FloatCards = ({ cards }: { cards: Card[] }) => {
|
|
18
|
+
if (cards.length === 0) return null
|
|
19
|
+
return (
|
|
20
|
+
<div className="hanchor">
|
|
21
|
+
<div className="htoasts">
|
|
22
|
+
{cards.map((c) => (
|
|
23
|
+
<div key={c.key} className={c.tone === 'warn' ? 'htoast warn' : 'htoast'} role="alert">
|
|
24
|
+
{c.head == null ? null : <><b>{c.head}</b> </>}{c.body}
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// (flip to stay in the window, shift off its edges, an offset from the
|
|
4
4
|
// trigger), a portal lifts it out of every box that clips, and hover opens
|
|
5
5
|
// it after a short delay, so a pointer passing by never flickers one open.
|
|
6
|
-
//
|
|
7
|
-
|
|
6
|
+
// A popover (Pop) opens on a click instead, and Escape or a click outside
|
|
7
|
+
// closes it. Nothing here is positioned by hand.
|
|
8
|
+
import { autoUpdate, flip, FloatingFocusManager, FloatingPortal, offset, safePolygon, shift, useClick, useDismiss, useFloating, useFocus, useHover, useInteractions, useRole, type Placement } from '@floating-ui/react'
|
|
8
9
|
import { observer, useLocalObservable } from 'mobx-react-lite'
|
|
9
10
|
import type { HTMLAttributes, ReactNode } from 'react'
|
|
10
11
|
|
|
@@ -25,13 +26,15 @@ type TipProps = Omit<HTMLAttributes<HTMLSpanElement>, 'content'> & {
|
|
|
25
26
|
fallbacks?: Placement[]
|
|
26
27
|
/** the pointer can move into it (a log to scroll): it stays open on the way */
|
|
27
28
|
interactive?: boolean
|
|
29
|
+
/** opens the moment the pointer arrives, for a trigger whose only words are its tip */
|
|
30
|
+
instant?: boolean
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/** a trigger and what floats over it on hover or focus */
|
|
31
|
-
export const Tip = observer(({ content, children, tipClass = 'ftip', placement = 'top', fallbacks, interactive = false, ...rest }: TipProps) => {
|
|
34
|
+
export const Tip = observer(({ content, children, tipClass = 'ftip', placement = 'top', fallbacks, interactive = false, instant = false, ...rest }: TipProps) => {
|
|
32
35
|
const st = useLocalObservable(() => ({ open: false, set(v: boolean) { this.open = v } }))
|
|
33
36
|
const f = useFloating({ open: st.open, onOpenChange: (v) => st.set(v), placement, strategy: 'fixed', middleware: middleware(6, fallbacks), whileElementsMounted: autoUpdate })
|
|
34
|
-
const hover = useHover(f.context, { delay: { open: OPEN_MS, close: interactive ? 100 : 0 }, handleClose: interactive ? safePolygon() : null })
|
|
37
|
+
const hover = useHover(f.context, { delay: { open: instant ? 0 : OPEN_MS, close: interactive ? 100 : 0 }, handleClose: interactive ? safePolygon() : null })
|
|
35
38
|
const focus = useFocus(f.context)
|
|
36
39
|
const dismiss = useDismiss(f.context)
|
|
37
40
|
const role = useRole(f.context, { role: 'tooltip' })
|
|
@@ -47,3 +50,37 @@ export const Tip = observer(({ content, children, tipClass = 'ftip', placement =
|
|
|
47
50
|
</>
|
|
48
51
|
)
|
|
49
52
|
})
|
|
53
|
+
|
|
54
|
+
type PopProps = {
|
|
55
|
+
/** what the trigger button shows */
|
|
56
|
+
label: ReactNode
|
|
57
|
+
className?: string
|
|
58
|
+
'aria-label'?: string
|
|
59
|
+
popClass?: string
|
|
60
|
+
placement?: Placement
|
|
61
|
+
/** what floats, given the way to close it (an entry that acts closes it) */
|
|
62
|
+
children: (close: () => void) => ReactNode
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** a button that opens what floats under it on a click; focus moves into
|
|
66
|
+
* it, and Escape or a click outside closes it */
|
|
67
|
+
export const Pop = observer(({ label, className, popClass = 'fpop', placement = 'bottom-start', children, ...rest }: PopProps) => {
|
|
68
|
+
const st = useLocalObservable(() => ({ open: false, set(v: boolean) { this.open = v } }))
|
|
69
|
+
const f = useFloating({ open: st.open, onOpenChange: (v) => st.set(v), placement, strategy: 'fixed', middleware: middleware(4), whileElementsMounted: autoUpdate })
|
|
70
|
+
const click = useClick(f.context)
|
|
71
|
+
const dismiss = useDismiss(f.context)
|
|
72
|
+
const role = useRole(f.context, { role: 'dialog' })
|
|
73
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role])
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<button type="button" ref={f.refs.setReference} className={className} {...getReferenceProps(rest)}>{label}</button>
|
|
77
|
+
{st.open ? (
|
|
78
|
+
<FloatingPortal>
|
|
79
|
+
<FloatingFocusManager context={f.context} modal={false}>
|
|
80
|
+
<div ref={f.refs.setFloating} className={popClass} style={f.floatingStyles} {...getFloatingProps()}>{children(() => st.set(false))}</div>
|
|
81
|
+
</FloatingFocusManager>
|
|
82
|
+
</FloatingPortal>
|
|
83
|
+
) : null}
|
|
84
|
+
</>
|
|
85
|
+
)
|
|
86
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// A repo's git page state: one repo's git, read from the daemon while the
|
|
2
|
+
// page is shown (`/api/home/git.json`), a week of commits and HEAD's last
|
|
3
|
+
// one past that. The activity page reads every repo for a window; this reads
|
|
4
|
+
// one, whatever that window is.
|
|
5
|
+
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
6
|
+
import type { ActivityRow } from '../../home/activity-shape.ts'
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
9
|
+
|
|
10
|
+
export const GIT_POLL_MS = 30_000
|
|
11
|
+
|
|
12
|
+
export class GitSt {
|
|
13
|
+
row: ActivityRow | null = null
|
|
14
|
+
error = ''
|
|
15
|
+
private timer: ReturnType<typeof setInterval> | null = null
|
|
16
|
+
|
|
17
|
+
constructor(private st: RepoSt) {
|
|
18
|
+
makeAutoObservable<this, 'st' | 'timer'>(this, { st: false, timer: false, row: observableRef })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async load(): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
const r = await reach(`/api/home/git.json?repo=${encodeURIComponent(this.st.key)}`, { signal: AbortSignal.timeout(30_000) })
|
|
24
|
+
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
25
|
+
const j = (await r.json()) as ActivityRow
|
|
26
|
+
runInAction(() => { this.row = j; this.error = '' })
|
|
27
|
+
} catch (e) {
|
|
28
|
+
runInAction(() => { this.error = ownTrouble(e, this.st.home.machine, (m) => `read git: ${m}`) })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** reads while the page is shown */
|
|
33
|
+
start(): void {
|
|
34
|
+
if (this.timer != null) return
|
|
35
|
+
void this.load()
|
|
36
|
+
this.timer = setInterval(() => void this.load(), GIT_POLL_MS)
|
|
37
|
+
}
|
|
38
|
+
stop(): void {
|
|
39
|
+
if (this.timer != null) clearInterval(this.timer)
|
|
40
|
+
this.timer = null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// A repo's git page: its branch against its upstream, what is not committed,
|
|
2
|
+
// and its commits of the last week (HEAD's last one when the week holds
|
|
3
|
+
// none), with the ways out to VS Code and Fork. Every repo at once: the
|
|
4
|
+
// activity page (⌘L).
|
|
5
|
+
import { observer } from 'mobx-react-lite'
|
|
6
|
+
import { useEffect } from 'react'
|
|
7
|
+
import { isMainBranch, upstreamWords } from '../../home/activity-shape.ts'
|
|
8
|
+
import { Commit } from './activity-view.tsx'
|
|
9
|
+
import { Icon, ICON } from './icons.tsx'
|
|
10
|
+
import type { RepoSt } from './repo-st.ts'
|
|
11
|
+
|
|
12
|
+
const KIND_WORD = { staged: 'staged', modified: 'modified', untracked: 'new' } as const
|
|
13
|
+
|
|
14
|
+
export const GitPage = observer(({ app }: { app: RepoSt }) => {
|
|
15
|
+
const g = app.git
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
g.start()
|
|
18
|
+
return () => g.stop()
|
|
19
|
+
}, [g])
|
|
20
|
+
const r = g.row
|
|
21
|
+
const m = app.home.machine
|
|
22
|
+
const now = app.clock.now
|
|
23
|
+
const branch = r == null ? '' : r.branch ?? (r.detached == null ? '' : `detached ${r.detached}`)
|
|
24
|
+
return (
|
|
25
|
+
<div className="colcard checkcard">
|
|
26
|
+
<div className="chead">
|
|
27
|
+
<span className="tid">git</span>
|
|
28
|
+
<h3 className="ctitle">{app.name}</h3>
|
|
29
|
+
<span className="sp" />
|
|
30
|
+
{m == null ? null : <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>}
|
|
31
|
+
<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>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="usays">its branch, what is not committed, and its commits of the last week. Every repo at once: the activity page (⌘L)</div>
|
|
34
|
+
{g.error !== '' ? <div className="warnline">{g.error}</div> : null}
|
|
35
|
+
{r == null ? (g.error === '' ? <div className="dim twait"><span className="spin" />reading git…</div> : null) : r.error != null ? <div className="tl red">git failed: {r.error}</div> : (
|
|
36
|
+
<>
|
|
37
|
+
<div className="cfacts">
|
|
38
|
+
<div className="cf"><div className="ck">branch</div><div className={`cv tl ${isMainBranch(r.branch) ? 'ok' : 'warn'}`}>{branch}</div></div>
|
|
39
|
+
<div className="cf"><div className="ck">upstream</div><div className="cv">{upstreamWords(r)}</div></div>
|
|
40
|
+
</div>
|
|
41
|
+
<section className="csec">
|
|
42
|
+
<div className="cstitle">not committed ({r.files.length})</div>
|
|
43
|
+
{r.files.length === 0 ? <div className="dim">nothing: the tree is clean</div> : (
|
|
44
|
+
<table className="gitfiles"><tbody>{r.files.map((f) => <tr key={f.path}><td className={`gitkind ${f.kind}`}>{KIND_WORD[f.kind]}</td><td className="gitpath">{f.path}</td></tr>)}</tbody></table>
|
|
45
|
+
)}
|
|
46
|
+
</section>
|
|
47
|
+
<section className="csec">
|
|
48
|
+
<div className="cstitle">commits, last week ({r.commits.length})</div>
|
|
49
|
+
<table className="gitcommits">
|
|
50
|
+
<tbody>
|
|
51
|
+
{r.commits.map((c) => <tr key={c.sha}><Commit c={c} now={now} old={false} /></tr>)}
|
|
52
|
+
{r.last != null ? <tr><Commit c={r.last} now={now} old /></tr> : null}
|
|
53
|
+
{r.commits.length === 0 && r.last == null ? <tr><td className="dim">no commit yet</td></tr> : null}
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
</section>
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
})
|
|
@@ -22,9 +22,25 @@ export type HashSel =
|
|
|
22
22
|
| { t: 'tests'; check: string | null }
|
|
23
23
|
/** what administers the repo: archive, reload its hub, keep it active */
|
|
24
24
|
| { t: 'admin' }
|
|
25
|
+
/** the repo's README, what a repo shows with nothing else named */
|
|
26
|
+
| { t: 'readme' }
|
|
27
|
+
/** the global processes the repo declares, one card each, `id` the one
|
|
28
|
+
* a tree row opened (scrolled to and lit), null for none */
|
|
29
|
+
| { t: 'processes'; id: string | null }
|
|
30
|
+
/** what the machine plans to run in this repo, and when */
|
|
31
|
+
| { t: 'plan' }
|
|
32
|
+
/** the repo's git: branch, upstream, uncommitted files, recent commits */
|
|
33
|
+
| { t: 'git' }
|
|
34
|
+
/** what runs when: every check on every context, with the legend */
|
|
35
|
+
| { t: 'when' }
|
|
36
|
+
/** the checks one owner ships: shipkit's, or the repo's own */
|
|
37
|
+
| { t: 'checks'; from: CheckOwner }
|
|
38
|
+
|
|
39
|
+
/** whose a gate check is */
|
|
40
|
+
export type CheckOwner = 'shipkit' | 'repo'
|
|
25
41
|
|
|
26
42
|
export type ParsedHash = {
|
|
27
|
-
kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'admin' | 'none'
|
|
43
|
+
kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'admin' | 'readme' | 'processes' | 'plan' | 'git' | 'when' | 'checks' | 'none'
|
|
28
44
|
/** the collection, for an item. null for everything else */
|
|
29
45
|
col: string | null
|
|
30
46
|
id: string
|
|
@@ -40,6 +56,12 @@ const CAT = 'cat-'
|
|
|
40
56
|
const STATUS = 'page:status'
|
|
41
57
|
const TESTS = 'page:tests'
|
|
42
58
|
const ADMIN = 'page:admin'
|
|
59
|
+
const README = 'page:readme'
|
|
60
|
+
const PROCESSES = 'page:processes'
|
|
61
|
+
const PLAN = 'page:plan'
|
|
62
|
+
const GIT = 'page:git'
|
|
63
|
+
const WHEN = 'page:when'
|
|
64
|
+
const CHECKS = 'page:checks:'
|
|
43
65
|
|
|
44
66
|
/** what this selection puts in the url. null only for no selection at all,
|
|
45
67
|
* and then the hash is left alone rather than wiped */
|
|
@@ -53,6 +75,12 @@ export function hashFor(sel: HashSel | null): string | null {
|
|
|
53
75
|
case 'status': return STATUS
|
|
54
76
|
case 'tests': return sel.check == null ? TESTS : `${TESTS}:${sel.check}`
|
|
55
77
|
case 'admin': return ADMIN
|
|
78
|
+
case 'readme': return README
|
|
79
|
+
case 'processes': return sel.id == null ? PROCESSES : `${PROCESSES}:${sel.id}`
|
|
80
|
+
case 'plan': return PLAN
|
|
81
|
+
case 'git': return GIT
|
|
82
|
+
case 'when': return WHEN
|
|
83
|
+
case 'checks': return `${CHECKS}${sel.from}`
|
|
56
84
|
}
|
|
57
85
|
}
|
|
58
86
|
|
|
@@ -62,6 +90,16 @@ export function parseHash(raw: string): ParsedHash {
|
|
|
62
90
|
if (raw === STATUS) return { kind: 'status', col: null, id: '', tools: null }
|
|
63
91
|
if (raw === TESTS) return { kind: 'tests', col: null, id: '', tools: null }
|
|
64
92
|
if (raw === ADMIN) return { kind: 'admin', col: null, id: '', tools: null }
|
|
93
|
+
if (raw === README) return { kind: 'readme', col: null, id: '', tools: null }
|
|
94
|
+
if (raw === PROCESSES) return { kind: 'processes', col: null, id: '', tools: null }
|
|
95
|
+
if (raw === PLAN) return { kind: 'plan', col: null, id: '', tools: null }
|
|
96
|
+
if (raw === GIT) return { kind: 'git', col: null, id: '', tools: null }
|
|
97
|
+
if (raw === WHEN) return { kind: 'when', col: null, id: '', tools: null }
|
|
98
|
+
if (raw === `${CHECKS}shipkit` || raw === `${CHECKS}repo`) return { kind: 'checks', col: null, id: raw.slice(CHECKS.length), tools: null }
|
|
99
|
+
if (raw.startsWith(`${PROCESSES}:`)) {
|
|
100
|
+
const id = raw.slice(PROCESSES.length + 1)
|
|
101
|
+
return id === '' ? none : { kind: 'processes', col: null, id, tools: null }
|
|
102
|
+
}
|
|
65
103
|
// a check id carries colons of its own (`test:rvlib-shipkit`)
|
|
66
104
|
if (raw.startsWith(`${TESTS}:`)) {
|
|
67
105
|
const check = raw.slice(TESTS.length + 1)
|
|
@@ -104,6 +142,12 @@ export function selOf(p: ParsedHash): HashSel | null {
|
|
|
104
142
|
case 'status': return { t: 'status' }
|
|
105
143
|
case 'tests': return { t: 'tests', check: p.id === '' ? null : p.id }
|
|
106
144
|
case 'admin': return { t: 'admin' }
|
|
145
|
+
case 'readme': return { t: 'readme' }
|
|
146
|
+
case 'processes': return { t: 'processes', id: p.id === '' ? null : p.id }
|
|
147
|
+
case 'plan': return { t: 'plan' }
|
|
148
|
+
case 'git': return { t: 'git' }
|
|
149
|
+
case 'when': return { t: 'when' }
|
|
150
|
+
case 'checks': return { t: 'checks', from: p.id as CheckOwner }
|
|
107
151
|
}
|
|
108
152
|
}
|
|
109
153
|
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
// A repo's pieces of the top bar
|
|
2
|
-
// and the
|
|
3
|
-
//
|
|
4
|
-
// (
|
|
5
|
-
//
|
|
1
|
+
// A repo's pieces of the top bar. On the right: modernize while it is behind
|
|
2
|
+
// and the hub's own processes (RepoTools), then a spinner while something
|
|
3
|
+
// runs, its words on hover, and one check… button that opens the two runs
|
|
4
|
+
// that are not one check (RepoRun), then at the far right end the editor
|
|
5
|
+
// icons (RepoOpeners). The home strip puts the repo's global processes right
|
|
6
|
+
// after its name (home-strip.tsx). The bar carries no trouble text:
|
|
7
|
+
// the hub's own trouble is a card floating under it (hubCards, float-cards.tsx).
|
|
8
|
+
// On the home they ride in the home strip (home-strip.tsx); a hub of its own
|
|
6
9
|
// draws them in its own header, with its name. The repo's sections are the
|
|
7
|
-
// left pane's (repo-sections.
|
|
8
|
-
// bottom of the page (dock-view.tsx).
|
|
10
|
+
// top rows of the left pane's tree (repo-sections.ts). What runs, drawn, is
|
|
11
|
+
// the dock at the bottom of the page (dock-view.tsx).
|
|
9
12
|
import { observer } from 'mobx-react-lite'
|
|
10
13
|
import { Icon, ICON } from './icons.tsx'
|
|
14
|
+
import { Pop, Tip } from './floating.tsx'
|
|
15
|
+
import { FloatCards, type Card } from './float-cards.tsx'
|
|
11
16
|
import type { RepoSt } from './repo-st.ts'
|
|
12
17
|
|
|
13
18
|
/** the repo's hub has not answered on the home: its reason and its fix show
|
|
@@ -17,56 +22,64 @@ export const notUp = (app: RepoSt): boolean => {
|
|
|
17
22
|
const m = app.home.machine
|
|
18
23
|
if (m == null || app.started) return false
|
|
19
24
|
if (!app.cols.cached) return true
|
|
20
|
-
|
|
25
|
+
// the daemon silent: the hub's state is unknown, not down, so its last screen stays
|
|
26
|
+
return m.fresh && m.down === '' && m.worker(app.key)?.status !== 'up'
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
/** what is running, in
|
|
24
|
-
* most, else the newest run the record has in flight.
|
|
25
|
-
|
|
26
|
-
const RunState = observer(({ app }: { app: RepoSt }) => {
|
|
29
|
+
/** what is running, in words: the hub's own run first, since it says the
|
|
30
|
+
* most, else the newest run the record has in flight. Null while idle */
|
|
31
|
+
export const runWords = (app: RepoSt): string | null => {
|
|
27
32
|
const own = app.checks.strip
|
|
33
|
+
if (own !== '') return own
|
|
28
34
|
const run = app.live.view?.runs[0]
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
if (run != null) return `${run.label} · ${run.done} of ${run.total}${run.reds > 0 ? ` · ${run.reds} red so far` : ''}`
|
|
36
|
+
return app.checks.busy ? 'running' : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** a spinner while something runs, its words in a tooltip that opens at
|
|
40
|
+
* once; nothing at all while idle */
|
|
41
|
+
const RunState = observer(({ app }: { app: RepoSt }) => {
|
|
42
|
+
const text = runWords(app)
|
|
43
|
+
if (text == null) return null
|
|
31
44
|
return (
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<span className="htext">{text}</span>
|
|
36
|
-
</span>
|
|
45
|
+
<Tip className="hbusy" instant placement="bottom" tabIndex={0} role="status" aria-label={`running: ${text}`} content={text}>
|
|
46
|
+
<span className="spin" />
|
|
47
|
+
</Tip>
|
|
37
48
|
)
|
|
38
49
|
})
|
|
39
50
|
|
|
40
51
|
export const HEALTH_TITLE = 'run the health pass: every check but the build steps, as shipkit check runs them (creds, probes, rules, contracts)'
|
|
41
52
|
export const GATE_TITLE = 'run the gate: every check shipkit ci runs, the build steps included (lint, typecheck, tests)'
|
|
42
53
|
|
|
43
|
-
/** the
|
|
44
|
-
|
|
45
|
-
export const RepoRun = observer(({ app }: { app: RepoSt }) => {
|
|
54
|
+
/** the two runs, each with what it does; an entry closes the menu as it starts */
|
|
55
|
+
export const CheckMenu = observer(({ app, close }: { app: RepoSt; close: () => void }) => {
|
|
46
56
|
const u = app.checks
|
|
47
57
|
return (
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
<Icon d={ICON.health} /><span className="lbl">health pass</span>
|
|
58
|
+
<div className="hmenu">
|
|
59
|
+
<button className="hmitem" disabled={u.busy} onClick={() => { close(); void u.run() }}>
|
|
60
|
+
<Icon d={ICON.health} /><span><b>health pass</b><small>{HEALTH_TITLE}</small></span>
|
|
52
61
|
</button>
|
|
53
|
-
<button className="
|
|
54
|
-
<Icon d={ICON.gate} /><span
|
|
62
|
+
<button className="hmitem" disabled={u.busy} onClick={() => { close(); void u.run(undefined, { gate: true, label: 'the gate' }) }}>
|
|
63
|
+
<Icon d={ICON.gate} /><span><b>gate</b><small>{GATE_TITLE}</small></span>
|
|
55
64
|
</button>
|
|
56
|
-
</
|
|
65
|
+
</div>
|
|
57
66
|
)
|
|
58
67
|
})
|
|
59
68
|
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
|
|
69
|
+
/** the spinner while a run goes, then one small check… button with the two
|
|
70
|
+
* runs behind it. The bar ends on the right, so the spinner comes and goes
|
|
71
|
+
* to the button's left and the button never moves */
|
|
72
|
+
export const RepoRun = observer(({ app }: { app: RepoSt }) => (
|
|
73
|
+
<span className="hrun">
|
|
74
|
+
<RunState app={app} />
|
|
75
|
+
<Pop className="ghost hcheck" aria-label="run a check" label="check…" placement="bottom-end">{(close) => <CheckMenu app={app} close={close} />}</Pop>
|
|
76
|
+
</span>
|
|
68
77
|
))
|
|
69
78
|
|
|
79
|
+
/** what is wrong with the hub itself, one card per problem */
|
|
80
|
+
export const hubCards = (app: RepoSt): Card[] =>
|
|
81
|
+
app.cols.hubStatus.map((p) => ({ key: `hub:${p.id}`, tone: p.severity === 'red' ? 'red' : 'warn', body: p.text }))
|
|
82
|
+
|
|
70
83
|
/** modernize, while the repo runs an older shipkit than the home */
|
|
71
84
|
const Modernize = observer(({ app }: { app: RepoSt }) => {
|
|
72
85
|
const m = app.home.machine
|
|
@@ -88,48 +101,25 @@ export const RepoOpeners = observer(({ app }: { app: RepoSt }) => {
|
|
|
88
101
|
)
|
|
89
102
|
})
|
|
90
103
|
|
|
91
|
-
/** the right side of the bar for a repo, before the
|
|
92
|
-
* the openers, which close it */
|
|
104
|
+
/** the right side of the bar for a repo, before the openers, which close it */
|
|
93
105
|
export const RepoTools = observer(({ app }: { app: RepoSt }) => (
|
|
94
106
|
<>
|
|
95
|
-
<ProcessChips app={app} />
|
|
96
|
-
<HubStatus app={app} />
|
|
97
107
|
<Modernize app={app} />
|
|
98
108
|
</>
|
|
99
109
|
))
|
|
100
110
|
|
|
101
111
|
/** the bar of a hub of its own: its name, then the same pieces the home
|
|
102
|
-
* strip carries for a repo */
|
|
112
|
+
* strip carries for a repo, and its trouble floating under it */
|
|
103
113
|
export const HeaderBar = observer(({ app }: { app: RepoSt }) => (
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
<>
|
|
115
|
+
<header className="hdr">
|
|
116
|
+
<span className="hname" style={app.name === '' ? undefined : { color: app.home.projects.identityOf(app.name).colorDark }}>{app.name !== '' ? app.name : 'hub'}</span>
|
|
117
|
+
<span className="sp" />
|
|
118
|
+
<RepoTools app={app} />
|
|
119
|
+
<RepoRun app={app} />
|
|
120
|
+
<RepoOpeners app={app} />
|
|
121
|
+
</header>
|
|
122
|
+
<FloatCards cards={hubCards(app)} />
|
|
123
|
+
</>
|
|
111
124
|
))
|
|
112
125
|
|
|
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
|
|
118
|
-
return (
|
|
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>
|
|
134
|
-
)
|
|
135
|
-
})
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// exists so the whole text of a run stays reachable.
|
|
7
7
|
import { makeAutoObservable, runInAction } from 'mobx'
|
|
8
8
|
import type { RepoSt } from './repo-st.ts'
|
|
9
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
9
10
|
|
|
10
11
|
export class HealthSvc {
|
|
11
12
|
statusMd: string | null = null
|
|
@@ -25,10 +26,10 @@ export class HealthSvc {
|
|
|
25
26
|
|
|
26
27
|
async load(): Promise<void> {
|
|
27
28
|
try {
|
|
28
|
-
const s = (await (await
|
|
29
|
+
const s = (await (await reach(this.st.url('/api/status.json'))).json()) as { statusMd: string | null }
|
|
29
30
|
runInAction(() => { this.statusMd = s.statusMd })
|
|
30
31
|
} catch (e) {
|
|
31
|
-
runInAction(() => { this.statusMd = `FAIL status fetch broke: ${
|
|
32
|
+
runInAction(() => { this.statusMd = ownTrouble(e, this.st.home.machine, (m) => `FAIL status fetch broke: ${m}`) || this.statusMd })
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|