rvlib-shipkit 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// RepoSt: one repo's hub page as a node of the page tree (home-st.ts owns
|
|
2
|
+
// the root, build/app-state-tree). Owns cross-service state: selection, the
|
|
3
|
+
// severity filter (an ADDRESS: ?f=needs · ?f=red), and the hash router: every
|
|
4
|
+
// page and every tab is a selection, every selection has an address
|
|
5
|
+
// (hash-route.ts), and setSel is the one place both move together. A repo
|
|
6
|
+
// that is not shown keeps all of it and polls nothing (resume, pause), and
|
|
7
|
+
// only the shown one writes the url.
|
|
8
|
+
import { makeAutoObservable } from 'mobx'
|
|
9
|
+
import { ClockSvc } from './clock-svc.ts'
|
|
10
|
+
import { ColsSvc } from './cols-svc.ts'
|
|
11
|
+
import { canonicalSel, currentHash, hashFor, parseHash, selOf, type HashSel } from './hash-route.ts'
|
|
12
|
+
import { ComfySvc } from './comfy-svc.ts'
|
|
13
|
+
import { HealthSvc } from './health-svc.ts'
|
|
14
|
+
import { ReadmeSvc } from './readme-svc.ts'
|
|
15
|
+
import { LiveSvc } from './live-svc.ts'
|
|
16
|
+
import { MediaSvc } from './media-svc.ts'
|
|
17
|
+
import { TimelineSvc } from './timeline-svc.ts'
|
|
18
|
+
import { TreeSvc } from './tree-svc.ts'
|
|
19
|
+
import { ChecksSvc } from './checks-svc.ts'
|
|
20
|
+
import { ProcessesSvc } from './processes-svc.ts'
|
|
21
|
+
import { TestsSvc } from './tests-svc.ts'
|
|
22
|
+
import { AdminSvc } from './admin-svc.ts'
|
|
23
|
+
import { repoSections } from './repo-sections.tsx'
|
|
24
|
+
import { sectionKeyLabel } from './keys.ts'
|
|
25
|
+
import type { Resource } from './hub-types.ts'
|
|
26
|
+
import type { HomeSt } from './home-st.ts'
|
|
27
|
+
|
|
28
|
+
export type Sel = HashSel | null
|
|
29
|
+
|
|
30
|
+
/** one picker target (cmd+k): run() fires the SAME action clicking would */
|
|
31
|
+
/** what a picker entry wears: a repo's own icon, or a place's button icon */
|
|
32
|
+
export type JumpFace = { repo: string } | { place: 'home' | 'planned' | 'activity' | 'archived' }
|
|
33
|
+
/** `key`: the shortcut that does the same, printed at the entry's end */
|
|
34
|
+
export type Jump = { id: string; label: string; section: string; run: () => void; face?: JumpFace; key?: string }
|
|
35
|
+
|
|
36
|
+
/** all · needs (yellow+red) · red — one severity ladder for every section */
|
|
37
|
+
export type Filter = 'all' | 'needs' | 'red'
|
|
38
|
+
export const parseFilter = (search: string): Filter => {
|
|
39
|
+
const f = new URLSearchParams(search).get('f')
|
|
40
|
+
return f === 'needs' || f === 'red' ? f : 'all'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class RepoSt {
|
|
44
|
+
/** the repo's name, from /hub.json; its key until that answers */
|
|
45
|
+
name: string
|
|
46
|
+
/** where this repo sits on disk, from /hub.json: what makes a source
|
|
47
|
+
* path on a page a link that opens his editor */
|
|
48
|
+
root = ''
|
|
49
|
+
/** the one second tick every age of this repo reads (clock-svc.ts). It
|
|
50
|
+
* ticks only while the repo is shown, so a hidden page repaints nothing */
|
|
51
|
+
clock: ClockSvc
|
|
52
|
+
media: MediaSvc
|
|
53
|
+
cols: ColsSvc
|
|
54
|
+
tree: TreeSvc
|
|
55
|
+
health: HealthSvc
|
|
56
|
+
readme: ReadmeSvc
|
|
57
|
+
checks: ChecksSvc
|
|
58
|
+
comfy: ComfySvc
|
|
59
|
+
timeline: TimelineSvc
|
|
60
|
+
/** the runs in flight, whoever started them (live-svc.ts) */
|
|
61
|
+
live: LiveSvc
|
|
62
|
+
processes: ProcessesSvc
|
|
63
|
+
/** the pages behind the test groups of the tree (tests-svc.ts) */
|
|
64
|
+
tests: TestsSvc
|
|
65
|
+
/** the admin page: archive behind its confirmation, reload, keep active */
|
|
66
|
+
admin: AdminSvc
|
|
67
|
+
sel: Sel = null
|
|
68
|
+
filter: Filter = 'all'
|
|
69
|
+
/** sidebar search — live substring filter over asset + collection rows */
|
|
70
|
+
query = ''
|
|
71
|
+
/** the first resume or prefetch ran: on the home, only once this repo's
|
|
72
|
+
* hub answered */
|
|
73
|
+
started = false
|
|
74
|
+
/** the page showed it once: only a repo shown has a region mounted, so a
|
|
75
|
+
* repo read ahead in the background costs no render */
|
|
76
|
+
seen = false
|
|
77
|
+
/** the sidebar search box, so `/` focuses this repo's own */
|
|
78
|
+
searchEl: HTMLInputElement | null = null
|
|
79
|
+
/** its timers run: only the shown repo's do */
|
|
80
|
+
polling = false
|
|
81
|
+
private beat = false
|
|
82
|
+
/** the hub name, the gate block and STATUS.md were asked for */
|
|
83
|
+
private onceRead = false
|
|
84
|
+
|
|
85
|
+
/** `key` names the repo in the root's map, `base` is where its hub
|
|
86
|
+
* answers: '/r/<repo>' on the home, '' on a hub of its own */
|
|
87
|
+
constructor(readonly home: HomeSt, readonly key: string, readonly base: string) {
|
|
88
|
+
this.name = key
|
|
89
|
+
// children built in the ctor body: a field initializer would run
|
|
90
|
+
// before `home` is assigned
|
|
91
|
+
this.clock = new ClockSvc()
|
|
92
|
+
this.media = new MediaSvc(this)
|
|
93
|
+
this.cols = new ColsSvc(this)
|
|
94
|
+
this.tree = new TreeSvc(this)
|
|
95
|
+
this.health = new HealthSvc(this)
|
|
96
|
+
this.readme = new ReadmeSvc(this)
|
|
97
|
+
this.checks = new ChecksSvc(this)
|
|
98
|
+
this.comfy = new ComfySvc(this)
|
|
99
|
+
this.timeline = new TimelineSvc(this)
|
|
100
|
+
this.live = new LiveSvc(this)
|
|
101
|
+
this.processes = new ProcessesSvc(this)
|
|
102
|
+
this.tests = new TestsSvc(this)
|
|
103
|
+
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, processes: false, tests: false, admin: false, searchEl: false, beat: false })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** an absolute hub path ('/api/…', '/media/…', '/hub.json') as this
|
|
108
|
+
* repo's hub answers it. Every request of the repo goes through here */
|
|
109
|
+
url(path: string): string {
|
|
110
|
+
return path.startsWith('/') ? `${this.base}${path}` : path
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** the repo the page shows now: only it writes the url */
|
|
114
|
+
get isShown(): boolean {
|
|
115
|
+
return this.home.shown === this
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** what the url carries for this repo: its filter, then its selection */
|
|
119
|
+
get search(): string {
|
|
120
|
+
return this.filter === 'all' ? '' : `?f=${this.filter}`
|
|
121
|
+
}
|
|
122
|
+
get hash(): string {
|
|
123
|
+
const h = this.sel?.t === 'asset' ? this.media.hash : hashFor(this.sel)
|
|
124
|
+
return h == null ? '' : `#${h}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** what is read once, the first time the repo starts or is read ahead */
|
|
128
|
+
private readOnce(): Promise<unknown> {
|
|
129
|
+
this.started = true
|
|
130
|
+
this.onceRead = true
|
|
131
|
+
return Promise.all([this.readHub(), this.checks.loadGate(), this.health.load()])
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** the repo's name and where it sits on disk, from /hub.json */
|
|
135
|
+
private readHub(): Promise<void> {
|
|
136
|
+
return fetch(this.url('/hub.json'))
|
|
137
|
+
.then((r) => r.json() as Promise<{ name: string; root?: string }>)
|
|
138
|
+
.then((h) => { this.setHub(h.name, h.root ?? '') })
|
|
139
|
+
.catch(() => {})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** the first screen of the repo, read in the background while another
|
|
143
|
+
* one shows, so its first visit has its data at once. One step at a
|
|
144
|
+
* time: the browser opens six connections to the home, and the repo on
|
|
145
|
+
* screen keeps most of them. Nothing polls: a resume takes over when it
|
|
146
|
+
* shows */
|
|
147
|
+
async prefetch(): Promise<void> {
|
|
148
|
+
if (this.started) return
|
|
149
|
+
this.started = true
|
|
150
|
+
const steps: (() => Promise<unknown>)[] = [
|
|
151
|
+
() => this.cols.poll(),
|
|
152
|
+
() => this.readOnce(),
|
|
153
|
+
() => this.processes.load(),
|
|
154
|
+
() => this.media.heartbeat(),
|
|
155
|
+
() => this.live.poll(),
|
|
156
|
+
() => this.timeline.load(),
|
|
157
|
+
]
|
|
158
|
+
for (const step of steps) {
|
|
159
|
+
// shown meanwhile: its resume polls everything, so stop here
|
|
160
|
+
if (this.polling) return
|
|
161
|
+
await step()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** start polling. The first one also reads what is read once */
|
|
166
|
+
resume(): void {
|
|
167
|
+
if (this.polling) return
|
|
168
|
+
this.polling = true
|
|
169
|
+
if (!this.onceRead) {
|
|
170
|
+
void this.readOnce()
|
|
171
|
+
this.applyHash()
|
|
172
|
+
}
|
|
173
|
+
this.clock.start()
|
|
174
|
+
this.processes.start()
|
|
175
|
+
this.media.start()
|
|
176
|
+
this.cols.start()
|
|
177
|
+
this.live.start()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** stop polling, keep every state: a resume shows the same page */
|
|
181
|
+
pause(): void {
|
|
182
|
+
if (!this.polling) return
|
|
183
|
+
this.polling = false
|
|
184
|
+
this.clock.stop()
|
|
185
|
+
this.processes.stop()
|
|
186
|
+
this.media.stop()
|
|
187
|
+
this.cols.stop()
|
|
188
|
+
this.live.stop()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
dispose(): void {
|
|
192
|
+
this.pause()
|
|
193
|
+
this.media.dispose()
|
|
194
|
+
this.checks.dispose()
|
|
195
|
+
this.comfy.dispose()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
markSeen(): void {
|
|
199
|
+
this.seen = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private setHub(n: string, root: string): void {
|
|
203
|
+
this.name = n
|
|
204
|
+
this.root = root
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
setFilter(v: Filter): void {
|
|
208
|
+
this.filter = v
|
|
209
|
+
if (!this.isShown) return
|
|
210
|
+
const u = new URL(location.href)
|
|
211
|
+
v === 'all' ? u.searchParams.delete('f') : u.searchParams.set('f', v)
|
|
212
|
+
history.replaceState(null, '', u)
|
|
213
|
+
}
|
|
214
|
+
/** the atelier rewrites its tool set in place: same page, no entry */
|
|
215
|
+
replaceHash(h: string): void {
|
|
216
|
+
if (this.isShown && location.hash.slice(1) !== h) history.replaceState(null, '', `#${h}`)
|
|
217
|
+
}
|
|
218
|
+
setQuery(v: string): void {
|
|
219
|
+
this.query = v
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** every jump target: section titles, media assets, collection instances */
|
|
223
|
+
get jumps(): Jump[] {
|
|
224
|
+
const out: Jump[] = []
|
|
225
|
+
for (const cat of new Set(this.media.assets.map((a) => a.cat)))
|
|
226
|
+
out.push({ id: `cat:${cat}`, label: cat, section: 'media', run: () => this.showCat(cat) })
|
|
227
|
+
for (const a of this.media.assets)
|
|
228
|
+
out.push({ id: `asset:${a.id}`, label: a.label, section: a.cat, run: () => this.selectAsset(a.id) })
|
|
229
|
+
// the section lines, with their keys: the same list the left pane draws
|
|
230
|
+
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 })
|
|
232
|
+
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) })
|
|
234
|
+
for (const i of col.instances)
|
|
235
|
+
out.push({ id: `item:${i.id}`, label: i.text, section: col.title, run: () => this.cols.select(col.id, i.id) })
|
|
236
|
+
}
|
|
237
|
+
return out
|
|
238
|
+
}
|
|
239
|
+
// ── selection + hash deep-links ───────────────────────────────────────
|
|
240
|
+
selectAsset(id: string): void {
|
|
241
|
+
this.setSel({ t: 'asset', id })
|
|
242
|
+
void this.media.select(id)
|
|
243
|
+
}
|
|
244
|
+
showCat(cat: string): void {
|
|
245
|
+
this.setSel({ t: 'cat', cat })
|
|
246
|
+
this.media.openCats.add(cat)
|
|
247
|
+
void this.media.fillCat()
|
|
248
|
+
}
|
|
249
|
+
showStatus(): void {
|
|
250
|
+
this.setSel({ t: 'status' })
|
|
251
|
+
void this.health.load()
|
|
252
|
+
}
|
|
253
|
+
/** where the test checks' time goes: the `tests` group (null) or one test
|
|
254
|
+
* check's group. Fetched as the page opens, never on the poll */
|
|
255
|
+
showTests(check: string | null): void {
|
|
256
|
+
this.setSel({ t: 'tests', check })
|
|
257
|
+
void this.tests.load(check)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** the ONE place a selection is written, so the url always describes what
|
|
261
|
+
* is on screen. A new page is a history entry, so back returns to the
|
|
262
|
+
* page before it. The same address is never pushed twice: applyHash
|
|
263
|
+
* lands here too, with the hash already naming this selection */
|
|
264
|
+
setSel(sel: Sel): void {
|
|
265
|
+
this.sel = sel
|
|
266
|
+
const h = hashFor(sel)
|
|
267
|
+
if (h == null || !this.isShown) return
|
|
268
|
+
const cur = parseHash(currentHash())
|
|
269
|
+
const next = parseHash(h)
|
|
270
|
+
// the tool set rides after the asset id (media-svc syncHash), so an
|
|
271
|
+
// address is the same page whatever tools it carries
|
|
272
|
+
if (cur.kind === next.kind && cur.col === next.col && cur.id === next.id) return
|
|
273
|
+
history.pushState(null, '', `#${h}`)
|
|
274
|
+
}
|
|
275
|
+
/** one tree row, opened: an asset goes to the atelier, everything else to
|
|
276
|
+
* its own item card. A gate check has a page of its own now, so nothing
|
|
277
|
+
* lands on the raw wall of text unless it is asked for by name */
|
|
278
|
+
openResource(r: Resource): void {
|
|
279
|
+
const assetId = r.id.replace(/^media:/, '')
|
|
280
|
+
if (r.collection === 'media' && this.media.assets.some((a) => a.id === assetId)) {
|
|
281
|
+
this.selectAsset(assetId)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
this.cols.select(r.collection, r.id)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** what administers the repo: reload its hub, keep it active, archive it */
|
|
288
|
+
showAdmin(): void {
|
|
289
|
+
this.admin.cancel()
|
|
290
|
+
this.setSel({ t: 'admin' })
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** collection view — one card per instance */
|
|
294
|
+
showCol(id: string): void {
|
|
295
|
+
this.setSel({ t: 'col', id })
|
|
296
|
+
}
|
|
297
|
+
/** open whatever a selection names, through the same calls a click makes */
|
|
298
|
+
go(sel: HashSel): void {
|
|
299
|
+
switch (sel.t) {
|
|
300
|
+
case 'asset': return this.selectAsset(sel.id)
|
|
301
|
+
case 'cat': return this.showCat(sel.cat)
|
|
302
|
+
case 'col': return this.showCol(sel.id)
|
|
303
|
+
case 'item': return this.cols.select(sel.col, sel.id)
|
|
304
|
+
case 'status': return this.showStatus()
|
|
305
|
+
case 'tests': return this.showTests(sel.check)
|
|
306
|
+
case 'admin': return this.showAdmin()
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/** the url decides the page: a reload, back, forward and a pasted link
|
|
310
|
+
* all land here, and only a hash naming something else moves the view */
|
|
311
|
+
applyHash(): void {
|
|
312
|
+
if (!this.isShown) return
|
|
313
|
+
const p = parseHash(currentHash())
|
|
314
|
+
const named = selOf(p)
|
|
315
|
+
if (named == null) return
|
|
316
|
+
// an address naming what this build opens under another address is
|
|
317
|
+
// rewritten in place: a replace, so back never lands on the stale one
|
|
318
|
+
const sel = canonicalSel(named)
|
|
319
|
+
if (sel !== named) history.replaceState(null, '', `#${hashFor(sel)}`)
|
|
320
|
+
// media addresses resolve only once the first heartbeat listed the
|
|
321
|
+
// assets (afterFirstBeat calls back)
|
|
322
|
+
if (sel.t === 'asset' && !this.media.assets.some((a) => a.id === sel.id)) return
|
|
323
|
+
if (sel.t === 'cat' && !this.media.assets.some((a) => a.cat === sel.cat)) return
|
|
324
|
+
if (sel.t === 'asset' && this.media.curId === sel.id) {
|
|
325
|
+
// same asset, new tool set (a pasted share link): apply NOW, a
|
|
326
|
+
// parked pendingTools would hijack the next selection instead
|
|
327
|
+
if (p.tools != null) this.media.applyTab(p.tools)
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
if (hashFor(sel) === hashFor(this.sel)) return
|
|
331
|
+
if (sel.t === 'asset') this.media.pendingTools = p.tools
|
|
332
|
+
this.go(sel)
|
|
333
|
+
}
|
|
334
|
+
/** first heartbeat has the asset list — only then can a hash resolve */
|
|
335
|
+
afterFirstBeat(): void {
|
|
336
|
+
if (this.beat || !this.isShown) return
|
|
337
|
+
this.beat = true
|
|
338
|
+
this.applyHash()
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -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,
|
|
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 over the tabs: 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' },
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// each repo's last first screen, kept per browser in IndexedDB (a repo's rows
|
|
2
|
+
// run to near a megabyte, past what localStorage holds for twenty repos): the
|
|
3
|
+
// rows, the collections, the plan rows, the hub problems and the first media
|
|
4
|
+
// category. A load paints it at once, then the hub's answer replaces it.
|
|
5
|
+
// Anything that fails (no IndexedDB, a private window, a blocked store) reads
|
|
6
|
+
// as nothing kept: the page then waits on the hub as it always did.
|
|
7
|
+
import type { ColMeta, HubProblem, Resource } from './hub-types.ts'
|
|
8
|
+
import type { PlanRow } from '../../plan/planner.ts'
|
|
9
|
+
|
|
10
|
+
export type Screen = {
|
|
11
|
+
at: number
|
|
12
|
+
collections: ColMeta[]
|
|
13
|
+
resources: Resource[]
|
|
14
|
+
planned: PlanRow[] | null
|
|
15
|
+
hub: HubProblem[]
|
|
16
|
+
/** the first media category, so the header's media link is there too */
|
|
17
|
+
mediaCat: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DB = 'shipkit-hub'
|
|
21
|
+
const STORE = 'screens'
|
|
22
|
+
/** the longest a load waits on the store before it paints without it */
|
|
23
|
+
export const SCREEN_WAIT_MS = 150
|
|
24
|
+
/** a repo's screen is written at most this often */
|
|
25
|
+
export const SCREEN_WRITE_MS = 60_000
|
|
26
|
+
|
|
27
|
+
let opened: Promise<IDBDatabase | null> | null = null
|
|
28
|
+
|
|
29
|
+
function db(): Promise<IDBDatabase | null> {
|
|
30
|
+
opened ??= new Promise((ok) => {
|
|
31
|
+
try {
|
|
32
|
+
const idb = globalThis.indexedDB
|
|
33
|
+
if (idb == null) return ok(null)
|
|
34
|
+
const r = idb.open(DB, 1)
|
|
35
|
+
r.onupgradeneeded = () => { r.result.createObjectStore(STORE) }
|
|
36
|
+
r.onsuccess = () => ok(r.result)
|
|
37
|
+
r.onerror = () => ok(null)
|
|
38
|
+
r.onblocked = () => ok(null)
|
|
39
|
+
} catch {
|
|
40
|
+
ok(null)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
return opened
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** the screen kept for a repo, or null */
|
|
47
|
+
export async function readScreen(key: string): Promise<Screen | null> {
|
|
48
|
+
const d = await db()
|
|
49
|
+
if (d == null) return null
|
|
50
|
+
return new Promise((ok) => {
|
|
51
|
+
try {
|
|
52
|
+
const r = d.transaction(STORE, 'readonly').objectStore(STORE).get(key)
|
|
53
|
+
r.onsuccess = () => {
|
|
54
|
+
const v = r.result as Screen | undefined
|
|
55
|
+
ok(v != null && Array.isArray(v.resources) && Array.isArray(v.collections) ? v : null)
|
|
56
|
+
}
|
|
57
|
+
r.onerror = () => ok(null)
|
|
58
|
+
} catch {
|
|
59
|
+
ok(null)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** keeps a repo's screen; a failure only means the next load waits */
|
|
65
|
+
export async function writeScreen(key: string, s: Screen): Promise<void> {
|
|
66
|
+
const d = await db()
|
|
67
|
+
if (d == null) return
|
|
68
|
+
try {
|
|
69
|
+
d.transaction(STORE, 'readwrite').objectStore(STORE).put(s, key)
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// a full or blocked store only means the next load waits on the hub
|
|
72
|
+
console.warn(`shipkit: could not keep the screen of ${key}: ${(e as Error).message}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// A repo page's left pane: its sections (repo-sections.tsx), then the tree of
|
|
2
|
+
// every resource of the repo, one row deep in its path, under its filter, its
|
|
3
|
+
// two tabs and its severity filter. Only the tree scrolls, so the sections and
|
|
4
|
+
// the filter stay in reach. The row itself is tree-row.tsx.
|
|
5
|
+
import { observer } from 'mobx-react-lite'
|
|
6
|
+
import { Help } from './help.tsx'
|
|
7
|
+
import { SEARCH_KEY } from './keys.ts'
|
|
8
|
+
import { Sections } from './repo-sections.tsx'
|
|
9
|
+
import type { RepoSt } from './repo-st.ts'
|
|
10
|
+
import { TYPE_MARK } from './tree-build.ts'
|
|
11
|
+
import { TreeRowView } from './tree-row.tsx'
|
|
12
|
+
|
|
13
|
+
const Tree = observer(({ app }: { app: RepoSt }) => {
|
|
14
|
+
const t = app.tree
|
|
15
|
+
return (
|
|
16
|
+
// roving focus: the arrows belong to the tree only while it HAS the
|
|
17
|
+
// focus, so the atelier keeps its own keys
|
|
18
|
+
<div className="tree" tabIndex={0} onKeyDown={(e) => { if (t.key(e.key)) e.preventDefault() }}>
|
|
19
|
+
{t.rows.map((r) => <TreeRowView key={r.key} app={app} row={r} />)}
|
|
20
|
+
{!app.cols.loaded && !app.cols.cached ? <div className="tempty twait"><span className="spin" />reading the repo…</div> : null}
|
|
21
|
+
{app.cols.loaded && t.rows.length === 0 ? <div className="tempty">{app.filter === 'red' ? 'no reds — nothing is blocked ✨' : app.filter === 'needs' ? 'nothing needs you ✨' : 'no resources yet'}</div> : null}
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
|
|
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
|
+
// segmented severity filter — all · needs (yellow+red) · red, the address
|
|
46
|
+
// rides at /?f=… so a filtered view is shareable. The shortcut letter rides
|
|
47
|
+
// INSIDE its own button, so the binding is discoverable without a legend
|
|
48
|
+
const FILTER_KEY = { all: 'a', needs: 'n', red: 'r' } as const
|
|
49
|
+
const SegFilter = observer(({ app }: { app: RepoSt }) => (
|
|
50
|
+
<span className="seg">
|
|
51
|
+
{(['all', 'needs', 'red'] as const).map((f) => (
|
|
52
|
+
<button key={f} className={app.filter === f ? 'on' : ''} onClick={() => app.setFilter(f)}>
|
|
53
|
+
{f}<span className="sk">{FILTER_KEY[f]}</span>
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</span>
|
|
57
|
+
))
|
|
58
|
+
|
|
59
|
+
export const Sidebar = observer(({ app }: { app: RepoSt }) => (
|
|
60
|
+
<aside>
|
|
61
|
+
<Sections app={app} />
|
|
62
|
+
<div className="ssearch">
|
|
63
|
+
<span className="sfield">
|
|
64
|
+
<input ref={(el) => { app.searchEl = el }} placeholder="filter" aria-label="filter the tree" value={app.query}
|
|
65
|
+
onChange={(e) => app.setQuery(e.target.value)}
|
|
66
|
+
onKeyDown={(e) => { if (e.key === 'Escape') app.setQuery('') }} />
|
|
67
|
+
<kbd className="skey">{SEARCH_KEY}</kbd>
|
|
68
|
+
</span>
|
|
69
|
+
<SegFilter app={app} />
|
|
70
|
+
</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
|
+
{app.media.dead != null ? <div className="tempty"><span className="warnline">media API down: {app.media.dead}</span></div> : null}
|
|
80
|
+
<div className="tscroll">
|
|
81
|
+
<Tree app={app} />
|
|
82
|
+
</div>
|
|
83
|
+
</aside>
|
|
84
|
+
))
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// tier.
|
|
6
6
|
import { observer } from 'mobx-react-lite'
|
|
7
7
|
import { durationTone, fmtMs } from './dur.ts'
|
|
8
|
-
import type {
|
|
8
|
+
import type { RepoSt } from './repo-st.ts'
|
|
9
9
|
import type { Resource, SuiteFacts, TestFacts, TestRun } from './hub-types.ts'
|
|
10
10
|
import { copy, PathCode, Row, Section } from './check-view.tsx'
|
|
11
11
|
import { editorHref, repoPath } from './check-words.ts'
|
|
@@ -34,7 +34,7 @@ const overBudget = (r: TestRun, slow: boolean): boolean => !slow && r.tier === '
|
|
|
34
34
|
|
|
35
35
|
const dot = (r: TestRun, slow: boolean): string => (r.status === 'fail' ? 'red' : overBudget(r, slow) ? 'warn' : 'ok')
|
|
36
36
|
|
|
37
|
-
const RunLine = ({ app, r, slow }: { app:
|
|
37
|
+
const RunLine = ({ app, r, slow }: { app: RepoSt; r: TestRun; slow: boolean }) => (
|
|
38
38
|
<div className={`rline rtrun${r.status === 'fail' ? ' red' : ''}`} title={stampWords(r.at)}>
|
|
39
39
|
<TimeAgo clock={app.clock} at={r.at} />
|
|
40
40
|
<span className="rid"><i className={`rdot ${dot(r, slow)}`} />{r.status === 'fail' ? 'failed' : 'passed'}</span>
|
|
@@ -43,7 +43,7 @@ const RunLine = ({ app, r, slow }: { app: HubSt; r: TestRun; slow: boolean }) =>
|
|
|
43
43
|
</div>
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
const Declaration = observer(({ app, s }: { app:
|
|
46
|
+
const Declaration = observer(({ app, s }: { app: RepoSt; s: SuiteFacts }) => {
|
|
47
47
|
const where = s.unit == null ? 'tests.slow' : `units['${s.unit}'].tests.slow`
|
|
48
48
|
return (
|
|
49
49
|
<Section title="declared slow" help="the quick tier leaves it out: its declaration says what it catches and why it cannot be fast">
|
|
@@ -67,7 +67,7 @@ const Declaration = observer(({ app, s }: { app: HubSt; s: SuiteFacts }) => {
|
|
|
67
67
|
)
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
-
export const TestCard = observer(({ app, id }: { app:
|
|
70
|
+
export const TestCard = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
71
71
|
const i: Resource | null = app.cols.find('tests', id)
|
|
72
72
|
if (i == null || i.test == null) return <div className="ph">this test file is gone: the repo moved on</div>
|
|
73
73
|
const t = i.test
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
6
6
|
import type { TestCheckView, TestsRootView } from '../../collections/test-time.ts'
|
|
7
7
|
import { testId } from '../../collections/collection-types.ts'
|
|
8
|
-
import type {
|
|
8
|
+
import type { RepoSt } from './repo-st.ts'
|
|
9
9
|
|
|
10
10
|
export type { TestCheckView, TestsRootView }
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ export class TestsSvc {
|
|
|
17
17
|
errors = new Map<string, string>()
|
|
18
18
|
loading = new Set<string>()
|
|
19
19
|
|
|
20
|
-
constructor(private st:
|
|
20
|
+
constructor(private st: RepoSt) {
|
|
21
21
|
makeAutoObservable<this, 'st'>(this, { st: false, views: observableRef })
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -45,7 +45,7 @@ export class TestsSvc {
|
|
|
45
45
|
const k = keyOf(check)
|
|
46
46
|
runInAction(() => { this.loading.add(k) })
|
|
47
47
|
try {
|
|
48
|
-
const r = await fetch(check == null ? '/api/tests.json' : `/api/tests.json?check=${encodeURIComponent(check)}`)
|
|
48
|
+
const r = await fetch(this.st.url(check == null ? '/api/tests.json' : `/api/tests.json?check=${encodeURIComponent(check)}`))
|
|
49
49
|
if (!r.ok) throw new Error(await r.text())
|
|
50
50
|
const v = (await r.json()) as TestsRootView | TestCheckView
|
|
51
51
|
runInAction(() => {
|