rvlib-shipkit 1.4.0 → 1.6.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 +114 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/assets/gen/comfyts-gen.ts +12 -8
- package/src/assets/server/studio-server.ts +14 -2
- package/src/check.ts +73 -103
- package/src/checks/SK001-package-json-shape.check.ts +11 -6
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +7 -3
- 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 +4 -2
- 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 +4 -0
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
- 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 +4 -1
- package/src/checks/SK028-readme.check.ts +5 -1
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/checks/SK030-cache-audit.check.ts +34 -0
- package/src/checks/SK031-trace-audit.check.ts +39 -0
- package/src/checks/SK032-ignored-inputs.check.ts +79 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +17 -13
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_CacheAudit.ts +43 -0
- 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 +8 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +120 -7
- package/src/cli/cmd_Home.ts +26 -19
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +20 -8
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +5 -4
- 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 +6 -4
- 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_TraceAudit.ts +62 -0
- 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 +11 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +12 -4
- package/src/collections/providers.ts +5 -3
- package/src/core/app-icon.ts +154 -0
- package/src/core/audit-report.ts +21 -0
- package/src/core/cache-audit.ts +72 -0
- package/src/core/canonical-scripts.ts +2 -2
- package/src/core/check-ctx.ts +14 -5
- package/src/core/check-runner.ts +43 -23
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/floor-resolve.ts +5 -7
- package/src/core/git-root.ts +13 -0
- package/src/core/ignored-block.ts +94 -0
- package/src/core/ignored-inputs.ts +47 -0
- package/src/core/jobs.ts +9 -2
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/one-lib.ts +59 -4
- 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/read-set.ts +97 -9
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +12 -9
- package/src/core/run-ci.ts +14 -5
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +48 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/trace-audit.ts +120 -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 +10 -14
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/gui-app.ts +24 -27
- package/src/home/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/home-start.ts +11 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/open-app.ts +2 -2
- package/src/home/pm.ts +8 -0
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/via-hub.ts +14 -0
- package/src/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/db.ts +2 -13
- package/src/ledger/gate-checks.ts +133 -61
- package/src/ledger/git-key.ts +153 -31
- package/src/ledger/git-scan.ts +5 -2
- package/src/ledger/ledger.ts +136 -21
- package/src/ledger/store.ts +14 -0
- package/src/ledger/toolchain-identity.ts +248 -45
- package/src/plan/repo-source.ts +18 -39
- package/src/problems/extract.ts +3 -0
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +46 -54
- 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 +1 -1
- package/src/serve/hub-app/admin-svc.ts +36 -3
- package/src/serve/hub-app/admin-view.tsx +46 -12
- 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 +14 -35
- package/src/serve/hub-app/col-card.tsx +1 -44
- package/src/serve/hub-app/col-tables-view.tsx +84 -11
- package/src/serve/hub-app/col-tables.ts +27 -0
- package/src/serve/hub-app/cols-svc.ts +5 -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 +6 -0
- 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 +14 -2
- package/src/serve/hub-app/home-strip.css +43 -12
- package/src/serve/hub-app/home-strip.tsx +115 -64
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +32 -8
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +89 -63
- package/src/serve/hub-app/icons.tsx +11 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +15 -4
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +18 -22
- 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 +45 -0
- package/src/serve/hub-app/project-list.css +4 -1
- package/src/serve/hub-app/project-list.tsx +6 -3
- package/src/serve/hub-app/projects-svc.ts +25 -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 +131 -0
- package/src/serve/hub-app/repo-st.ts +70 -14
- package/src/serve/hub-app/sidebar.tsx +17 -42
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/tree-build.ts +151 -155
- package/src/serve/hub-app/tree-row.tsx +55 -12
- package/src/serve/hub-app/tree-svc.ts +97 -56
- 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 +7 -18
- package/src/serve/project-gutter-route.ts +22 -1
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/suite/judge-suites.ts +6 -0
- package/src/trace/interpose.c +655 -0
- package/src/trace/read-trace.ts +485 -0
- package/src/trace/tracer.ts +282 -0
- 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
|
@@ -21,11 +21,14 @@
|
|
|
21
21
|
.pjarow span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
22
22
|
.pjarow img{flex:none;border-radius:3px;object-fit:contain}
|
|
23
23
|
.pjaini{flex:none;width:14px;height:14px;border-radius:3px;background:var(--panel2)}
|
|
24
|
-
/* the foot
|
|
24
|
+
/* the foot: sort (its title names the order), the step keys hint, archived
|
|
25
25
|
carries its count and stays lit while its rows show */
|
|
26
26
|
.pgsort,.pjarbtn{flex:none;display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s1);background:transparent;border:1px solid var(--line);border-radius:3px;
|
|
27
27
|
color:var(--dim);font-size:11px;cursor:pointer}
|
|
28
28
|
.pgsort{flex:0 1 auto;min-width:0}
|
|
29
|
+
/* the step keys, a hint: the repo above or below */
|
|
30
|
+
.pjstep{flex:none;display:inline-flex;gap:var(--s0);margin-left:auto;color:var(--dim);font-size:11px}
|
|
31
|
+
.pjstep kbd{padding:0 var(--s1);border:1px solid var(--line);border-radius:3px;font:inherit}
|
|
29
32
|
.pgsortl{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
30
33
|
.pgsort .hico,.pjarbtn .hico{flex:none;width:14px;height:14px}
|
|
31
34
|
.pgsort:hover,.pjarbtn:hover{color:var(--ink);border-color:var(--accent)}
|
|
@@ -24,7 +24,7 @@ import { useEffect, type CSSProperties } from 'react'
|
|
|
24
24
|
import { middleware, OPEN_MS } from './floating.tsx'
|
|
25
25
|
import { Icon, ICON } from './icons.tsx'
|
|
26
26
|
import { plainClick } from './home-st.ts'
|
|
27
|
-
import { PICKER_KEY, PLACE_KEY } from './keys.ts'
|
|
27
|
+
import { PICKER_KEY, PLACE_KEY, STEP_KEYS } from './keys.ts'
|
|
28
28
|
import { cardModel, faceOf, hueOf, SORTS, type Place, type Project, type ProjectsSvc } from './projects-svc.ts'
|
|
29
29
|
|
|
30
30
|
/** what the list needs of its page: the hub and the home both carry one */
|
|
@@ -195,6 +195,9 @@ export const ProjectList = observer(({ app }: { app: ListHost }) => {
|
|
|
195
195
|
) : null}
|
|
196
196
|
<div className="pjfoot">
|
|
197
197
|
<SortButton s={s} />
|
|
198
|
+
<span className="pjstep" title={`${STEP_KEYS[0]} and ${STEP_KEYS[1]} open the repo above or below, from any page`}>
|
|
199
|
+
{STEP_KEYS.map((k) => <kbd key={k}>{k}</kbd>)}
|
|
200
|
+
</span>
|
|
198
201
|
{s.archived.length === 0 || s.onArchived == null ? null : (
|
|
199
202
|
<button className={`pjarbtn${s.archivedOpen ? ' on' : ''}`} aria-pressed={s.archivedOpen}
|
|
200
203
|
title={`${s.archivedOpen ? 'hide' : 'show'} the ${s.archived.length} archived repos: nothing runs there, a row opens the archived page`}
|
|
@@ -213,7 +216,7 @@ const orderWords = (s: ProjectsSvc): string => {
|
|
|
213
216
|
return m === 'manual' ? 'manual order' : SORTS.find((x) => x.id === m)!.label
|
|
214
217
|
}
|
|
215
218
|
|
|
216
|
-
/** the sort button:
|
|
219
|
+
/** the sort button: its title names the order the list is in, it opens the menu,
|
|
217
220
|
* and sorts nothing itself */
|
|
218
221
|
const SortButton = observer(({ s }: { s: ProjectsSvc }) => {
|
|
219
222
|
const f = useFloating({
|
|
@@ -231,7 +234,7 @@ const SortButton = observer(({ s }: { s: ProjectsSvc }) => {
|
|
|
231
234
|
<>
|
|
232
235
|
<button ref={f.refs.setReference} className="pgsort" title={`the list is in ${orderWords(s)}: pick a sort`}
|
|
233
236
|
{...getReferenceProps({ onClick: () => s.toggleSortMenu() })}>
|
|
234
|
-
<Icon d={ICON.sort} /><span className="pgsortl">
|
|
237
|
+
<Icon d={ICON.sort} /><span className="pgsortl">sort</span>
|
|
235
238
|
</button>
|
|
236
239
|
{s.sortMenuOpen ? (
|
|
237
240
|
<FloatingPortal>
|
|
@@ -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
|
|
|
@@ -272,6 +277,16 @@ export class ProjectsSvc {
|
|
|
272
277
|
}
|
|
273
278
|
|
|
274
279
|
|
|
280
|
+
/** the repo above (-1) or below (+1) the selected one, round the ends;
|
|
281
|
+
* with none selected, down starts at the top and up at the bottom */
|
|
282
|
+
step(by: -1 | 1): void {
|
|
283
|
+
const n = this.list.length
|
|
284
|
+
if (n === 0) return
|
|
285
|
+
const at = this.list.findIndex((p) => this.isCurrent(p))
|
|
286
|
+
const next = at === -1 ? (by === 1 ? 0 : n - 1) : (at + by + n) % n
|
|
287
|
+
this.select(this.list[next]!.name)
|
|
288
|
+
}
|
|
289
|
+
|
|
275
290
|
/** a click selects once it is sure no second click follows */
|
|
276
291
|
click(name: string): void {
|
|
277
292
|
this.select(name)
|
|
@@ -302,7 +317,7 @@ export class ProjectsSvc {
|
|
|
302
317
|
|
|
303
318
|
private async saveIdentity(name: string, id: Identity, h: ReturnType<typeof setTimeout>): Promise<void> {
|
|
304
319
|
try {
|
|
305
|
-
const r = await
|
|
320
|
+
const r = await reach('/api/projects/identity', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, ...id }) })
|
|
306
321
|
if (!r.ok) throw new Error(await r.text())
|
|
307
322
|
const j = (await r.json()) as { identity: Identity }
|
|
308
323
|
runInAction(() => {
|
|
@@ -310,7 +325,7 @@ export class ProjectsSvc {
|
|
|
310
325
|
this.error = ''
|
|
311
326
|
})
|
|
312
327
|
} catch (e) {
|
|
313
|
-
runInAction(() => { this.error = `colours of ${name}: ${
|
|
328
|
+
runInAction(() => { this.error = ownTrouble(e, SAID_ELSEWHERE, (m) => `colours of ${name}: ${m}`) })
|
|
314
329
|
} finally {
|
|
315
330
|
// a later pick rests on its own timer, which keeps the guard
|
|
316
331
|
if (this.identitySaves.get(name) === h) this.identitySaves.delete(name)
|
|
@@ -390,11 +405,11 @@ export class ProjectsSvc {
|
|
|
390
405
|
const by = new Map(this.list.map((p) => [p.name, p]))
|
|
391
406
|
this.list = names.map((n) => by.get(n)).filter((p): p is Project => p != null)
|
|
392
407
|
try {
|
|
393
|
-
const r = await
|
|
408
|
+
const r = await reach('/api/projects/order', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ names }) })
|
|
394
409
|
if (!r.ok) throw new Error(await r.text())
|
|
395
410
|
runInAction(() => { this.error = '' })
|
|
396
411
|
} catch (e) {
|
|
397
|
-
runInAction(() => { this.error = `reorder: ${
|
|
412
|
+
runInAction(() => { this.error = ownTrouble(e, SAID_ELSEWHERE, (m) => `reorder: ${m}`) })
|
|
398
413
|
}
|
|
399
414
|
}
|
|
400
415
|
|
|
@@ -427,7 +442,7 @@ export class ProjectsSvc {
|
|
|
427
442
|
async load(): Promise<void> {
|
|
428
443
|
const order = this.orderGen
|
|
429
444
|
try {
|
|
430
|
-
const r = await
|
|
445
|
+
const r = await reach('/api/projects.json')
|
|
431
446
|
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
432
447
|
const j = (await r.json()) as { projects: Project[]; archived?: ArchivedRow[] }
|
|
433
448
|
runInAction(() => {
|
|
@@ -440,17 +455,17 @@ export class ProjectsSvc {
|
|
|
440
455
|
this.fresh = true
|
|
441
456
|
})
|
|
442
457
|
} catch (e) {
|
|
443
|
-
runInAction(() => { this.error = `projects: ${
|
|
458
|
+
runInAction(() => { this.error = ownTrouble(e, SAID_ELSEWHERE, (m) => `projects: ${m}`) })
|
|
444
459
|
}
|
|
445
460
|
}
|
|
446
461
|
|
|
447
462
|
async open(name: string): Promise<void> {
|
|
448
463
|
try {
|
|
449
|
-
const r = await
|
|
464
|
+
const r = await reach('/api/projects/open', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name }) })
|
|
450
465
|
if (!r.ok) throw new Error(await r.text())
|
|
451
466
|
runInAction(() => { this.error = '' })
|
|
452
467
|
} catch (e) {
|
|
453
|
-
runInAction(() => { this.error = `open ${name}: ${
|
|
468
|
+
runInAction(() => { this.error = ownTrouble(e, SAID_ELSEWHERE, (m) => `open ${name}: ${m}`) })
|
|
454
469
|
}
|
|
455
470
|
}
|
|
456
471
|
}
|
|
@@ -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,131 @@
|
|
|
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). Every repo draws the same rows, so
|
|
5
|
+
// the list reads the same from repo to repo: a section this repo has nothing
|
|
6
|
+
// in is a greyed row, and only opens a page that can say it is empty (the
|
|
7
|
+
// processes page). Plan, git and admin come from the machine, so a hub of its
|
|
8
|
+
// own has none of the three. The counts read the severity filter, like everything under them,
|
|
9
|
+
// and come from what the page already holds (the kept screen on a reload), so
|
|
10
|
+
// none arrives late and moves the tree.
|
|
11
|
+
import { RUNS_ID } from '../../collections/collection-types.ts'
|
|
12
|
+
import type { CheckOwner, HashSel } from './hash-route.ts'
|
|
13
|
+
import { sectionLabel, type SectionId } from './keys.ts'
|
|
14
|
+
import type { RepoSt } from './repo-st.ts'
|
|
15
|
+
import { passesFilter } from './tree-build.ts'
|
|
16
|
+
import { openWikis, WIKIS } from './wikis-view.tsx'
|
|
17
|
+
|
|
18
|
+
export type RepoSection = {
|
|
19
|
+
id: SectionId
|
|
20
|
+
label: string
|
|
21
|
+
title: string
|
|
22
|
+
/** null when a number would say nothing */
|
|
23
|
+
count: number | null
|
|
24
|
+
/** this repo has nothing there: the row is greyed */
|
|
25
|
+
empty: boolean
|
|
26
|
+
/** its click opens a page: false for an empty section with no empty page */
|
|
27
|
+
opens: boolean
|
|
28
|
+
tone: 'red' | 'warn' | null
|
|
29
|
+
on: boolean
|
|
30
|
+
go: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const counted = (n: number, tone: 'red' | 'warn'): Pick<RepoSection, 'count' | 'tone'> => (n === 0 ? { count: null, tone: null } : { count: n, tone })
|
|
34
|
+
|
|
35
|
+
/** the section a page belongs to: a collection, one of its items, its own
|
|
36
|
+
* page. null for a page no section owns (the table of every check). A check
|
|
37
|
+
* lives under whose it is, which only the resource knows: `ownerOf` reads it */
|
|
38
|
+
export function sectionOfSel(sel: HashSel | null, ownerOf: (checkItemId: string) => CheckOwner | null = () => null): SectionId | null {
|
|
39
|
+
if (sel == null) return 'readme'
|
|
40
|
+
switch (sel.t) {
|
|
41
|
+
case 'readme': return 'readme'
|
|
42
|
+
case 'processes': return 'processes'
|
|
43
|
+
case 'plan': return 'plan'
|
|
44
|
+
case 'git': return 'git'
|
|
45
|
+
case 'when': return 'when'
|
|
46
|
+
case 'checks': return sel.from
|
|
47
|
+
case 'admin': return 'admin'
|
|
48
|
+
case 'tests': return 'tests'
|
|
49
|
+
case 'asset': case 'cat': return 'media'
|
|
50
|
+
case 'status': return null
|
|
51
|
+
case 'col': case 'item': {
|
|
52
|
+
const col = sel.t === 'col' ? sel.id : sel.col
|
|
53
|
+
if (col === RUNS_ID) return 'runs'
|
|
54
|
+
if (col === WIKIS) return 'wikis'
|
|
55
|
+
if (col === 'contracts') return 'contracts'
|
|
56
|
+
if (col === 'creds') return 'secrets'
|
|
57
|
+
if (col === 'checks') return sel.t === 'item' ? ownerOf(sel.id) : null
|
|
58
|
+
return col === 'asks' || col === 'tests' || col === 'reflections' ? col : null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** the section a repo's current page belongs to, a check read off its
|
|
64
|
+
* resource for whose it is */
|
|
65
|
+
export const sectionOfRepo = (app: RepoSt): SectionId | null =>
|
|
66
|
+
sectionOfSel(app.sel, (id) => app.cols.find('checks', id)?.check?.from ?? null)
|
|
67
|
+
|
|
68
|
+
/** open a section's own page, through the calls a click makes */
|
|
69
|
+
export function openSection(app: RepoSt, id: SectionId): void {
|
|
70
|
+
switch (id) {
|
|
71
|
+
case 'readme': return app.showReadme()
|
|
72
|
+
case 'contracts': return app.showCol('contracts')
|
|
73
|
+
case 'shipkit': case 'repo': return app.showChecks(id)
|
|
74
|
+
case 'secrets': return app.showCol('creds')
|
|
75
|
+
case 'runs': return app.cols.select('runs', RUNS_ID)
|
|
76
|
+
case 'processes': return app.showProcesses(null)
|
|
77
|
+
case 'plan': return app.showPlan()
|
|
78
|
+
case 'git': return app.showGit()
|
|
79
|
+
case 'when': return app.showWhen()
|
|
80
|
+
case 'reflections': return app.showCol('reflections')
|
|
81
|
+
case 'asks': return app.showCol('asks')
|
|
82
|
+
case 'tests': return app.showTests(null)
|
|
83
|
+
case 'media': {
|
|
84
|
+
const cat = app.media.assets[0]?.cat ?? app.cols.cachedCat ?? undefined
|
|
85
|
+
if (cat != null) app.showCat(cat)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
case 'wikis': return openWikis(app)
|
|
89
|
+
case 'admin': return app.showAdmin()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** the sections whose page says it is empty, so an empty row still opens it */
|
|
94
|
+
const EMPTY_PAGES = new Set<SectionId>(['processes'])
|
|
95
|
+
|
|
96
|
+
export function repoSections(app: RepoSt): RepoSection[] {
|
|
97
|
+
const has = (col: string): boolean => app.cols.colMeta.some((c) => c.id === col)
|
|
98
|
+
const cur = sectionOfRepo(app)
|
|
99
|
+
const res = app.cols.resources.filter((r) => passesFilter(r, app.filter))
|
|
100
|
+
const reds = (col: string): number => res.filter((r) => r.collection === col && r.state === 'red').length
|
|
101
|
+
const none = { count: null, tone: null }
|
|
102
|
+
const line = (id: SectionId, title: string, count: Pick<RepoSection, 'count' | 'tone'>, empty: string | null = null): RepoSection => {
|
|
103
|
+
const opens = empty == null || EMPTY_PAGES.has(id)
|
|
104
|
+
return { id, label: sectionLabel(id), title: empty ?? title, ...(empty == null ? count : none), empty: empty != null, opens, on: opens && cur === id, go: opens ? () => openSection(app, id) : () => {} }
|
|
105
|
+
}
|
|
106
|
+
const out: RepoSection[] = [line('readme', 'the README, what the repo is', none)]
|
|
107
|
+
const ownReds = (from: CheckOwner): number => res.filter((r) => r.check?.from === from && r.state === 'red').length
|
|
108
|
+
const unless = (on: boolean, why: string): string | null => (on ? null : why)
|
|
109
|
+
out.push(line('contracts', 'what a release venue asks of this repo, and what it still lacks', counted(reds('contracts'), 'red'), unless(has('contracts'), 'no contract: no release venue applies to this repo')))
|
|
110
|
+
const noChecks = unless(has('checks'), 'no checks yet: its hub has not answered')
|
|
111
|
+
out.push(line('shipkit', 'the checks shipkit ships to every repo, one page each', counted(ownReds('shipkit'), 'red'), noChecks))
|
|
112
|
+
out.push(line('repo', 'the checks this repo declares itself, one page each', counted(ownReds('repo'), 'red'), noChecks))
|
|
113
|
+
out.push(line('secrets', 'the credentials this repo needs, present or missing', counted(reds('creds'), 'red'), unless(has('creds'), 'no secrets: this repo needs no credential')))
|
|
114
|
+
out.push(line('when', 'what runs when: every check on every context (commit, ci, full, release, health, save, clock), with the legend', none, noChecks))
|
|
115
|
+
// a page, never a resource: every hub keeps a run record
|
|
116
|
+
out.push(line('runs', 'every run on record, the slowest checks and the longest runs', none))
|
|
117
|
+
// the plan and git come from the machine, so a hub of its own has neither
|
|
118
|
+
if (app.home.machine != null) out.push(line('plan', 'what the machine runs here on its own time, and when', none))
|
|
119
|
+
const procs = app.globalProcesses
|
|
120
|
+
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'),
|
|
121
|
+
unless(procs.length > 0, 'no process: this repo declares no global process (a <id>.shipkit.ts file with globalProcess adds one)')))
|
|
122
|
+
if (app.home.machine != null) out.push(line('git', 'the branch, its upstream, what is not committed, the last commits', none))
|
|
123
|
+
const open = res.filter((r) => r.collection === 'asks' && r.type === 'ask' && r.slot !== 'done').length
|
|
124
|
+
out.push(line('asks', 'what waits on a human', counted(open, 'warn'), unless(has('asks'), 'no asks: nothing has ever waited on a human here')))
|
|
125
|
+
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'), unless(has('reflections'), 'no reflections: this repo logged no friction')))
|
|
126
|
+
out.push(line('tests', 'every test file, slowest first, then each test check', counted(reds('tests'), 'red'), unless(has('tests'), 'no tests: this repo has no test file')))
|
|
127
|
+
out.push(line('media', 'the assets, in the atelier', none, unless((app.media.assets[0]?.cat ?? app.cols.cachedCat) != null, 'no media: this repo has no asset kit')))
|
|
128
|
+
out.push(line('wikis', 'every wiki this repo mounts, as a link graph', none, unless(has(WIKIS), 'no wiki: this repo mounts none')))
|
|
129
|
+
if (app.home.machine != null) out.push(line('admin', 'reload the hub, keep the repo active, archive it', none))
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
@@ -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,10 +17,11 @@ 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'
|
|
@@ -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()
|
|
@@ -228,9 +227,9 @@ export class RepoSt {
|
|
|
228
227
|
out.push({ id: `asset:${a.id}`, label: a.label, section: a.cat, run: () => this.selectAsset(a.id) })
|
|
229
228
|
// the section lines, with their keys: the same list the left pane draws
|
|
230
229
|
const secs = repoSections(this)
|
|
231
|
-
for (const s of secs) out.push({ id: `section:${s.id}`, label: s.label, section: 'section', key: sectionKeyLabel(s.id), run: s.go })
|
|
230
|
+
for (const s of secs.filter((x) => x.opens)) out.push({ id: `section:${s.id}`, label: s.label, section: 'section', key: sectionKeyLabel(s.id), run: s.go })
|
|
232
231
|
for (const col of this.cols.sidebarCols) {
|
|
233
|
-
if (!secs.some((s) => s.id === col.id)) out.push({ id: `col:${col.id}`, label: col.title, section: 'section', run: () => this.showCol(col.id) })
|
|
232
|
+
if (!secs.some((s) => s.id === col.id && s.opens)) out.push({ id: `col:${col.id}`, label: col.title, section: 'section', run: () => this.showCol(col.id) })
|
|
234
233
|
for (const i of col.instances)
|
|
235
234
|
out.push({ id: `item:${i.id}`, label: i.text, section: col.title, run: () => this.cols.select(col.id, i.id) })
|
|
236
235
|
}
|
|
@@ -257,10 +256,27 @@ export class RepoSt {
|
|
|
257
256
|
void this.tests.load(check)
|
|
258
257
|
}
|
|
259
258
|
|
|
259
|
+
/** a walk with the tree's arrows is under way, and whether the current
|
|
260
|
+
* history entry is its own: a held arrow opens ~30 pages a second, and
|
|
261
|
+
* the browser throws past 100 pushState calls per 10 seconds */
|
|
262
|
+
private walking = false
|
|
263
|
+
private walked = false
|
|
264
|
+
/** open pages as one walk: the first step is a history entry, every next
|
|
265
|
+
* one rewrites it, so back returns to where the walk started */
|
|
266
|
+
walk(open: () => void): void {
|
|
267
|
+
this.walking = true
|
|
268
|
+
try {
|
|
269
|
+
open()
|
|
270
|
+
} finally {
|
|
271
|
+
this.walking = false
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
260
275
|
/** the ONE place a selection is written, so the url always describes what
|
|
261
276
|
* is on screen. A new page is a history entry, so back returns to the
|
|
262
|
-
* page before it. The same address is never
|
|
263
|
-
* lands here too, with the hash already naming
|
|
277
|
+
* page before it, except inside a walk (walk). The same address is never
|
|
278
|
+
* pushed twice: applyHash lands here too, with the hash already naming
|
|
279
|
+
* this selection */
|
|
264
280
|
setSel(sel: Sel): void {
|
|
265
281
|
this.sel = sel
|
|
266
282
|
const h = hashFor(sel)
|
|
@@ -270,12 +286,15 @@ export class RepoSt {
|
|
|
270
286
|
// the tool set rides after the asset id (media-svc syncHash), so an
|
|
271
287
|
// address is the same page whatever tools it carries
|
|
272
288
|
if (cur.kind === next.kind && cur.col === next.col && cur.id === next.id) return
|
|
273
|
-
history.
|
|
289
|
+
if (this.walking && this.walked) history.replaceState(null, '', `#${h}`)
|
|
290
|
+
else history.pushState(null, '', `#${h}`)
|
|
291
|
+
this.walked = this.walking
|
|
274
292
|
}
|
|
275
293
|
/** one tree row, opened: an asset goes to the atelier, everything else to
|
|
276
294
|
* its own item card. A gate check has a page of its own now, so nothing
|
|
277
295
|
* lands on the raw wall of text unless it is asked for by name */
|
|
278
296
|
openResource(r: Resource): void {
|
|
297
|
+
if (r.type === 'process') return this.showProcesses(r.id.replace(/^process:/, ''))
|
|
279
298
|
const assetId = r.id.replace(/^media:/, '')
|
|
280
299
|
if (r.collection === 'media' && this.media.assets.some((a) => a.id === assetId)) {
|
|
281
300
|
this.selectAsset(assetId)
|
|
@@ -290,6 +309,37 @@ export class RepoSt {
|
|
|
290
309
|
this.setSel({ t: 'admin' })
|
|
291
310
|
}
|
|
292
311
|
|
|
312
|
+
/** the repo's README, the page a repo opens on */
|
|
313
|
+
showReadme(): void {
|
|
314
|
+
this.setSel({ t: 'readme' })
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** the global processes this repo declares, as pm runs them. None on a
|
|
318
|
+
* hub of its own, since only the home talks to pm */
|
|
319
|
+
get globalProcesses(): ProcessRow[] {
|
|
320
|
+
return this.home.machine?.processGroups.find((g) => g.core !== true && g.repo === this.key)?.rows ?? []
|
|
321
|
+
}
|
|
322
|
+
/** what the machine plans to run here */
|
|
323
|
+
showPlan(): void {
|
|
324
|
+
this.setSel({ t: 'plan' })
|
|
325
|
+
}
|
|
326
|
+
/** what runs when: every check on every context */
|
|
327
|
+
showWhen(): void {
|
|
328
|
+
this.setSel({ t: 'when' })
|
|
329
|
+
}
|
|
330
|
+
/** the checks one owner ships, as the checks table */
|
|
331
|
+
showChecks(from: CheckOwner): void {
|
|
332
|
+
this.setSel({ t: 'checks', from })
|
|
333
|
+
}
|
|
334
|
+
/** the repo's git */
|
|
335
|
+
showGit(): void {
|
|
336
|
+
this.setSel({ t: 'git' })
|
|
337
|
+
}
|
|
338
|
+
/** the processes page, `id` the card to scroll to and light */
|
|
339
|
+
showProcesses(id: string | null): void {
|
|
340
|
+
this.setSel({ t: 'processes', id })
|
|
341
|
+
}
|
|
342
|
+
|
|
293
343
|
/** collection view — one card per instance */
|
|
294
344
|
showCol(id: string): void {
|
|
295
345
|
this.setSel({ t: 'col', id })
|
|
@@ -304,6 +354,12 @@ export class RepoSt {
|
|
|
304
354
|
case 'status': return this.showStatus()
|
|
305
355
|
case 'tests': return this.showTests(sel.check)
|
|
306
356
|
case 'admin': return this.showAdmin()
|
|
357
|
+
case 'readme': return this.showReadme()
|
|
358
|
+
case 'processes': return this.showProcesses(sel.id)
|
|
359
|
+
case 'plan': return this.showPlan()
|
|
360
|
+
case 'git': return this.showGit()
|
|
361
|
+
case 'when': return this.showWhen()
|
|
362
|
+
case 'checks': return this.showChecks(sel.from)
|
|
307
363
|
}
|
|
308
364
|
}
|
|
309
365
|
/** the url decides the page: a reload, back, forward and a pasted link
|
|
@@ -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 {
|
|
7
|
-
import { SEARCH_KEY } from './keys.ts'
|
|
8
|
-
import { Sections } from './repo-sections.tsx'
|
|
6
|
+
import { isTreeWalk, SEARCH_KEY } from './keys.ts'
|
|
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
|
-
//
|
|
17
|
-
// focus
|
|
18
|
-
<div className="tree"
|
|
14
|
+
// the page's key handler drives it (hub-app.tsx), so the arrows need
|
|
15
|
+
// no focus here; the atelier consumes its own keys before that
|
|
16
|
+
<div className="tree">
|
|
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
|
-
onKeyDown={(e) => { if (e.key === 'Escape') app.setQuery('') }} />
|
|
49
|
+
onKeyDown={(e) => { if (e.key === 'Escape') app.setQuery(''); else if (isTreeWalk(e.key) && app.tree.key(e.key)) e.preventDefault() }} />
|
|
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} />
|