rvlib-shipkit 1.2.0 → 1.4.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 +104 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +15 -10
- package/src/check.ts +26 -10
- package/src/checks/SK001-package-json-shape.check.ts +34 -5
- 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 +103 -61
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK014-prose-ste.check.ts +16 -6
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK016-legacy-check-shapes.check.ts +11 -5
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
- package/src/checks/SK024-identity-colors.check.ts +5 -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 +50 -0
- package/src/checks/SK028-readme.check.ts +83 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Ci.ts +5 -0
- package/src/cli/cmd_CrossRepoActivity.ts +119 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -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 +9 -1
- package/src/collections/checks.ts +3 -2
- package/src/collections/collection-types.ts +15 -0
- package/src/collections/providers.ts +7 -3
- package/src/collections/test-time.ts +10 -1
- 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/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-runner.ts +66 -17
- package/src/core/discover-checks.ts +10 -5
- 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 +1 -1
- package/src/core/layout.ts +2 -0
- package/src/core/motherhome.ts +88 -66
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +14 -5
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +8 -7
- package/src/core/run-ci.ts +46 -15
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/wire.ts +49 -9
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +138 -0
- package/src/home/activity.ts +256 -0
- package/src/home/global-units.ts +3 -2
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +10 -2
- package/src/home/home-routes.ts +34 -6
- package/src/home/home-start.ts +33 -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 +40 -7
- package/src/home/process-rows.ts +222 -0
- package/src/home/resources.ts +99 -20
- package/src/home/run-daemon.ts +12 -7
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +6 -3
- package/src/home/supervise.ts +16 -1
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +32 -5
- package/src/ledger/db.ts +1 -1
- package/src/ledger/gate-checks.ts +20 -6
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +39 -10
- package/src/ledger/store.ts +30 -2
- package/src/ledger/world-cache.ts +63 -14
- package/src/plan/cadence.ts +21 -5
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +138 -13
- package/src/plan/repo-source.ts +120 -8
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +17 -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-words.ts +2 -0
- package/src/serve/hub-app/checks-svc.ts +2 -1
- package/src/serve/hub-app/col-tables-view.tsx +124 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +28 -0
- 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/hash-route.ts +7 -1
- package/src/serve/hub-app/header-bar.tsx +79 -104
- package/src/serve/hub-app/home-st.ts +50 -13
- package/src/serve/hub-app/home-strip.css +81 -23
- package/src/serve/hub-app/home-strip.tsx +243 -102
- package/src/serve/hub-app/home.html +4 -0
- package/src/serve/hub-app/hub-app.tsx +103 -161
- package/src/serve/hub-app/hub-types.ts +6 -3
- package/src/serve/hub-app/hub.css +101 -39
- package/src/serve/hub-app/icons.tsx +27 -3
- package/src/serve/hub-app/index.html +4 -0
- package/src/serve/hub-app/keys.ts +44 -9
- package/src/serve/hub-app/live-svc.ts +24 -64
- package/src/serve/hub-app/machine-st.ts +65 -13
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/next-run.ts +1 -1
- package/src/serve/hub-app/palette-st.ts +13 -9
- 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 +21 -85
- package/src/serve/hub-app/planned-view.tsx +12 -7
- package/src/serve/hub-app/planned.css +5 -2
- package/src/serve/hub-app/project-list.css +43 -21
- package/src/serve/hub-app/project-list.tsx +122 -23
- package/src/serve/hub-app/projects-svc.ts +120 -12
- 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 +26 -7
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/tests-view.tsx +31 -4
- package/src/serve/hub-app/timeline-chart.tsx +31 -27
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +2 -2
- package/src/serve/hub-app/timeline-view.tsx +29 -9
- package/src/serve/hub-runtime.ts +17 -9
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +100 -20
- package/src/serve/project-gutter.ts +34 -13
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/serve-hub.ts +13 -8
- package/src/serve/tracked-watch.ts +33 -1
- package/src/serve/warm-cell.ts +49 -16
- package/src/shipkit-files.ts +20 -4
- 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/checks/SK023-stores-under-cap.check.ts +0 -49
- package/src/checks/contracts.check.ts +0 -20
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// server measures at most once per 5s whatever the number of tabs, so this
|
|
4
4
|
// poll is cheap.
|
|
5
5
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
6
|
-
import type { ProjectRow as Project, Release } from '../project-gutter-route.ts'
|
|
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
8
|
|
|
9
|
-
export type { ProjectRow as Project, Release } from '../project-gutter-route.ts'
|
|
9
|
+
export type { ArchivedRow, ProjectRow as Project, Release } from '../project-gutter-route.ts'
|
|
10
10
|
|
|
11
11
|
export const PROJECTS_POLL_MS = 5_000
|
|
12
12
|
/** how long the colour picker rests before its colour is written */
|
|
@@ -16,6 +16,38 @@ export const IDENTITY_SAVE_MS = 400
|
|
|
16
16
|
* next load while the daemon is asked again */
|
|
17
17
|
export const LIST_KEY = 'hub:projects'
|
|
18
18
|
|
|
19
|
+
/** where this browser keeps whether the rail's archived rows show */
|
|
20
|
+
export const ARCHIVED_OPEN_KEY = 'hub:archived-open'
|
|
21
|
+
|
|
22
|
+
const keptOpen = (): boolean => {
|
|
23
|
+
try { return globalThis.localStorage?.getItem(ARCHIVED_OPEN_KEY) === '1' } catch { return false }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** where this browser keeps the last sort it picked */
|
|
27
|
+
export const SORT_KEY = 'hub:sort'
|
|
28
|
+
|
|
29
|
+
/** a sort the foot's menu offers: each one reorders the list once, and the
|
|
30
|
+
* result is the new manual order */
|
|
31
|
+
export type SortMode = 'activity' | 'commit' | 'name' | 'asks'
|
|
32
|
+
|
|
33
|
+
/** the menu's lines, in their order: a label naming the order, a title
|
|
34
|
+
* saying how it breaks ties */
|
|
35
|
+
export const SORTS: { id: SortMode; label: string; title: string }[] = [
|
|
36
|
+
{ id: 'activity', label: 'last activity', title: 'newest first: the last commit, shipkit run or Claude session' },
|
|
37
|
+
{ id: 'commit', label: 'last commit', title: 'newest commit on the checked out branch first' },
|
|
38
|
+
{ id: 'name', label: 'name', title: 'A to Z' },
|
|
39
|
+
{ id: 'asks', label: 'open asks', title: 'the repo waiting on the most asks first' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const keptSort = (): SortMode | null => {
|
|
43
|
+
try {
|
|
44
|
+
const v = globalThis.localStorage?.getItem(SORT_KEY)
|
|
45
|
+
return SORTS.some((m) => m.id === v) ? (v as SortMode) : null
|
|
46
|
+
} catch {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
/** the project list this browser last saw, or [] when storage is absent,
|
|
20
52
|
* blocked or holds anything else */
|
|
21
53
|
export function loadList(): Project[] {
|
|
@@ -78,6 +110,15 @@ export const byActivity = (list: Project[]): string[] =>
|
|
|
78
110
|
.sort((a, b) => (b.p.activity ?? -Infinity) - (a.p.activity ?? -Infinity) || a.i - b.i)
|
|
79
111
|
.map(({ p }) => p.name)
|
|
80
112
|
|
|
113
|
+
/** the names in a sort's order, ties in their current order */
|
|
114
|
+
export function sortNames(list: Project[], mode: SortMode): string[] {
|
|
115
|
+
if (mode === 'activity') return byActivity(list)
|
|
116
|
+
const cmp: (a: Project, b: Project) => number = mode === 'commit'
|
|
117
|
+
? (a, b) => (b.commit ?? -Infinity) - (a.commit ?? -Infinity)
|
|
118
|
+
: mode === 'asks' ? (a, b) => b.asks - a.asks
|
|
119
|
+
: (a, b) => a.name.localeCompare(b.name)
|
|
120
|
+
return list.map((p, i) => ({ p, i })).sort((a, b) => cmp(a.p, b.p) || a.i - b.i).map(({ p }) => p.name)
|
|
121
|
+
}
|
|
81
122
|
|
|
82
123
|
/** the hub state dot the home draws on a tile, only when it needs a look:
|
|
83
124
|
* its hub starts or crashed. A hub that is up is the normal case and draws none */
|
|
@@ -132,7 +173,7 @@ export function cardModel(p: Project, now: number): CardModel {
|
|
|
132
173
|
}
|
|
133
174
|
|
|
134
175
|
/** a page of the home that is no repo's */
|
|
135
|
-
export type Place = 'overview' | 'planned'
|
|
176
|
+
export type Place = 'overview' | 'planned' | 'activity'
|
|
136
177
|
|
|
137
178
|
export class ProjectsSvc {
|
|
138
179
|
/** the last list this browser saw until the first poll answers, so the
|
|
@@ -140,6 +181,16 @@ export class ProjectsSvc {
|
|
|
140
181
|
list: Project[] = loadList()
|
|
141
182
|
/** why the last poll or open failed, shown on the list's head */
|
|
142
183
|
error = ''
|
|
184
|
+
/** the repos whose repo.config.ts says `archived: true`: never in `list`,
|
|
185
|
+
* only on the archived page */
|
|
186
|
+
archived: ArchivedRow[] = []
|
|
187
|
+
/** the archived button at the foot of the rail is on, its rows shown, per browser */
|
|
188
|
+
archivedOpen = keptOpen()
|
|
189
|
+
/** the sort menu at the foot of the rail is open */
|
|
190
|
+
sortMenuOpen = false
|
|
191
|
+
/** the last sort picked in this browser; `sortMode` says whether the list
|
|
192
|
+
* is still in its order */
|
|
193
|
+
sortPicked: SortMode | null = keptSort()
|
|
143
194
|
/** the daemon answered once: until then the list is the kept one */
|
|
144
195
|
fresh = false
|
|
145
196
|
/** the row under the pointer, whose hover card shows */
|
|
@@ -147,8 +198,9 @@ export class ProjectsSvc {
|
|
|
147
198
|
/** the tile being dragged, and the gap it would land in */
|
|
148
199
|
drag: string | null = null
|
|
149
200
|
gap: number | null = null
|
|
150
|
-
/** the repo the home page shows
|
|
151
|
-
* hub of its own, whose repo is
|
|
201
|
+
/** the repo the home page shows, or showed last while a global page is
|
|
202
|
+
* up; null until one is picked, and on a hub of its own, whose repo is
|
|
203
|
+
* `current`. The list's api is always the
|
|
152
204
|
* page origin's own: the home's machine wide one, or this hub's */
|
|
153
205
|
selected: string | null = null
|
|
154
206
|
/** what a single click does: the home shows that repo in place, a hub
|
|
@@ -157,8 +209,16 @@ export class ProjectsSvc {
|
|
|
157
209
|
/** the two places at the top of the list, the overview and the plan:
|
|
158
210
|
* the home sets it, a hub of its own has none */
|
|
159
211
|
onPlace: ((p: Place) => void) | null = null
|
|
212
|
+
/** the archived page, where an archived row of the rail leads: the home sets it */
|
|
213
|
+
onArchived: (() => void) | null = null
|
|
214
|
+
/** the picker's line after the overview: the home sets it */
|
|
215
|
+
onPicker: (() => void) | null = null
|
|
216
|
+
/** the picker is open, so its line is lit: the home sets it */
|
|
217
|
+
pickerOpen: () => boolean = () => false
|
|
160
218
|
/** the place shown, marked at the top of the list; null on a repo page */
|
|
161
219
|
place: Place | null = null
|
|
220
|
+
/** a global page is up: the selected repo's row stays marked, dimmer */
|
|
221
|
+
away = false
|
|
162
222
|
/** the home's hub state per repo; a hub draws none */
|
|
163
223
|
dotOf: (name: string) => HubDot = () => null
|
|
164
224
|
/** one line when a machine resource the repo uses is off */
|
|
@@ -172,12 +232,38 @@ export class ProjectsSvc {
|
|
|
172
232
|
private orderGen = 0
|
|
173
233
|
|
|
174
234
|
constructor() {
|
|
175
|
-
makeAutoObservable<this, 'gen' | 'orderGen' | 'identitySaves' | 'saveIdentity' | 'kept' | 'keep'>(this, { gen: false, orderGen: false, identitySaves: false, saveIdentity: false, kept: false, keep: false, list: observableRef, onSelect: false, onPlace: false, dotOf: false, warnOf: false })
|
|
235
|
+
makeAutoObservable<this, 'gen' | 'orderGen' | 'identitySaves' | 'saveIdentity' | 'kept' | 'keep'>(this, { gen: false, orderGen: false, identitySaves: false, saveIdentity: false, kept: false, keep: false, list: observableRef, onSelect: false, onPlace: false, onPicker: false, pickerOpen: false, onArchived: false, dotOf: false, warnOf: false })
|
|
176
236
|
}
|
|
177
237
|
|
|
178
|
-
|
|
238
|
+
toggleArchivedOpen(): void {
|
|
239
|
+
this.archivedOpen = !this.archivedOpen
|
|
240
|
+
try { globalThis.localStorage?.setItem(ARCHIVED_OPEN_KEY, this.archivedOpen ? '1' : '0') } catch { /* private window: the choice lasts the tab */ }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
isArchived(name: string): boolean {
|
|
244
|
+
return this.archived.some((p) => p.name === name)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** archive a repo or bring it back: written into its repo.config.ts
|
|
248
|
+
* (`archived`, only that key), then the list is read again, so the repo
|
|
249
|
+
* moves between the rail and the archived page, and the daemon stops or
|
|
250
|
+
* loads its hub on its next tick */
|
|
251
|
+
async setArchived(name: string, on: boolean): Promise<void> {
|
|
252
|
+
try {
|
|
253
|
+
const r = await fetch('/api/projects/archived', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, archived: on }) })
|
|
254
|
+
if (!r.ok) throw new Error(await r.text())
|
|
255
|
+
runInAction(() => { this.error = '' })
|
|
256
|
+
await this.load()
|
|
257
|
+
} catch (e) {
|
|
258
|
+
runInAction(() => { this.error = `${on ? 'archive' : 'unarchive'} ${name}: ${(e as Error).message}` })
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** the tile drawn as the current one. The home marks only a repo that
|
|
263
|
+
* was picked (it sets `onPlace`); a hub of its own marks its own repo */
|
|
179
264
|
isCurrent(p: Project): boolean {
|
|
180
|
-
|
|
265
|
+
if (this.selected != null) return p.name === this.selected
|
|
266
|
+
return this.onPlace == null && p.current
|
|
181
267
|
}
|
|
182
268
|
|
|
183
269
|
select(name: string): void {
|
|
@@ -272,9 +358,30 @@ export class ProjectsSvc {
|
|
|
272
358
|
void this.reorder(moveTo(this.list.map((p) => p.name), from, gap))
|
|
273
359
|
}
|
|
274
360
|
|
|
275
|
-
/**
|
|
276
|
-
|
|
277
|
-
|
|
361
|
+
/** the sort the list is in now: the last one picked while the order
|
|
362
|
+
* still matches it, else the manual order a drag made */
|
|
363
|
+
get sortMode(): SortMode | 'manual' {
|
|
364
|
+
const m = this.sortPicked
|
|
365
|
+
if (m == null) return 'manual'
|
|
366
|
+
const now = this.list.map((p) => p.name)
|
|
367
|
+
return sortNames(this.list, m).every((n, i) => n === now[i]) ? m : 'manual'
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** the button opens or closes the menu, and sorts nothing */
|
|
371
|
+
toggleSortMenu(): void {
|
|
372
|
+
this.sortMenuOpen = !this.sortMenuOpen
|
|
373
|
+
}
|
|
374
|
+
setSortMenu(open: boolean): void {
|
|
375
|
+
this.sortMenuOpen = open
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** a pick: one reorder in that sort's order, then the menu closes; from
|
|
379
|
+
* then on it is the manual order */
|
|
380
|
+
sortBy(mode: SortMode): void {
|
|
381
|
+
this.sortMenuOpen = false
|
|
382
|
+
this.sortPicked = mode
|
|
383
|
+
try { globalThis.localStorage?.setItem(SORT_KEY, mode) } catch { /* private window: the pick lasts the tab */ }
|
|
384
|
+
void this.reorder(sortNames(this.list, mode))
|
|
278
385
|
}
|
|
279
386
|
|
|
280
387
|
/** shows the new order at once, then saves it for every hub */
|
|
@@ -322,12 +429,13 @@ export class ProjectsSvc {
|
|
|
322
429
|
try {
|
|
323
430
|
const r = await fetch('/api/projects.json')
|
|
324
431
|
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
325
|
-
const j = (await r.json()) as { projects: Project[] }
|
|
432
|
+
const j = (await r.json()) as { projects: Project[]; archived?: ArchivedRow[] }
|
|
326
433
|
runInAction(() => {
|
|
327
434
|
// a reorder since this left, or a drag in flight: the next poll answers
|
|
328
435
|
if (order !== this.orderGen || this.drag != null) return
|
|
329
436
|
const mine = new Map(this.list.filter((p) => this.identitySaves.has(p.name)).map((p) => [p.name, p.identity]))
|
|
330
437
|
this.list = mine.size === 0 ? j.projects : j.projects.map((p) => (mine.has(p.name) ? { ...p, identity: mine.get(p.name)! } : p))
|
|
438
|
+
this.archived = j.archived ?? []
|
|
331
439
|
this.error = ''
|
|
332
440
|
this.fresh = true
|
|
333
441
|
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// the repo's README, shown when no row is picked. Read again each time the
|
|
2
|
+
// empty page shows, so an edited README appears without a reload
|
|
3
|
+
import { makeAutoObservable, runInAction } from 'mobx'
|
|
4
|
+
import type { RepoSt } from './repo-st.ts'
|
|
5
|
+
|
|
6
|
+
export class ReadmeSvc {
|
|
7
|
+
/** the file name at the root, null when the repo has none */
|
|
8
|
+
file: string | null = null
|
|
9
|
+
md: string | null = null
|
|
10
|
+
error: string | null = null
|
|
11
|
+
loaded = false
|
|
12
|
+
|
|
13
|
+
constructor(private st: RepoSt) {
|
|
14
|
+
makeAutoObservable<this, 'st'>(this, { st: false })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** where a README image loads from: the hub's file route, under this repo */
|
|
18
|
+
fileUrl = (rel: string): string => this.st.url(`/api/readme-file/${rel}`)
|
|
19
|
+
|
|
20
|
+
async load(): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
const r = await fetch(this.st.url('/api/readme.json'))
|
|
23
|
+
if (!r.ok) throw new Error(`http ${r.status}: ${await r.text()}`)
|
|
24
|
+
const j = (await r.json()) as { file: string | null; md: string | null }
|
|
25
|
+
runInAction(() => {
|
|
26
|
+
this.file = j.file
|
|
27
|
+
this.md = j.md
|
|
28
|
+
this.error = null
|
|
29
|
+
this.loaded = true
|
|
30
|
+
})
|
|
31
|
+
} catch (e) {
|
|
32
|
+
runInAction(() => {
|
|
33
|
+
this.error = `README fetch broke: ${(e as Error).message}`
|
|
34
|
+
this.loaded = true
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// the page with no row picked: the repo's README, images included, or the
|
|
2
|
+
// hint when the repo has none
|
|
3
|
+
import { observer } from 'mobx-react-lite'
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import { renderReadme } from './md.ts'
|
|
6
|
+
import type { RepoSt } from './repo-st.ts'
|
|
7
|
+
|
|
8
|
+
export const ReadmeView = observer(({ app }: { app: RepoSt }) => {
|
|
9
|
+
const r = app.readme
|
|
10
|
+
useEffect(() => { void r.load() }, [r])
|
|
11
|
+
if (r.error != null) return <div className="ph"><div className="errs">{r.error}</div>Pick a row on the left.</div>
|
|
12
|
+
if (r.md == null) return <div className="ph">{r.loaded ? 'Pick a row on the left.' : ''}</div>
|
|
13
|
+
return (
|
|
14
|
+
<div className="readme">
|
|
15
|
+
<div className="readme-head">{r.file} · pick a row on the left for the rest</div>
|
|
16
|
+
<div className="md readme-body" dangerouslySetInnerHTML={{ __html: renderReadme(r.md, r.fileUrl) }} />
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* the README on a repo page with no row picked: a readable column held to the left
|
|
2
|
+
(the pane centers its child, the auto right margin takes the slack), images at their width */
|
|
3
|
+
.readme{width:100%;max-width:860px;margin:0 auto 0 var(--s3);padding:var(--s3) var(--s4)}
|
|
4
|
+
.readme-head{color:var(--dim);font-size:11px;margin-bottom:var(--s2)}
|
|
5
|
+
.readme-body{font-size:13.5px;line-height:1.6}
|
|
6
|
+
.readme .readme-body h1{font-size:22px;margin:var(--s3) 0 var(--s2)}
|
|
7
|
+
.readme .readme-body h2{font-size:17px;margin:var(--s3) 0 var(--s2);border-bottom:1px solid var(--line);padding-bottom:var(--s1)}
|
|
8
|
+
.readme .readme-body h3{font-size:14px}
|
|
9
|
+
.readme-body img{max-width:100%;height:auto;border-radius:4px}
|
|
10
|
+
.readme-body pre{overflow-x:auto;padding:var(--s2);background:var(--hi3);border-radius:4px}
|
|
11
|
+
.readme-body a{color:var(--blue)}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// A repo page's sections: the pages reached for most, one line each at the top
|
|
2
|
+
// of the left pane, over the tree, drawn 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 line. The lines come from what the page already holds (the kept
|
|
6
|
+
// screen on a reload), so none arrives late and moves the tree.
|
|
7
|
+
import { observer } from 'mobx-react-lite'
|
|
8
|
+
import { RUNS_ID } from '../../collections/collection-types.ts'
|
|
9
|
+
import { Icon, ICON } from './icons.tsx'
|
|
10
|
+
import { sectionKeyLabel, type SectionId } from './keys.ts'
|
|
11
|
+
import type { RepoSt } from './repo-st.ts'
|
|
12
|
+
import { openWikis, WIKIS } from './wikis-view.tsx'
|
|
13
|
+
|
|
14
|
+
export type RepoSection = {
|
|
15
|
+
id: SectionId
|
|
16
|
+
label: string
|
|
17
|
+
title: string
|
|
18
|
+
/** null when a number would say nothing */
|
|
19
|
+
count: number | null
|
|
20
|
+
tone: 'red' | 'warn' | null
|
|
21
|
+
on: boolean
|
|
22
|
+
go: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const counted = (n: number, tone: 'red' | 'warn'): Pick<RepoSection, 'count' | 'tone'> => (n === 0 ? { count: null, tone: null } : { count: n, tone })
|
|
26
|
+
|
|
27
|
+
export function repoSections(app: RepoSt): RepoSection[] {
|
|
28
|
+
const has = (col: string): boolean => app.cols.colMeta.some((c) => c.id === col)
|
|
29
|
+
const sel = app.sel
|
|
30
|
+
const onCol = (col: string): boolean => (sel?.t === 'col' && sel.id === col) || (sel?.t === 'item' && sel.col === col)
|
|
31
|
+
const res = app.cols.resources
|
|
32
|
+
const reds = (col: string): number => res.filter((r) => r.collection === col && r.state === 'red').length
|
|
33
|
+
const none = { count: null, tone: null }
|
|
34
|
+
const out: RepoSection[] = []
|
|
35
|
+
if (has('checks')) out.push({ id: 'checks', label: 'checks', title: 'every check the gate and the health pass run, one page each', ...counted(reds('checks'), 'red'), on: onCol('checks'), go: () => app.showCol('checks') })
|
|
36
|
+
// a page, never a resource: every hub keeps a run record
|
|
37
|
+
out.push({ id: 'runs', label: 'runs', title: 'every run on record, the slowest checks and the longest runs', ...none, on: onCol('runs'), go: () => app.cols.select('runs', RUNS_ID) })
|
|
38
|
+
if (has('asks')) {
|
|
39
|
+
const open = res.filter((r) => r.collection === 'asks' && r.type === 'ask' && r.slot !== 'done').length
|
|
40
|
+
out.push({ id: 'asks', label: 'asks', title: 'what waits on a human', ...counted(open, 'warn'), on: onCol('asks'), go: () => app.showCol('asks') })
|
|
41
|
+
}
|
|
42
|
+
if (has('tests')) out.push({ id: 'tests', label: 'tests', title: 'every test file, slowest first, then each test check', ...counted(reds('tests'), 'red'), on: sel?.t === 'tests' || onCol('tests'), go: () => app.showTests(null) })
|
|
43
|
+
const firstCat = app.media.assets[0]?.cat ?? app.cols.cachedCat ?? undefined
|
|
44
|
+
if (firstCat != null) out.push({ id: 'media', label: 'media', title: 'the assets, in the atelier', ...none, on: sel?.t === 'asset' || sel?.t === 'cat', go: () => app.showCat(firstCat) })
|
|
45
|
+
if (has(WIKIS)) out.push({ id: 'wikis', label: 'wikis', title: 'every wiki this repo mounts, as a link graph', ...none, on: onCol(WIKIS), go: () => openWikis(app) })
|
|
46
|
+
if (app.home.machine != null) out.push({ id: 'admin', label: 'admin', title: 'reload the hub, keep the repo active, archive it', ...none, on: sel?.t === 'admin', go: () => app.showAdmin() })
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const Sections = observer(({ app }: { app: RepoSt }) => (
|
|
51
|
+
<nav className="rsecs" aria-label="sections">
|
|
52
|
+
{repoSections(app).map((s) => (
|
|
53
|
+
<button key={s.id} className={`rsec${s.on ? ' cur' : ''}`} aria-current={s.on ? 'page' : undefined} title={`${s.title} (${sectionKeyLabel(s.id)})`} onClick={s.go}>
|
|
54
|
+
<Icon d={ICON[s.id]} />
|
|
55
|
+
<span className="rsecl">{s.label}</span>
|
|
56
|
+
{s.count == null ? null : <b className={`rsecn n${s.tone}`}>{s.count}</b>}
|
|
57
|
+
<kbd className="pjkey">{sectionKeyLabel(s.id)}</kbd>
|
|
58
|
+
</button>
|
|
59
|
+
))}
|
|
60
|
+
</nav>
|
|
61
|
+
))
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
// (hash-route.ts), and setSel is the one place both move together. A repo
|
|
6
6
|
// that is not shown keeps all of it and polls nothing (resume, pause), and
|
|
7
7
|
// only the shown one writes the url.
|
|
8
|
-
import { RUNS_ID } from '../../collections/collection-types.ts'
|
|
9
8
|
import { makeAutoObservable } from 'mobx'
|
|
10
9
|
import { ClockSvc } from './clock-svc.ts'
|
|
11
10
|
import { ColsSvc } from './cols-svc.ts'
|
|
12
11
|
import { canonicalSel, currentHash, hashFor, parseHash, selOf, type HashSel } from './hash-route.ts'
|
|
13
12
|
import { ComfySvc } from './comfy-svc.ts'
|
|
14
13
|
import { HealthSvc } from './health-svc.ts'
|
|
14
|
+
import { ReadmeSvc } from './readme-svc.ts'
|
|
15
15
|
import { LiveSvc } from './live-svc.ts'
|
|
16
16
|
import { MediaSvc } from './media-svc.ts'
|
|
17
17
|
import { TimelineSvc } from './timeline-svc.ts'
|
|
@@ -19,13 +19,19 @@ import { TreeSvc } from './tree-svc.ts'
|
|
|
19
19
|
import { ChecksSvc } from './checks-svc.ts'
|
|
20
20
|
import { ProcessesSvc } from './processes-svc.ts'
|
|
21
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'
|
|
22
25
|
import type { Resource } from './hub-types.ts'
|
|
23
26
|
import type { HomeSt } from './home-st.ts'
|
|
24
27
|
|
|
25
28
|
export type Sel = HashSel | null
|
|
26
29
|
|
|
27
|
-
/** one picker target (cmd+
|
|
28
|
-
|
|
30
|
+
/** one picker target (cmd+j): 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 }
|
|
29
35
|
|
|
30
36
|
/** all · needs (yellow+red) · red — one severity ladder for every section */
|
|
31
37
|
export type Filter = 'all' | 'needs' | 'red'
|
|
@@ -47,6 +53,7 @@ export class RepoSt {
|
|
|
47
53
|
cols: ColsSvc
|
|
48
54
|
tree: TreeSvc
|
|
49
55
|
health: HealthSvc
|
|
56
|
+
readme: ReadmeSvc
|
|
50
57
|
checks: ChecksSvc
|
|
51
58
|
comfy: ComfySvc
|
|
52
59
|
timeline: TimelineSvc
|
|
@@ -55,6 +62,8 @@ export class RepoSt {
|
|
|
55
62
|
processes: ProcessesSvc
|
|
56
63
|
/** the pages behind the test groups of the tree (tests-svc.ts) */
|
|
57
64
|
tests: TestsSvc
|
|
65
|
+
/** the admin page: archive behind its confirmation, reload, keep active */
|
|
66
|
+
admin: AdminSvc
|
|
58
67
|
sel: Sel = null
|
|
59
68
|
filter: Filter = 'all'
|
|
60
69
|
/** sidebar search — live substring filter over asset + collection rows */
|
|
@@ -84,13 +93,15 @@ export class RepoSt {
|
|
|
84
93
|
this.cols = new ColsSvc(this)
|
|
85
94
|
this.tree = new TreeSvc(this)
|
|
86
95
|
this.health = new HealthSvc(this)
|
|
96
|
+
this.readme = new ReadmeSvc(this)
|
|
87
97
|
this.checks = new ChecksSvc(this)
|
|
88
98
|
this.comfy = new ComfySvc(this)
|
|
89
99
|
this.timeline = new TimelineSvc(this)
|
|
90
100
|
this.live = new LiveSvc(this)
|
|
91
101
|
this.processes = new ProcessesSvc(this)
|
|
92
102
|
this.tests = new TestsSvc(this)
|
|
93
|
-
|
|
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 })
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
/** an absolute hub path ('/api/…', '/media/…', '/hub.json') as this
|
|
@@ -215,10 +226,11 @@ export class RepoSt {
|
|
|
215
226
|
out.push({ id: `cat:${cat}`, label: cat, section: 'media', run: () => this.showCat(cat) })
|
|
216
227
|
for (const a of this.media.assets)
|
|
217
228
|
out.push({ id: `asset:${a.id}`, label: a.label, section: a.cat, run: () => this.selectAsset(a.id) })
|
|
218
|
-
// the
|
|
219
|
-
|
|
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 })
|
|
220
232
|
for (const col of this.cols.sidebarCols) {
|
|
221
|
-
out.push({ id: `col:${col.id}`, label: col.title, section: 'section', run: () => this.showCol(col.id) })
|
|
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) })
|
|
222
234
|
for (const i of col.instances)
|
|
223
235
|
out.push({ id: `item:${i.id}`, label: i.text, section: col.title, run: () => this.cols.select(col.id, i.id) })
|
|
224
236
|
}
|
|
@@ -272,6 +284,12 @@ export class RepoSt {
|
|
|
272
284
|
this.cols.select(r.collection, r.id)
|
|
273
285
|
}
|
|
274
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
|
+
|
|
275
293
|
/** collection view — one card per instance */
|
|
276
294
|
showCol(id: string): void {
|
|
277
295
|
this.setSel({ t: 'col', id })
|
|
@@ -285,6 +303,7 @@ export class RepoSt {
|
|
|
285
303
|
case 'item': return this.cols.select(sel.col, sel.id)
|
|
286
304
|
case 'status': return this.showStatus()
|
|
287
305
|
case 'tests': return this.showTests(sel.check)
|
|
306
|
+
case 'admin': return this.showAdmin()
|
|
288
307
|
}
|
|
289
308
|
}
|
|
290
309
|
/** 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
|
|
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' },
|
|
@@ -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
|
+
))
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// THE PAGES OF THE TEST GROUPS: `tests` itself,
|
|
1
|
+
// THE PAGES OF THE TEST GROUPS: `tests` itself, every test file of the repo
|
|
2
|
+
// slowest first then one line per test check, and
|
|
2
3
|
// each test check (`test`, `test:<unit>`), which answers one question: where
|
|
3
4
|
// does its time go. The check's own facts (its sentence, its command, its
|
|
4
5
|
// last wall time and its trend) are the check page's pieces, read from the
|
|
5
6
|
// resource the tree already holds. The breakdown is the one fetch the page
|
|
6
7
|
// makes (tests-svc.ts), shaped on the server (collections/test-time.ts).
|
|
7
8
|
import { observer } from 'mobx-react-lite'
|
|
8
|
-
import type { FailingFile, FileTime, LastRun, Phases, Shape, TestCheckRow, TestCheckView, TestsRootView } from '../../collections/test-time.ts'
|
|
9
|
+
import type { FailingFile, FileTime, LastRun, Phases, Shape, TestCheckRow, TestCheckView, TestFileRow, TestsRootView } from '../../collections/test-time.ts'
|
|
9
10
|
import { CommandLine, LastRunValue, Row, Says, Section, Timings } from './check-view.tsx'
|
|
10
11
|
import { durationTone, fmtMs } from './dur.ts'
|
|
11
12
|
import type { RepoSt } from './repo-st.ts'
|
|
@@ -217,16 +218,42 @@ const CheckRows = observer(({ app, rows }: { app: RepoSt; rows: TestCheckRow[] }
|
|
|
217
218
|
</div>
|
|
218
219
|
))
|
|
219
220
|
|
|
221
|
+
const STATUS_WORD = { pass: 'pass', fail: 'fail' } as const
|
|
222
|
+
|
|
223
|
+
/** every file of every test check on one list, slowest first: the answer to
|
|
224
|
+
* "which tests are slow", whatever check runs them */
|
|
225
|
+
const AllFiles = observer(({ app, rows }: { app: RepoSt; rows: TestFileRow[] }) => (
|
|
226
|
+
<div className="rlist">
|
|
227
|
+
<div className="rhead rall">
|
|
228
|
+
<span className="tleft">file</span>
|
|
229
|
+
<span className="tleft">check</span>
|
|
230
|
+
<span>duration</span>
|
|
231
|
+
<span>status</span>
|
|
232
|
+
<span>when</span>
|
|
233
|
+
</div>
|
|
234
|
+
{rows.map((f) => (
|
|
235
|
+
<div key={f.file} className={`rline rall${f.status === 'fail' ? ' red' : ''}`} title={f.file} onClick={() => app.tests.openFile(f.file)}>
|
|
236
|
+
<span className="rwhat">{f.inUnit}</span>
|
|
237
|
+
<span className="rtag" title="the check that runs it">{f.check}</span>
|
|
238
|
+
<span className={`rnum ${fileTone(f)}`}>{f.ms == null ? <span className="dim">never ran</span> : fmtMs(f.ms)}</span>
|
|
239
|
+
<span className={`rnum ${f.status === 'fail' ? 'bad' : 'dim'}`}>{f.status == null ? '' : STATUS_WORD[f.status]}</span>
|
|
240
|
+
{f.at == null ? <span /> : <TimeAgo clock={app.clock} at={f.at} />}
|
|
241
|
+
</div>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
))
|
|
245
|
+
|
|
220
246
|
export const TestsRootPage = observer(({ app }: { app: RepoSt }) => {
|
|
221
247
|
const v: TestsRootView | null = app.tests.root()
|
|
222
248
|
return (
|
|
223
249
|
<div className="colcard checkcard">
|
|
224
250
|
<div className="chead">
|
|
225
251
|
<span className="tid">tests</span>
|
|
226
|
-
<h3 className="ctitle">every test
|
|
252
|
+
<h3 className="ctitle">every test</h3>
|
|
227
253
|
</div>
|
|
228
|
-
<div className="usays">
|
|
254
|
+
<div className="usays">every test file by its last recorded duration, then each test check's last run. Click a file for its page, a check for where its time goes</div>
|
|
229
255
|
{v == null ? <Loading app={app} check={null} /> : <>
|
|
256
|
+
<Section title={`every test file (${v.files.length})`} help="every file of every test check, by its last recorded duration in the shared run, slowest first; the ones that never ran here last. Its tone is against its budget: over it reads red"><AllFiles app={app} rows={v.files} /></Section>
|
|
230
257
|
<Section title="test checks" help="one line per test check, slowest wall first"><CheckRows app={app} rows={v.checks} /></Section>
|
|
231
258
|
<Failing app={app} rows={v.failing} showCheck />
|
|
232
259
|
</>}
|