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
|
@@ -8,6 +8,7 @@ import type { PlannedAnswer } from '../../home/home-routes.ts'
|
|
|
8
8
|
import type { PlanRow } from '../../plan/planner.ts'
|
|
9
9
|
import { sectionOf, timeline, titleWords, waitingRows, waitingWords } from '../../plan/plan-timeline.ts'
|
|
10
10
|
import type { HomeSt } from './home-st.ts'
|
|
11
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
11
12
|
|
|
12
13
|
export const PLANNED_POLL_MS = 1_000
|
|
13
14
|
|
|
@@ -78,18 +79,22 @@ export class PlannedSt {
|
|
|
78
79
|
|
|
79
80
|
async poll(): Promise<void> {
|
|
80
81
|
try {
|
|
81
|
-
const r = await
|
|
82
|
+
const r = await reach('/api/home/planned.json', { signal: AbortSignal.timeout(8_000) })
|
|
82
83
|
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
83
84
|
const j = (await r.json()) as PlannedAnswer
|
|
84
85
|
runInAction(() => { this.data = j; this.error = '' })
|
|
85
86
|
} catch (e) {
|
|
86
|
-
runInAction(() => { this.error = (e
|
|
87
|
+
runInAction(() => { this.error = ownTrouble(e, this.st.machine) })
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
async run(row: PlanRow): Promise<void> {
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
try {
|
|
93
|
+
const r = await reach('/api/home/planned/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ repo: row.repo, check: row.check }) })
|
|
94
|
+
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`)
|
|
95
|
+
} catch (e) {
|
|
96
|
+
runInAction(() => { this.error = ownTrouble(e, this.st.machine, (m) => `run ${row.check}: ${m}`) })
|
|
97
|
+
}
|
|
93
98
|
await this.poll()
|
|
94
99
|
}
|
|
95
100
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The processes page of a repo: one card per global process it declares,
|
|
2
|
+
// each the process menu's two panels (actions, then its description as
|
|
3
|
+
// markdown) and its log under them. pm runs them, so the page reads the
|
|
4
|
+
// machine, never the hub. The card a tree row opened is scrolled to and lit.
|
|
5
|
+
import { observer } from 'mobx-react-lite'
|
|
6
|
+
import { useEffect, useRef } from 'react'
|
|
7
|
+
import { Log, ProcessMenu } from './home-strip.tsx'
|
|
8
|
+
import type { RepoSt } from './repo-st.ts'
|
|
9
|
+
import type { ProcessRow } from '../../home/process-rows.ts'
|
|
10
|
+
|
|
11
|
+
const noop = (): void => {}
|
|
12
|
+
|
|
13
|
+
const ProcessCard = observer(({ app, r, lit }: { app: RepoSt; r: ProcessRow; lit: boolean }) => {
|
|
14
|
+
const el = useRef<HTMLElement>(null)
|
|
15
|
+
useEffect(() => { if (lit) el.current?.scrollIntoView({ block: 'nearest' }) }, [lit])
|
|
16
|
+
const m = app.home.machine
|
|
17
|
+
if (m == null) return null
|
|
18
|
+
return (
|
|
19
|
+
<section ref={el} className={`csec pcard${lit ? ' lit' : ''}`} id={`process-${r.id}`}>
|
|
20
|
+
<ProcessMenu m={m} r={r} close={noop} lastLog={r.tail.length === 0} />
|
|
21
|
+
{r.tail.length === 0 ? null : <><div className="cstitle">log</div><Log lines={r.tail} /></>}
|
|
22
|
+
</section>
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const ProcessesPage = observer(({ app, id }: { app: RepoSt; id: string | null }) => {
|
|
27
|
+
const rows = app.globalProcesses
|
|
28
|
+
return (
|
|
29
|
+
<div className="colcard checkcard">
|
|
30
|
+
<div className="chead">
|
|
31
|
+
<span className="tid">processes</span>
|
|
32
|
+
<h3 className="ctitle">{app.name}</h3>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="usays">the processes this repo declares in its <code>.shipkit.ts</code> files. pm runs them, so they run whether or not the hub answers</div>
|
|
35
|
+
{rows.length === 0 ? <div className="dim">this repo declares no global process</div> : rows.map((r) => <ProcessCard key={r.id} app={app} r={r} lit={r.id === id} />)}
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
})
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// in), archived shows or hides the archived repos just above the foot
|
|
15
15
|
// (names only, no ⌘ number, a row opens the archived page, nothing loads),
|
|
16
16
|
// the foot never moving. On the home a section of four lines on top, in the
|
|
17
|
-
// repo rows' style, opens the
|
|
17
|
+
// repo rows' style, opens the picker, the overview, the activity and the
|
|
18
18
|
// plan, each with its shortcut at the line's end, the page shown lit. It
|
|
19
19
|
// renders from the first paint with the list this browser kept, so a load
|
|
20
20
|
// moves nothing.
|
|
@@ -30,11 +30,11 @@ import { cardModel, faceOf, hueOf, SORTS, type Place, type Project, type Project
|
|
|
30
30
|
/** what the list needs of its page: the hub and the home both carry one */
|
|
31
31
|
export type ListHost = { projects: ProjectsSvc }
|
|
32
32
|
|
|
33
|
-
/** the lines on top of the list, in their order: the
|
|
33
|
+
/** the lines on top of the list, in their order: the picker, the overview,
|
|
34
34
|
* then the places, their shortcuts ⌘J ⌘K ⌘L ⌘' top to bottom */
|
|
35
35
|
const PLACES: { id: Place | 'picker'; label: string; title: string }[] = [
|
|
36
|
-
{ id: 'overview', label: 'overview', title: 'the overview: shipkit, every global process and every repo on this machine' },
|
|
37
36
|
{ id: 'picker', label: 'jump to…', title: 'jump to a page, a repo or one of its resources' },
|
|
37
|
+
{ id: 'overview', label: 'overview', title: 'the overview: shipkit, every global process and every repo on this machine' },
|
|
38
38
|
{ id: 'activity', label: 'activity', title: 'what every repo committed lately, what it holds uncommitted, its branch and whether it is pushed' },
|
|
39
39
|
{ id: 'planned', label: 'planned', title: 'every check the machine runs on its own: now, next, late, and why' },
|
|
40
40
|
]
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
6
6
|
import type { ArchivedRow, ProjectRow as Project, Release } from '../project-gutter-route.ts'
|
|
7
7
|
import { identityOfName, type Identity } from '../../core/identity-color.ts'
|
|
8
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
9
|
+
|
|
10
|
+
/** the list never says the page's server is gone, even where no machine
|
|
11
|
+
* card is (a hub on its own): that hub's trouble card says it */
|
|
12
|
+
const SAID_ELSEWHERE = true
|
|
8
13
|
|
|
9
14
|
export type { ArchivedRow, ProjectRow as Project, Release } from '../project-gutter-route.ts'
|
|
10
15
|
|
|
@@ -250,12 +255,12 @@ export class ProjectsSvc {
|
|
|
250
255
|
* loads its hub on its next tick */
|
|
251
256
|
async setArchived(name: string, on: boolean): Promise<void> {
|
|
252
257
|
try {
|
|
253
|
-
const r = await
|
|
258
|
+
const r = await reach('/api/projects/archived', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, archived: on }) })
|
|
254
259
|
if (!r.ok) throw new Error(await r.text())
|
|
255
260
|
runInAction(() => { this.error = '' })
|
|
256
261
|
await this.load()
|
|
257
262
|
} catch (e) {
|
|
258
|
-
runInAction(() => { this.error = `${on ? 'archive' : 'unarchive'} ${name}: ${
|
|
263
|
+
runInAction(() => { this.error = ownTrouble(e, SAID_ELSEWHERE, (m) => `${on ? 'archive' : 'unarchive'} ${name}: ${m}`) })
|
|
259
264
|
}
|
|
260
265
|
}
|
|
261
266
|
|
|
@@ -302,7 +307,7 @@ export class ProjectsSvc {
|
|
|
302
307
|
|
|
303
308
|
private async saveIdentity(name: string, id: Identity, h: ReturnType<typeof setTimeout>): Promise<void> {
|
|
304
309
|
try {
|
|
305
|
-
const r = await
|
|
310
|
+
const r = await reach('/api/projects/identity', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, ...id }) })
|
|
306
311
|
if (!r.ok) throw new Error(await r.text())
|
|
307
312
|
const j = (await r.json()) as { identity: Identity }
|
|
308
313
|
runInAction(() => {
|
|
@@ -310,7 +315,7 @@ export class ProjectsSvc {
|
|
|
310
315
|
this.error = ''
|
|
311
316
|
})
|
|
312
317
|
} catch (e) {
|
|
313
|
-
runInAction(() => { this.error = `colours of ${name}: ${
|
|
318
|
+
runInAction(() => { this.error = ownTrouble(e, SAID_ELSEWHERE, (m) => `colours of ${name}: ${m}`) })
|
|
314
319
|
} finally {
|
|
315
320
|
// a later pick rests on its own timer, which keeps the guard
|
|
316
321
|
if (this.identitySaves.get(name) === h) this.identitySaves.delete(name)
|
|
@@ -390,11 +395,11 @@ export class ProjectsSvc {
|
|
|
390
395
|
const by = new Map(this.list.map((p) => [p.name, p]))
|
|
391
396
|
this.list = names.map((n) => by.get(n)).filter((p): p is Project => p != null)
|
|
392
397
|
try {
|
|
393
|
-
const r = await
|
|
398
|
+
const r = await reach('/api/projects/order', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ names }) })
|
|
394
399
|
if (!r.ok) throw new Error(await r.text())
|
|
395
400
|
runInAction(() => { this.error = '' })
|
|
396
401
|
} catch (e) {
|
|
397
|
-
runInAction(() => { this.error = `reorder: ${
|
|
402
|
+
runInAction(() => { this.error = ownTrouble(e, SAID_ELSEWHERE, (m) => `reorder: ${m}`) })
|
|
398
403
|
}
|
|
399
404
|
}
|
|
400
405
|
|
|
@@ -427,7 +432,7 @@ export class ProjectsSvc {
|
|
|
427
432
|
async load(): Promise<void> {
|
|
428
433
|
const order = this.orderGen
|
|
429
434
|
try {
|
|
430
|
-
const r = await
|
|
435
|
+
const r = await reach('/api/projects.json')
|
|
431
436
|
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
432
437
|
const j = (await r.json()) as { projects: Project[]; archived?: ArchivedRow[] }
|
|
433
438
|
runInAction(() => {
|
|
@@ -440,17 +445,17 @@ export class ProjectsSvc {
|
|
|
440
445
|
this.fresh = true
|
|
441
446
|
})
|
|
442
447
|
} catch (e) {
|
|
443
|
-
runInAction(() => { this.error = `projects: ${
|
|
448
|
+
runInAction(() => { this.error = ownTrouble(e, SAID_ELSEWHERE, (m) => `projects: ${m}`) })
|
|
444
449
|
}
|
|
445
450
|
}
|
|
446
451
|
|
|
447
452
|
async open(name: string): Promise<void> {
|
|
448
453
|
try {
|
|
449
|
-
const r = await
|
|
454
|
+
const r = await reach('/api/projects/open', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name }) })
|
|
450
455
|
if (!r.ok) throw new Error(await r.text())
|
|
451
456
|
runInAction(() => { this.error = '' })
|
|
452
457
|
} catch (e) {
|
|
453
|
-
runInAction(() => { this.error = `open ${name}: ${
|
|
458
|
+
runInAction(() => { this.error = ownTrouble(e, SAID_ELSEWHERE, (m) => `open ${name}: ${m}`) })
|
|
454
459
|
}
|
|
455
460
|
}
|
|
456
461
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
7
7
|
import type { ProseGroup, ProseView } from '../../prose/prose-view.ts'
|
|
8
8
|
import type { RepoSt } from './repo-st.ts'
|
|
9
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
9
10
|
|
|
10
11
|
export type TierFilter = 'all' | 'blocking' | 'soft'
|
|
11
12
|
export type ListKind = 'allow' | 'ban'
|
|
@@ -37,12 +38,12 @@ export class ProseHitsSt {
|
|
|
37
38
|
async load(): Promise<void> {
|
|
38
39
|
runInAction(() => { this.loading = true })
|
|
39
40
|
try {
|
|
40
|
-
const r = await
|
|
41
|
+
const r = await reach(this.st.url('/api/prose.json'))
|
|
41
42
|
if (!r.ok) throw new Error(await r.text())
|
|
42
43
|
const v = (await r.json()) as ProseView
|
|
43
44
|
runInAction(() => { this.view = v; this.error = '' })
|
|
44
45
|
} catch (e) {
|
|
45
|
-
runInAction(() => { this.error = `the hits could not be read: ${
|
|
46
|
+
runInAction(() => { this.error = ownTrouble(e, this.st.home.machine, (m) => `the hits could not be read: ${m}`) })
|
|
46
47
|
}
|
|
47
48
|
runInAction(() => { this.loading = false })
|
|
48
49
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// the one reading of "nothing behind the page answers": the web itself (a
|
|
2
|
+
// fetch that never gets an answer), or the daemon or pm behind it (the web's
|
|
3
|
+
// tagged 502, serve-web.ts). On the home that is one fact about the machine,
|
|
4
|
+
// said once, by the card under the strip (MachineSt). Every other surface
|
|
5
|
+
// that fails for it stays silent and keeps what it last read.
|
|
6
|
+
|
|
7
|
+
export type Who = 'web' | 'daemon' | 'pm'
|
|
8
|
+
|
|
9
|
+
export class Unreachable extends Error {
|
|
10
|
+
constructor(msg: string, readonly who: Who) {
|
|
11
|
+
super(msg)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** fetch, with the machine not answering thrown as Unreachable. Any other
|
|
16
|
+
* answer comes back as it is, an error status included */
|
|
17
|
+
export async function reach(input: string, init?: RequestInit): Promise<Response> {
|
|
18
|
+
let r: Response
|
|
19
|
+
try {
|
|
20
|
+
r = await fetch(input, init)
|
|
21
|
+
} catch (e) {
|
|
22
|
+
throw new Unreachable((e as Error).message, 'web')
|
|
23
|
+
}
|
|
24
|
+
if (r.headers.get('x-shipkit-daemon') === 'down') throw new Unreachable(await r.text(), 'daemon')
|
|
25
|
+
if (r.headers.get('x-shipkit-pm') === 'down') throw new Unreachable(await r.text(), 'pm')
|
|
26
|
+
return r
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** the words a surface shows for its failed request, or '' when the failure
|
|
30
|
+
* is the machine's: the machine card says that. `machine` is whether the
|
|
31
|
+
* page has one: a hub serving on its own has none, so a server it cannot
|
|
32
|
+
* reach is its own trouble there */
|
|
33
|
+
export function ownTrouble(e: unknown, machine: unknown, words: (msg: string) => string = (m) => m): string {
|
|
34
|
+
if (e instanceof Unreachable && (e.who !== 'web' || machine != null)) return ''
|
|
35
|
+
return words((e as Error).message)
|
|
36
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// empty page shows, so an edited README appears without a reload
|
|
3
3
|
import { makeAutoObservable, runInAction } from 'mobx'
|
|
4
4
|
import type { RepoSt } from './repo-st.ts'
|
|
5
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
5
6
|
|
|
6
7
|
export class ReadmeSvc {
|
|
7
8
|
/** the file name at the root, null when the repo has none */
|
|
@@ -19,7 +20,7 @@ export class ReadmeSvc {
|
|
|
19
20
|
|
|
20
21
|
async load(): Promise<void> {
|
|
21
22
|
try {
|
|
22
|
-
const r = await
|
|
23
|
+
const r = await reach(this.st.url('/api/readme.json'))
|
|
23
24
|
if (!r.ok) throw new Error(`http ${r.status}: ${await r.text()}`)
|
|
24
25
|
const j = (await r.json()) as { file: string | null; md: string | null }
|
|
25
26
|
runInAction(() => {
|
|
@@ -30,7 +31,7 @@ export class ReadmeSvc {
|
|
|
30
31
|
})
|
|
31
32
|
} catch (e) {
|
|
32
33
|
runInAction(() => {
|
|
33
|
-
this.error = `README fetch broke: ${
|
|
34
|
+
this.error = ownTrouble(e, this.st.home.machine, (m) => `README fetch broke: ${m}`) || null
|
|
34
35
|
this.loaded = true
|
|
35
36
|
})
|
|
36
37
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// A repo page's sections: the pages reached for most, the top rows of the
|
|
2
|
+
// left pane's tree (tree-row.tsx draws them the way the project list draws its
|
|
3
|
+
// lines: an icon, a label, a count where it says something, its ⌥ shortcut at
|
|
4
|
+
// the end, keys.ts, the current one lit). A page this repo does not have is
|
|
5
|
+
// not a row. The counts read the severity filter, like everything under them,
|
|
6
|
+
// and come from what the page already holds (the kept screen on a reload), so
|
|
7
|
+
// none arrives late and moves the tree.
|
|
8
|
+
import { RUNS_ID } from '../../collections/collection-types.ts'
|
|
9
|
+
import type { CheckOwner, HashSel } from './hash-route.ts'
|
|
10
|
+
import { sectionLabel, type SectionId } from './keys.ts'
|
|
11
|
+
import type { RepoSt } from './repo-st.ts'
|
|
12
|
+
import { passesFilter } from './tree-build.ts'
|
|
13
|
+
import { openWikis, WIKIS } from './wikis-view.tsx'
|
|
14
|
+
|
|
15
|
+
export type RepoSection = {
|
|
16
|
+
id: SectionId
|
|
17
|
+
label: string
|
|
18
|
+
title: string
|
|
19
|
+
/** null when a number would say nothing */
|
|
20
|
+
count: number | null
|
|
21
|
+
tone: 'red' | 'warn' | null
|
|
22
|
+
on: boolean
|
|
23
|
+
go: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const counted = (n: number, tone: 'red' | 'warn'): Pick<RepoSection, 'count' | 'tone'> => (n === 0 ? { count: null, tone: null } : { count: n, tone })
|
|
27
|
+
|
|
28
|
+
/** the section a page belongs to: a collection, one of its items, its own
|
|
29
|
+
* page. null for a page no section owns (the table of every check). A check
|
|
30
|
+
* lives under whose it is, which only the resource knows: `ownerOf` reads it */
|
|
31
|
+
export function sectionOfSel(sel: HashSel | null, ownerOf: (checkItemId: string) => CheckOwner | null = () => null): SectionId | null {
|
|
32
|
+
if (sel == null) return 'readme'
|
|
33
|
+
switch (sel.t) {
|
|
34
|
+
case 'readme': return 'readme'
|
|
35
|
+
case 'processes': return 'processes'
|
|
36
|
+
case 'plan': return 'plan'
|
|
37
|
+
case 'git': return 'git'
|
|
38
|
+
case 'when': return 'when'
|
|
39
|
+
case 'checks': return sel.from
|
|
40
|
+
case 'admin': return 'admin'
|
|
41
|
+
case 'tests': return 'tests'
|
|
42
|
+
case 'asset': case 'cat': return 'media'
|
|
43
|
+
case 'status': return null
|
|
44
|
+
case 'col': case 'item': {
|
|
45
|
+
const col = sel.t === 'col' ? sel.id : sel.col
|
|
46
|
+
if (col === RUNS_ID) return 'runs'
|
|
47
|
+
if (col === WIKIS) return 'wikis'
|
|
48
|
+
if (col === 'contracts') return 'contracts'
|
|
49
|
+
if (col === 'creds') return 'secrets'
|
|
50
|
+
if (col === 'checks') return sel.t === 'item' ? ownerOf(sel.id) : null
|
|
51
|
+
return col === 'asks' || col === 'tests' || col === 'reflections' ? col : null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** the section a repo's current page belongs to, a check read off its
|
|
57
|
+
* resource for whose it is */
|
|
58
|
+
export const sectionOfRepo = (app: RepoSt): SectionId | null =>
|
|
59
|
+
sectionOfSel(app.sel, (id) => app.cols.find('checks', id)?.check?.from ?? null)
|
|
60
|
+
|
|
61
|
+
/** open a section's own page, through the calls a click makes */
|
|
62
|
+
export function openSection(app: RepoSt, id: SectionId): void {
|
|
63
|
+
switch (id) {
|
|
64
|
+
case 'readme': return app.showReadme()
|
|
65
|
+
case 'contracts': return app.showCol('contracts')
|
|
66
|
+
case 'shipkit': case 'repo': return app.showChecks(id)
|
|
67
|
+
case 'secrets': return app.showCol('creds')
|
|
68
|
+
case 'runs': return app.cols.select('runs', RUNS_ID)
|
|
69
|
+
case 'processes': return app.showProcesses(null)
|
|
70
|
+
case 'plan': return app.showPlan()
|
|
71
|
+
case 'git': return app.showGit()
|
|
72
|
+
case 'when': return app.showWhen()
|
|
73
|
+
case 'reflections': return app.showCol('reflections')
|
|
74
|
+
case 'asks': return app.showCol('asks')
|
|
75
|
+
case 'tests': return app.showTests(null)
|
|
76
|
+
case 'media': {
|
|
77
|
+
const cat = app.media.assets[0]?.cat ?? app.cols.cachedCat ?? undefined
|
|
78
|
+
if (cat != null) app.showCat(cat)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
case 'wikis': return openWikis(app)
|
|
82
|
+
case 'admin': return app.showAdmin()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function repoSections(app: RepoSt): RepoSection[] {
|
|
87
|
+
const has = (col: string): boolean => app.cols.colMeta.some((c) => c.id === col)
|
|
88
|
+
const cur = sectionOfRepo(app)
|
|
89
|
+
const res = app.cols.resources.filter((r) => passesFilter(r, app.filter))
|
|
90
|
+
const reds = (col: string): number => res.filter((r) => r.collection === col && r.state === 'red').length
|
|
91
|
+
const none = { count: null, tone: null }
|
|
92
|
+
const line = (id: SectionId, title: string, count: Pick<RepoSection, 'count' | 'tone'>): RepoSection => ({ id, label: sectionLabel(id), title, ...count, on: cur === id, go: () => openSection(app, id) })
|
|
93
|
+
const out: RepoSection[] = [line('readme', 'the README, what the repo is', none)]
|
|
94
|
+
const ownReds = (from: CheckOwner): number => res.filter((r) => r.check?.from === from && r.state === 'red').length
|
|
95
|
+
if (has('contracts')) out.push(line('contracts', 'what a release venue asks of this repo, and what it still lacks', counted(reds('contracts'), 'red')))
|
|
96
|
+
if (has('checks')) out.push(line('shipkit', 'the checks shipkit ships to every repo, one page each', counted(ownReds('shipkit'), 'red')))
|
|
97
|
+
if (has('checks')) out.push(line('repo', 'the checks this repo declares itself, one page each', counted(ownReds('repo'), 'red')))
|
|
98
|
+
if (has('creds')) out.push(line('secrets', 'the credentials this repo needs, present or missing', counted(reds('creds'), 'red')))
|
|
99
|
+
if (has('checks')) out.push(line('when', 'what runs when: every check on every context (commit, ci, full, release, health, save, clock), with the legend', none))
|
|
100
|
+
// a page, never a resource: every hub keeps a run record
|
|
101
|
+
out.push(line('runs', 'every run on record, the slowest checks and the longest runs', none))
|
|
102
|
+
// the plan and git come from the machine, so a hub of its own has neither
|
|
103
|
+
if (app.home.machine != null) out.push(line('plan', 'what the machine runs here on its own time, and when', none))
|
|
104
|
+
const procs = app.globalProcesses
|
|
105
|
+
if (procs.length > 0) out.push(line('processes', 'the processes this repo declares: start, stop, open, their description and their log', counted(procs.filter((r) => r.dot === 'crashed').length, 'red')))
|
|
106
|
+
if (app.home.machine != null) out.push(line('git', 'the branch, its upstream, what is not committed, the last commits', none))
|
|
107
|
+
if (has('asks')) {
|
|
108
|
+
const open = res.filter((r) => r.collection === 'asks' && r.type === 'ask' && r.slot !== 'done').length
|
|
109
|
+
out.push(line('asks', 'what waits on a human', counted(open, 'warn')))
|
|
110
|
+
}
|
|
111
|
+
if (has('reflections')) out.push(line('reflections', 'friction this repo logged against the wiki, waiting to be folded in', counted(res.filter((r) => r.type === 'reflection').length, 'warn')))
|
|
112
|
+
if (has('tests')) out.push(line('tests', 'every test file, slowest first, then each test check', counted(reds('tests'), 'red')))
|
|
113
|
+
if ((app.media.assets[0]?.cat ?? app.cols.cachedCat) != null) out.push(line('media', 'the assets, in the atelier', none))
|
|
114
|
+
if (has(WIKIS)) out.push(line('wikis', 'every wiki this repo mounts, as a link graph', none))
|
|
115
|
+
if (app.home.machine != null) out.push(line('admin', 'reload the hub, keep the repo active, archive it', none))
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { makeAutoObservable } from 'mobx'
|
|
9
9
|
import { ClockSvc } from './clock-svc.ts'
|
|
10
10
|
import { ColsSvc } from './cols-svc.ts'
|
|
11
|
-
import { canonicalSel, currentHash, hashFor, parseHash, selOf, type HashSel } from './hash-route.ts'
|
|
11
|
+
import { canonicalSel, currentHash, hashFor, parseHash, selOf, type CheckOwner, type HashSel } from './hash-route.ts'
|
|
12
12
|
import { ComfySvc } from './comfy-svc.ts'
|
|
13
13
|
import { HealthSvc } from './health-svc.ts'
|
|
14
14
|
import { ReadmeSvc } from './readme-svc.ts'
|
|
@@ -17,17 +17,18 @@ import { MediaSvc } from './media-svc.ts'
|
|
|
17
17
|
import { TimelineSvc } from './timeline-svc.ts'
|
|
18
18
|
import { TreeSvc } from './tree-svc.ts'
|
|
19
19
|
import { ChecksSvc } from './checks-svc.ts'
|
|
20
|
-
import {
|
|
20
|
+
import { GitSt } from './git-st.ts'
|
|
21
|
+
import type { ProcessRow } from '../../home/process-rows.ts'
|
|
21
22
|
import { TestsSvc } from './tests-svc.ts'
|
|
22
23
|
import { AdminSvc } from './admin-svc.ts'
|
|
23
|
-
import { repoSections } from './repo-sections.
|
|
24
|
+
import { repoSections } from './repo-sections.ts'
|
|
24
25
|
import { sectionKeyLabel } from './keys.ts'
|
|
25
26
|
import type { Resource } from './hub-types.ts'
|
|
26
27
|
import type { HomeSt } from './home-st.ts'
|
|
27
28
|
|
|
28
29
|
export type Sel = HashSel | null
|
|
29
30
|
|
|
30
|
-
/** one picker target (cmd+
|
|
31
|
+
/** one picker target (cmd+j): run() fires the SAME action clicking would */
|
|
31
32
|
/** what a picker entry wears: a repo's own icon, or a place's button icon */
|
|
32
33
|
export type JumpFace = { repo: string } | { place: 'home' | 'planned' | 'activity' | 'archived' }
|
|
33
34
|
/** `key`: the shortcut that does the same, printed at the entry's end */
|
|
@@ -59,7 +60,8 @@ export class RepoSt {
|
|
|
59
60
|
timeline: TimelineSvc
|
|
60
61
|
/** the runs in flight, whoever started them (live-svc.ts) */
|
|
61
62
|
live: LiveSvc
|
|
62
|
-
|
|
63
|
+
/** the git page's one repo read (git-st.ts) */
|
|
64
|
+
git: GitSt
|
|
63
65
|
/** the pages behind the test groups of the tree (tests-svc.ts) */
|
|
64
66
|
tests: TestsSvc
|
|
65
67
|
/** the admin page: archive behind its confirmation, reload, keep active */
|
|
@@ -98,10 +100,10 @@ export class RepoSt {
|
|
|
98
100
|
this.comfy = new ComfySvc(this)
|
|
99
101
|
this.timeline = new TimelineSvc(this)
|
|
100
102
|
this.live = new LiveSvc(this)
|
|
101
|
-
this.
|
|
103
|
+
this.git = new GitSt(this)
|
|
102
104
|
this.tests = new TestsSvc(this)
|
|
103
105
|
this.admin = new AdminSvc(this)
|
|
104
|
-
makeAutoObservable<this, 'beat' | 'onceRead'>(this, { onceRead: false, home: false, key: false, base: false, clock: false, media: false, cols: false, tree: false, health: false, readme: false, checks: false, comfy: false, timeline: false, live: false,
|
|
106
|
+
makeAutoObservable<this, 'beat' | 'onceRead'>(this, { onceRead: false, home: false, key: false, base: false, clock: false, media: false, cols: false, tree: false, health: false, readme: false, checks: false, comfy: false, timeline: false, live: false, git: false, tests: false, admin: false, searchEl: false, beat: false })
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
/** an absolute hub path ('/api/…', '/media/…', '/hub.json') as this
|
|
@@ -150,7 +152,6 @@ export class RepoSt {
|
|
|
150
152
|
const steps: (() => Promise<unknown>)[] = [
|
|
151
153
|
() => this.cols.poll(),
|
|
152
154
|
() => this.readOnce(),
|
|
153
|
-
() => this.processes.load(),
|
|
154
155
|
() => this.media.heartbeat(),
|
|
155
156
|
() => this.live.poll(),
|
|
156
157
|
() => this.timeline.load(),
|
|
@@ -171,7 +172,6 @@ export class RepoSt {
|
|
|
171
172
|
this.applyHash()
|
|
172
173
|
}
|
|
173
174
|
this.clock.start()
|
|
174
|
-
this.processes.start()
|
|
175
175
|
this.media.start()
|
|
176
176
|
this.cols.start()
|
|
177
177
|
this.live.start()
|
|
@@ -182,7 +182,6 @@ export class RepoSt {
|
|
|
182
182
|
if (!this.polling) return
|
|
183
183
|
this.polling = false
|
|
184
184
|
this.clock.stop()
|
|
185
|
-
this.processes.stop()
|
|
186
185
|
this.media.stop()
|
|
187
186
|
this.cols.stop()
|
|
188
187
|
this.live.stop()
|
|
@@ -276,6 +275,7 @@ export class RepoSt {
|
|
|
276
275
|
* its own item card. A gate check has a page of its own now, so nothing
|
|
277
276
|
* lands on the raw wall of text unless it is asked for by name */
|
|
278
277
|
openResource(r: Resource): void {
|
|
278
|
+
if (r.type === 'process') return this.showProcesses(r.id.replace(/^process:/, ''))
|
|
279
279
|
const assetId = r.id.replace(/^media:/, '')
|
|
280
280
|
if (r.collection === 'media' && this.media.assets.some((a) => a.id === assetId)) {
|
|
281
281
|
this.selectAsset(assetId)
|
|
@@ -290,6 +290,37 @@ export class RepoSt {
|
|
|
290
290
|
this.setSel({ t: 'admin' })
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/** the repo's README, the page a repo opens on */
|
|
294
|
+
showReadme(): void {
|
|
295
|
+
this.setSel({ t: 'readme' })
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** the global processes this repo declares, as pm runs them. None on a
|
|
299
|
+
* hub of its own, since only the home talks to pm */
|
|
300
|
+
get globalProcesses(): ProcessRow[] {
|
|
301
|
+
return this.home.machine?.processGroups.find((g) => g.core !== true && g.repo === this.key)?.rows ?? []
|
|
302
|
+
}
|
|
303
|
+
/** what the machine plans to run here */
|
|
304
|
+
showPlan(): void {
|
|
305
|
+
this.setSel({ t: 'plan' })
|
|
306
|
+
}
|
|
307
|
+
/** what runs when: every check on every context */
|
|
308
|
+
showWhen(): void {
|
|
309
|
+
this.setSel({ t: 'when' })
|
|
310
|
+
}
|
|
311
|
+
/** the checks one owner ships, as the checks table */
|
|
312
|
+
showChecks(from: CheckOwner): void {
|
|
313
|
+
this.setSel({ t: 'checks', from })
|
|
314
|
+
}
|
|
315
|
+
/** the repo's git */
|
|
316
|
+
showGit(): void {
|
|
317
|
+
this.setSel({ t: 'git' })
|
|
318
|
+
}
|
|
319
|
+
/** the processes page, `id` the card to scroll to and light */
|
|
320
|
+
showProcesses(id: string | null): void {
|
|
321
|
+
this.setSel({ t: 'processes', id })
|
|
322
|
+
}
|
|
323
|
+
|
|
293
324
|
/** collection view — one card per instance */
|
|
294
325
|
showCol(id: string): void {
|
|
295
326
|
this.setSel({ t: 'col', id })
|
|
@@ -304,6 +335,12 @@ export class RepoSt {
|
|
|
304
335
|
case 'status': return this.showStatus()
|
|
305
336
|
case 'tests': return this.showTests(sel.check)
|
|
306
337
|
case 'admin': return this.showAdmin()
|
|
338
|
+
case 'readme': return this.showReadme()
|
|
339
|
+
case 'processes': return this.showProcesses(sel.id)
|
|
340
|
+
case 'plan': return this.showPlan()
|
|
341
|
+
case 'git': return this.showGit()
|
|
342
|
+
case 'when': return this.showWhen()
|
|
343
|
+
case 'checks': return this.showChecks(sel.from)
|
|
307
344
|
}
|
|
308
345
|
}
|
|
309
346
|
/** the url decides the page: a reload, back, forward and a pasted link
|
|
@@ -7,7 +7,7 @@ export type RunsTab = 'runs' | 'files' | 'slowest' | 'longest'
|
|
|
7
7
|
|
|
8
8
|
/** the four lists, in tab order. `key` is what the address carries */
|
|
9
9
|
export const RUNS_TABS: { key: RunsTab; label: string; help: string }[] = [
|
|
10
|
-
{ key: 'runs', label: 'runs', help: `every run on record, newest first: shipkit ci, shipkit check, the hub's buttons, and the runs the plan starts. One line per run: when, who started it, what ran, its wall time, how many checks ${RAN}, how many were ${CACHED} (answered from the cache, nothing ran), and its reds. Unfold a run for one line per check, named by its title with its id as the small tag, in a box that scrolls: its reds first, then what ${RAN}, slowest first, then what was ${CACHED}. Click a check for its page. Every run in time, and how many ran at once, is the chart
|
|
10
|
+
{ key: 'runs', label: 'runs', help: `every run on record, newest first: shipkit ci, shipkit check, the hub's buttons, and the runs the plan starts. One line per run: when, who started it, what ran, its wall time, how many checks ${RAN}, how many were ${CACHED} (answered from the cache, nothing ran), and its reds. Unfold a run for one line per check, named by its title with its id as the small tag, in a box that scrolls: its reds first, then what ${RAN}, slowest first, then what was ${CACHED}. Click a check for its page. Every run in time, and how many ran at once, is the chart under this list: press fit for the whole record` },
|
|
11
11
|
{ key: 'files', label: 'test files to fix', help: "bun test files, not checks: each file's last recorded run inside the check that ran it (test, test:<unit>), kept when a test in it failed or when it broke the quick tier budget per file. The same verdict colours the file in the tree under tests. Click a file for its page, the check tag for the check" },
|
|
12
12
|
{ key: 'slowest', label: 'slowest checks', help: 'the checks that cost the most when they really run: the mean and the worst of the runs that ran. A check whose worst run passed half its ceiling is flagged, because the next slow machine trips it' },
|
|
13
13
|
{ key: 'longest', label: 'longest runs', help: 'the invocations on record that took the longest, start to end, whatever ran them' },
|
|
@@ -1,47 +1,31 @@
|
|
|
1
|
-
// A repo page's left pane: its
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// the filter
|
|
1
|
+
// A repo page's left pane: the filter with its severity group on one line,
|
|
2
|
+
// then ONE tree whose top rows are the repo's sections (tree-svc.ts) and
|
|
3
|
+
// whose rows under them are every resource of the repo. Only the tree
|
|
4
|
+
// scrolls, so the filter stays in reach. The row itself is tree-row.tsx.
|
|
5
5
|
import { observer } from 'mobx-react-lite'
|
|
6
|
-
import { Help } from './help.tsx'
|
|
7
6
|
import { SEARCH_KEY } from './keys.ts'
|
|
8
|
-
import { Sections } from './repo-sections.tsx'
|
|
9
7
|
import type { RepoSt } from './repo-st.ts'
|
|
10
|
-
import { TYPE_MARK } from './tree-build.ts'
|
|
11
8
|
import { TreeRowView } from './tree-row.tsx'
|
|
12
9
|
|
|
13
10
|
const Tree = observer(({ app }: { app: RepoSt }) => {
|
|
14
11
|
const t = app.tree
|
|
12
|
+
const empty = app.cols.loaded && app.filter !== 'all' && app.query.trim() === '' && t.visible.length === 0
|
|
15
13
|
return (
|
|
16
14
|
// roving focus: the arrows belong to the tree only while it HAS the
|
|
17
15
|
// focus, so the atelier keeps its own keys
|
|
18
16
|
<div className="tree" tabIndex={0} onKeyDown={(e) => { if (t.key(e.key)) e.preventDefault() }}>
|
|
19
17
|
{t.rows.map((r) => <TreeRowView key={r.key} app={app} row={r} />)}
|
|
20
18
|
{!app.cols.loaded && !app.cols.cached ? <div className="tempty twait"><span className="spin" />reading the repo…</div> : null}
|
|
21
|
-
{
|
|
19
|
+
{empty ? <div className="tempty">{app.filter === 'red' ? 'no reds — nothing is blocked ✨' : 'nothing needs you ✨'}</div> : null}
|
|
20
|
+
{t.hidden > 0 ? (
|
|
21
|
+
<div className="tempty thidden" role="button" title="show every row (a)" onClick={() => app.setFilter('all')}>
|
|
22
|
+
{t.hidden} {t.hidden === 1 ? 'row' : 'rows'} hidden by <b>{app.filter}</b> · show all <kbd className="pjkey">a</kbd>
|
|
23
|
+
</div>
|
|
24
|
+
) : null}
|
|
22
25
|
</div>
|
|
23
26
|
)
|
|
24
27
|
})
|
|
25
28
|
|
|
26
|
-
/** what every glyph in the type slot means, behind one `(?)` */
|
|
27
|
-
const TypeLegend = () => (
|
|
28
|
-
<Help text={
|
|
29
|
-
<span className="legend">
|
|
30
|
-
{Object.entries(TYPE_MARK).map(([kind, m]) => (
|
|
31
|
-
<span key={kind} className="lrow"><i className={`ty ${m.hue}`}>{m.glyph}</i>{m.word}</span>
|
|
32
|
-
))}
|
|
33
|
-
</span>
|
|
34
|
-
} />
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
const TreeTabs = observer(({ app }: { app: RepoSt }) => (
|
|
38
|
-
<span className="seg">
|
|
39
|
-
{(['path', 'type'] as const).map((t) => (
|
|
40
|
-
<button key={t} className={app.tree.tab === t ? 'on' : ''} onClick={() => app.tree.setTab(t)}>by {t}</button>
|
|
41
|
-
))}
|
|
42
|
-
</span>
|
|
43
|
-
))
|
|
44
|
-
|
|
45
29
|
// segmented severity filter — all · needs (yellow+red) · red, the address
|
|
46
30
|
// rides at /?f=… so a filtered view is shareable. The shortcut letter rides
|
|
47
31
|
// INSIDE its own button, so the binding is discoverable without a legend
|
|
@@ -58,24 +42,15 @@ const SegFilter = observer(({ app }: { app: RepoSt }) => (
|
|
|
58
42
|
|
|
59
43
|
export const Sidebar = observer(({ app }: { app: RepoSt }) => (
|
|
60
44
|
<aside>
|
|
61
|
-
<Sections app={app} />
|
|
62
45
|
<div className="ssearch">
|
|
63
46
|
<span className="sfield">
|
|
64
|
-
<input ref={(el) => { app.searchEl = el }} placeholder="filter" aria-label="filter the tree" value={app.query}
|
|
47
|
+
<input ref={(el) => { app.searchEl = el }} placeholder="filter" aria-label="filter the tree by name or path" value={app.query}
|
|
65
48
|
onChange={(e) => app.setQuery(e.target.value)}
|
|
66
49
|
onKeyDown={(e) => { if (e.key === 'Escape') app.setQuery('') }} />
|
|
67
50
|
<kbd className="skey">{SEARCH_KEY}</kbd>
|
|
68
51
|
</span>
|
|
69
52
|
<SegFilter app={app} />
|
|
70
53
|
</div>
|
|
71
|
-
<div className="stabs">
|
|
72
|
-
<TreeTabs app={app} />
|
|
73
|
-
<span className="sp" />
|
|
74
|
-
<TypeLegend />
|
|
75
|
-
<span className="cnt">{app.tree.visible.length}</span>
|
|
76
|
-
</div>
|
|
77
|
-
{/* its line is held while the repo is read, so the tree never drops under it */}
|
|
78
|
-
{app.tree.whenLine === '' && app.cols.loaded ? null : <div className="swhen">{app.tree.whenLine === '' ? ' ' : app.tree.whenLine}</div>}
|
|
79
54
|
{app.media.dead != null ? <div className="tempty"><span className="warnline">media API down: {app.media.dead}</span></div> : null}
|
|
80
55
|
<div className="tscroll">
|
|
81
56
|
<Tree app={app} />
|