rvlib-shipkit 1.1.0 → 1.2.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 +67 -0
- package/package.json +3 -1
- package/src/check.ts +24 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
- package/src/checks/SK023-stores-under-cap.check.ts +49 -0
- package/src/checks/SK024-identity-colors.check.ts +49 -0
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +18 -5
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli.ts +22 -2
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/attach.ts +14 -9
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +40 -18
- package/src/core/discover-checks.ts +36 -18
- 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/retention.ts +213 -0
- package/src/core/run-check.ts +6 -2
- package/src/core/run-ci.ts +3 -0
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +86 -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 +208 -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 +141 -0
- package/src/home/resources.ts +338 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +149 -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 +172 -0
- package/src/home/supervise.ts +346 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widgets.ts +102 -0
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +5 -0
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/run-log.ts +58 -23
- package/src/ledger/store.ts +228 -103
- package/src/ledger/world-cache.ts +15 -13
- package/src/plan/cadence.ts +237 -0
- package/src/plan/planner.ts +303 -0
- package/src/plan/repo-source.ts +100 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/repo-config.ts +51 -2
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +1 -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/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +1 -1
- package/src/serve/hub-app/header-bar.tsx +44 -12
- 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 +241 -0
- package/src/serve/hub-app/home-strip.css +70 -0
- package/src/serve/hub-app/home-strip.tsx +245 -0
- package/src/serve/hub-app/home.html +17 -0
- package/src/serve/hub-app/hub-app.tsx +201 -93
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +43 -23
- package/src/serve/hub-app/icons.tsx +14 -0
- package/src/serve/hub-app/index.html +3 -1
- package/src/serve/hub-app/keys.ts +25 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +12 -9
- package/src/serve/hub-app/machine-st.ts +270 -0
- 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 +88 -0
- package/src/serve/hub-app/planned-st.ts +171 -0
- package/src/serve/hub-app/planned-view.tsx +109 -0
- package/src/serve/hub-app/planned.css +39 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +97 -0
- package/src/serve/hub-app/project-list.tsx +167 -0
- package/src/serve/hub-app/projects-svc.ts +139 -17
- 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/{hub-st.ts → repo-st.ts} +155 -84
- 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/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +10 -10
- package/src/serve/hub-app/timeline-chart.tsx +24 -12
- package/src/serve/hub-app/timeline-svc.ts +5 -4
- package/src/serve/hub-app/timeline-view.tsx +13 -13
- 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 +542 -0
- package/src/serve/project-gutter-route.ts +21 -0
- package/src/serve/project-gutter.ts +3 -2
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +142 -44
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +114 -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/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -1,14 +1,41 @@
|
|
|
1
|
-
// the project
|
|
2
|
-
// wiki, polled from /api/projects.json while the
|
|
1
|
+
// the project list's state: every repo on this disk that shares the house
|
|
2
|
+
// wiki, polled from /api/projects.json while the list is mounted. The
|
|
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
6
|
import type { ProjectRow as Project, Release } from '../project-gutter-route.ts'
|
|
7
|
-
import type
|
|
7
|
+
import { identityOfName, type Identity } from '../../core/identity-color.ts'
|
|
8
8
|
|
|
9
9
|
export type { ProjectRow as Project, Release } from '../project-gutter-route.ts'
|
|
10
10
|
|
|
11
11
|
export const PROJECTS_POLL_MS = 5_000
|
|
12
|
+
/** how long the colour picker rests before its colour is written */
|
|
13
|
+
export const IDENTITY_SAVE_MS = 400
|
|
14
|
+
|
|
15
|
+
/** where this browser keeps the last project list, painted at once on the
|
|
16
|
+
* next load while the daemon is asked again */
|
|
17
|
+
export const LIST_KEY = 'hub:projects'
|
|
18
|
+
|
|
19
|
+
/** the project list this browser last saw, or [] when storage is absent,
|
|
20
|
+
* blocked or holds anything else */
|
|
21
|
+
export function loadList(): Project[] {
|
|
22
|
+
try {
|
|
23
|
+
const v = JSON.parse(globalThis.localStorage?.getItem(LIST_KEY) ?? '[]') as unknown
|
|
24
|
+
return Array.isArray(v) && v.every((p) => typeof (p as Project)?.name === 'string') ? (v as Project[]) : []
|
|
25
|
+
} catch {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** keeps the list for the next load; false when storage refused it */
|
|
31
|
+
export function saveList(list: Project[]): boolean {
|
|
32
|
+
try {
|
|
33
|
+
globalThis.localStorage?.setItem(LIST_KEY, JSON.stringify(list))
|
|
34
|
+
return true
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
12
39
|
|
|
13
40
|
/** one of six kind hues, hashed from the name, so an initial keeps its
|
|
14
41
|
* colour from one load to the next */
|
|
@@ -51,6 +78,11 @@ export const byActivity = (list: Project[]): string[] =>
|
|
|
51
78
|
.sort((a, b) => (b.p.activity ?? -Infinity) - (a.p.activity ?? -Infinity) || a.i - b.i)
|
|
52
79
|
.map(({ p }) => p.name)
|
|
53
80
|
|
|
81
|
+
|
|
82
|
+
/** the hub state dot the home draws on a tile, only when it needs a look:
|
|
83
|
+
* its hub starts or crashed. A hub that is up is the normal case and draws none */
|
|
84
|
+
export type HubDot = 'starting' | 'crashed' | null
|
|
85
|
+
|
|
54
86
|
/** what the icon face shows, and nothing more: at most ONE badge (the open
|
|
55
87
|
* asks, the only fact that needs a human's hands), dim while VS Code does
|
|
56
88
|
* not have the folder open, and a pulsing edge while Claude sessions run */
|
|
@@ -99,22 +131,104 @@ export function cardModel(p: Project, now: number): CardModel {
|
|
|
99
131
|
}
|
|
100
132
|
}
|
|
101
133
|
|
|
134
|
+
/** a page of the home that is no repo's */
|
|
135
|
+
export type Place = 'overview' | 'planned'
|
|
136
|
+
|
|
102
137
|
export class ProjectsSvc {
|
|
103
|
-
list
|
|
104
|
-
|
|
138
|
+
/** the last list this browser saw until the first poll answers, so the
|
|
139
|
+
* list paints at once and nothing moves when the daemon answers */
|
|
140
|
+
list: Project[] = loadList()
|
|
141
|
+
/** why the last poll or open failed, shown on the list's head */
|
|
105
142
|
error = ''
|
|
106
|
-
/** the
|
|
107
|
-
|
|
143
|
+
/** the daemon answered once: until then the list is the kept one */
|
|
144
|
+
fresh = false
|
|
145
|
+
/** the row under the pointer, whose hover card shows */
|
|
146
|
+
hover: string | null = null
|
|
108
147
|
/** the tile being dragged, and the gap it would land in */
|
|
109
148
|
drag: string | null = null
|
|
110
149
|
gap: number | null = null
|
|
150
|
+
/** the repo the home page shows; null on the overview, the plan, and a
|
|
151
|
+
* hub of its own, whose repo is `current`. The list's api is always the
|
|
152
|
+
* page origin's own: the home's machine wide one, or this hub's */
|
|
153
|
+
selected: string | null = null
|
|
154
|
+
/** what a single click does: the home shows that repo in place, a hub
|
|
155
|
+
* of its own opens the home on it. Default: the folder in VS Code */
|
|
156
|
+
onSelect: (name: string) => void = (name) => void this.open(name)
|
|
157
|
+
/** the two places at the top of the list, the overview and the plan:
|
|
158
|
+
* the home sets it, a hub of its own has none */
|
|
159
|
+
onPlace: ((p: Place) => void) | null = null
|
|
160
|
+
/** the place shown, marked at the top of the list; null on a repo page */
|
|
161
|
+
place: Place | null = null
|
|
162
|
+
/** the home's hub state per repo; a hub draws none */
|
|
163
|
+
dotOf: (name: string) => HubDot = () => null
|
|
164
|
+
/** one line when a machine resource the repo uses is off */
|
|
165
|
+
warnOf: (name: string) => string | null = () => null
|
|
111
166
|
private gen = 0
|
|
167
|
+
/** a colour write waiting for the picker to rest, per repo: a poll keeps
|
|
168
|
+
* the colour shown for that repo meanwhile */
|
|
169
|
+
private identitySaves = new Map<string, ReturnType<typeof setTimeout>>()
|
|
112
170
|
/** bumped by every reorder, so a poll that left before it cannot put
|
|
113
171
|
* the old order back */
|
|
114
172
|
private orderGen = 0
|
|
115
173
|
|
|
116
|
-
constructor(
|
|
117
|
-
makeAutoObservable<this, '
|
|
174
|
+
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 })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** the tile drawn as the current one */
|
|
179
|
+
isCurrent(p: Project): boolean {
|
|
180
|
+
return this.selected != null ? p.name === this.selected : p.current
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
select(name: string): void {
|
|
184
|
+
this.selected = name
|
|
185
|
+
this.onSelect(name)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
/** a click selects once it is sure no second click follows */
|
|
190
|
+
click(name: string): void {
|
|
191
|
+
this.select(name)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** a double click also opens the folder in VS Code (its first click already selected) */
|
|
195
|
+
doubleClick(name: string): void {
|
|
196
|
+
void this.open(name)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** a repo's colours: what its config declares, else the ones SK024 would
|
|
200
|
+
* derive from its name, so every repo reads in a colour of its own */
|
|
201
|
+
identityOf(name: string): Identity {
|
|
202
|
+
return this.list.find((p) => p.name === name)?.identity ?? identityOfName(name)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** one colour set by hand: shown at once, written into the repo's
|
|
206
|
+
* repo.config.ts once the picker rests, so a drag across the colour
|
|
207
|
+
* field is one write, not one per frame */
|
|
208
|
+
setIdentity(name: string, patch: Partial<Identity>): void {
|
|
209
|
+
const next = { ...this.identityOf(name), ...patch }
|
|
210
|
+
this.list = this.list.map((p) => (p.name === name ? { ...p, identity: next } : p))
|
|
211
|
+
const t = this.identitySaves.get(name)
|
|
212
|
+
if (t != null) clearTimeout(t)
|
|
213
|
+
const h: ReturnType<typeof setTimeout> = setTimeout(() => void this.saveIdentity(name, next, h), IDENTITY_SAVE_MS)
|
|
214
|
+
this.identitySaves.set(name, h)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async saveIdentity(name: string, id: Identity, h: ReturnType<typeof setTimeout>): Promise<void> {
|
|
218
|
+
try {
|
|
219
|
+
const r = await fetch('/api/projects/identity', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, ...id }) })
|
|
220
|
+
if (!r.ok) throw new Error(await r.text())
|
|
221
|
+
const j = (await r.json()) as { identity: Identity }
|
|
222
|
+
runInAction(() => {
|
|
223
|
+
if (this.identitySaves.get(name) === h) this.list = this.list.map((p) => (p.name === name ? { ...p, identity: j.identity } : p))
|
|
224
|
+
this.error = ''
|
|
225
|
+
})
|
|
226
|
+
} catch (e) {
|
|
227
|
+
runInAction(() => { this.error = `colours of ${name}: ${(e as Error).message}` })
|
|
228
|
+
} finally {
|
|
229
|
+
// a later pick rests on its own timer, which keeps the guard
|
|
230
|
+
if (this.identitySaves.get(name) === h) this.identitySaves.delete(name)
|
|
231
|
+
}
|
|
118
232
|
}
|
|
119
233
|
|
|
120
234
|
get dragFrom(): number {
|
|
@@ -127,16 +241,12 @@ export class ProjectsSvc {
|
|
|
127
241
|
return this.gap
|
|
128
242
|
}
|
|
129
243
|
|
|
130
|
-
hoverIn(name: string
|
|
131
|
-
if (this.drag == null) this.hover =
|
|
244
|
+
hoverIn(name: string): void {
|
|
245
|
+
if (this.drag == null) this.hover = name
|
|
132
246
|
}
|
|
133
247
|
|
|
134
248
|
hoverOut(name: string): void {
|
|
135
|
-
if (this.hover
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
get hovered(): Project | null {
|
|
139
|
-
return this.hover == null ? null : this.list.find((p) => p.name === this.hover!.name) ?? null
|
|
249
|
+
if (this.hover === name) this.hover = null
|
|
140
250
|
}
|
|
141
251
|
|
|
142
252
|
dragStart(name: string): void {
|
|
@@ -187,6 +297,7 @@ export class ProjectsSvc {
|
|
|
187
297
|
const gen = ++this.gen
|
|
188
298
|
const tick = async (): Promise<void> => {
|
|
189
299
|
await this.load()
|
|
300
|
+
this.keep()
|
|
190
301
|
if (gen === this.gen) timer = setTimeout(() => void tick(), PROJECTS_POLL_MS)
|
|
191
302
|
}
|
|
192
303
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -197,6 +308,15 @@ export class ProjectsSvc {
|
|
|
197
308
|
}
|
|
198
309
|
}
|
|
199
310
|
|
|
311
|
+
/** the list as it is now, kept for the next load. Only a change is
|
|
312
|
+
* written: the poll repeats the same rows every five seconds */
|
|
313
|
+
private kept = ''
|
|
314
|
+
private keep(): void {
|
|
315
|
+
if (!this.fresh) return
|
|
316
|
+
const s = JSON.stringify(this.list)
|
|
317
|
+
if (s !== this.kept && saveList(this.list)) this.kept = s
|
|
318
|
+
}
|
|
319
|
+
|
|
200
320
|
async load(): Promise<void> {
|
|
201
321
|
const order = this.orderGen
|
|
202
322
|
try {
|
|
@@ -206,8 +326,10 @@ export class ProjectsSvc {
|
|
|
206
326
|
runInAction(() => {
|
|
207
327
|
// a reorder since this left, or a drag in flight: the next poll answers
|
|
208
328
|
if (order !== this.orderGen || this.drag != null) return
|
|
209
|
-
this.list
|
|
329
|
+
const mine = new Map(this.list.filter((p) => this.identitySaves.has(p.name)).map((p) => [p.name, p.identity]))
|
|
330
|
+
this.list = mine.size === 0 ? j.projects : j.projects.map((p) => (mine.has(p.name) ? { ...p, identity: mine.get(p.name)! } : p))
|
|
210
331
|
this.error = ''
|
|
332
|
+
this.fresh = true
|
|
211
333
|
})
|
|
212
334
|
} catch (e) {
|
|
213
335
|
runInAction(() => { this.error = `projects: ${(e as Error).message}` })
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// the same reasons a switch is.
|
|
6
6
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
7
7
|
import type { ProseGroup, ProseView } from '../../prose/prose-view.ts'
|
|
8
|
-
import type {
|
|
8
|
+
import type { RepoSt } from './repo-st.ts'
|
|
9
9
|
|
|
10
10
|
export type TierFilter = 'all' | 'blocking' | 'soft'
|
|
11
11
|
export type ListKind = 'allow' | 'ban'
|
|
@@ -29,7 +29,7 @@ export class ProseHitsSt {
|
|
|
29
29
|
full = new Set<string>()
|
|
30
30
|
form: { kind: ListKind; word: string; to: string; reason: string } = { kind: 'allow', word: '', to: '', reason: '' }
|
|
31
31
|
|
|
32
|
-
constructor(private st:
|
|
32
|
+
constructor(private st: RepoSt) {
|
|
33
33
|
// .ref: the view is swapped whole on every load, never edited in place
|
|
34
34
|
makeAutoObservable<this, 'st'>(this, { st: false, view: observableRef })
|
|
35
35
|
}
|
|
@@ -37,7 +37,7 @@ export class ProseHitsSt {
|
|
|
37
37
|
async load(): Promise<void> {
|
|
38
38
|
runInAction(() => { this.loading = true })
|
|
39
39
|
try {
|
|
40
|
-
const r = await fetch('/api/prose.json')
|
|
40
|
+
const r = await fetch(this.st.url('/api/prose.json'))
|
|
41
41
|
if (!r.ok) throw new Error(await r.text())
|
|
42
42
|
const v = (await r.json()) as ProseView
|
|
43
43
|
runInAction(() => { this.view = v; this.error = '' })
|
|
@@ -9,7 +9,7 @@ import type { ProseGroup, ProseViewHit } from '../../prose/prose-view.ts'
|
|
|
9
9
|
import type { ProseWords } from '../../prose/ste.ts'
|
|
10
10
|
import { editorHref } from './check-words.ts'
|
|
11
11
|
import { Help } from './help.tsx'
|
|
12
|
-
import type {
|
|
12
|
+
import type { RepoSt } from './repo-st.ts'
|
|
13
13
|
import { GROUP_CAP, HIT_CAP, ProseHitsSt, type ListKind, type TierFilter } from './prose-hits-st.ts'
|
|
14
14
|
|
|
15
15
|
/** blocking tiers read red, the soft tier reads as a warning */
|
|
@@ -47,7 +47,7 @@ const HitLine = ({ root, h, shape }: { root: string; h: ProseViewHit; shape: boo
|
|
|
47
47
|
)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const Group = observer(({ app, s, g }: { app:
|
|
50
|
+
const Group = observer(({ app, s, g }: { app: RepoSt; s: ProseHitsSt; g: ProseGroup }) => {
|
|
51
51
|
const open = s.open.has(g.key)
|
|
52
52
|
const t = TIER_WORD[g.tier] ?? { word: g.tier, tone: '' }
|
|
53
53
|
const shown = s.full.has(g.key) ? g.hits : g.hits.slice(0, HIT_CAP)
|
|
@@ -77,7 +77,7 @@ const Group = observer(({ app, s, g }: { app: HubSt; s: ProseHitsSt; g: ProseGro
|
|
|
77
77
|
)
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
const Hits = observer(({ app, s }: { app:
|
|
80
|
+
const Hits = observer(({ app, s }: { app: RepoSt; s: ProseHitsSt }) => {
|
|
81
81
|
const v = s.view
|
|
82
82
|
if (v == null) return <div className="dim">{s.error !== '' ? s.error : 'reading the hits…'}</div>
|
|
83
83
|
if (!v.scoped) return <div className="dim">this repo declares no prose scope, so the check reads no file: add `prose: {'{'} include: [...] {'}'}` to repo.config.ts</div>
|
|
@@ -130,7 +130,7 @@ const ListRows = ({ s, words }: { s: ProseHitsSt; words: ProseWords }) => {
|
|
|
130
130
|
)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const Lists = observer(({ app, s }: { app:
|
|
133
|
+
const Lists = observer(({ app, s }: { app: RepoSt; s: ProseHitsSt }) => {
|
|
134
134
|
const f = s.form
|
|
135
135
|
const g = app.checks.gate
|
|
136
136
|
return (
|
|
@@ -154,7 +154,7 @@ const Lists = observer(({ app, s }: { app: HubSt; s: ProseHitsSt }) => {
|
|
|
154
154
|
})
|
|
155
155
|
|
|
156
156
|
/** the two sections over one state, the part a test renders headless */
|
|
157
|
-
export const ProseSections = observer(({ app, s }: { app:
|
|
157
|
+
export const ProseSections = observer(({ app, s }: { app: RepoSt; s: ProseHitsSt }) => (
|
|
158
158
|
<>
|
|
159
159
|
<section className="csec">
|
|
160
160
|
<div className="cstitle">hits<Help text="every hit of the last read, grouped by the word or the rule that fired, the noisiest first. A file:line opens the editor on that line" /></div>
|
|
@@ -169,7 +169,7 @@ export const ProseSections = observer(({ app, s }: { app: HubSt; s: ProseHitsSt
|
|
|
169
169
|
|
|
170
170
|
/** mounted by the check page for SK014 only: the state lives while the page
|
|
171
171
|
* is open, and reads the hits once on the way in */
|
|
172
|
-
export const ProseHits = observer(({ app }: { app:
|
|
172
|
+
export const ProseHits = observer(({ app }: { app: RepoSt }) => {
|
|
173
173
|
const s = useMemo(() => new ProseHitsSt(app), [app])
|
|
174
174
|
useEffect(() => { void s.load() }, [s])
|
|
175
175
|
return <ProseSections app={app} s={s} />
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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.
|
|
7
8
|
import { RUNS_ID } from '../../collections/collection-types.ts'
|
|
8
9
|
import { makeAutoObservable } from 'mobx'
|
|
9
10
|
import { ClockSvc } from './clock-svc.ts'
|
|
@@ -16,114 +17,197 @@ import { MediaSvc } from './media-svc.ts'
|
|
|
16
17
|
import { TimelineSvc } from './timeline-svc.ts'
|
|
17
18
|
import { TreeSvc } from './tree-svc.ts'
|
|
18
19
|
import { ChecksSvc } from './checks-svc.ts'
|
|
19
|
-
import {
|
|
20
|
+
import { ProcessesSvc } from './processes-svc.ts'
|
|
20
21
|
import { TestsSvc } from './tests-svc.ts'
|
|
21
22
|
import type { Resource } from './hub-types.ts'
|
|
23
|
+
import type { HomeSt } from './home-st.ts'
|
|
22
24
|
|
|
23
25
|
export type Sel = HashSel | null
|
|
24
26
|
|
|
25
|
-
/** one
|
|
27
|
+
/** one picker target (cmd+k): run() fires the SAME action clicking would */
|
|
26
28
|
export type Jump = { id: string; label: string; section: string; run: () => void }
|
|
27
29
|
|
|
28
30
|
/** all · needs (yellow+red) · red — one severity ladder for every section */
|
|
29
31
|
export type Filter = 'all' | 'needs' | 'red'
|
|
30
|
-
const parseFilter = (search: string): Filter => {
|
|
32
|
+
export const parseFilter = (search: string): Filter => {
|
|
31
33
|
const f = new URLSearchParams(search).get('f')
|
|
32
34
|
return f === 'needs' || f === 'red' ? f : 'all'
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
export class
|
|
36
|
-
name
|
|
37
|
+
export class RepoSt {
|
|
38
|
+
/** the repo's name, from /hub.json; its key until that answers */
|
|
39
|
+
name: string
|
|
37
40
|
/** where this repo sits on disk, from /hub.json: what makes a source
|
|
38
41
|
* path on a page a link that opens his editor */
|
|
39
42
|
root = ''
|
|
40
|
-
/** the one second tick every age
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
/** the one second tick every age of this repo reads (clock-svc.ts). It
|
|
44
|
+
* ticks only while the repo is shown, so a hidden page repaints nothing */
|
|
45
|
+
clock: ClockSvc
|
|
46
|
+
media: MediaSvc
|
|
47
|
+
cols: ColsSvc
|
|
48
|
+
tree: TreeSvc
|
|
49
|
+
health: HealthSvc
|
|
50
|
+
checks: ChecksSvc
|
|
51
|
+
comfy: ComfySvc
|
|
52
|
+
timeline: TimelineSvc
|
|
49
53
|
/** the runs in flight, whoever started them (live-svc.ts) */
|
|
50
|
-
live
|
|
51
|
-
|
|
54
|
+
live: LiveSvc
|
|
55
|
+
processes: ProcessesSvc
|
|
52
56
|
/** the pages behind the test groups of the tree (tests-svc.ts) */
|
|
53
|
-
tests
|
|
57
|
+
tests: TestsSvc
|
|
54
58
|
sel: Sel = null
|
|
55
|
-
filter: Filter =
|
|
59
|
+
filter: Filter = 'all'
|
|
56
60
|
/** sidebar search — live substring filter over asset + collection rows */
|
|
57
61
|
query = ''
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
/** the first resume or prefetch ran: on the home, only once this repo's
|
|
63
|
+
* hub answered */
|
|
64
|
+
started = false
|
|
65
|
+
/** the page showed it once: only a repo shown has a region mounted, so a
|
|
66
|
+
* repo read ahead in the background costs no render */
|
|
67
|
+
seen = false
|
|
68
|
+
/** the sidebar search box, so `/` focuses this repo's own */
|
|
69
|
+
searchEl: HTMLInputElement | null = null
|
|
70
|
+
/** its timers run: only the shown repo's do */
|
|
71
|
+
polling = false
|
|
72
|
+
private beat = false
|
|
73
|
+
/** the hub name, the gate block and STATUS.md were asked for */
|
|
74
|
+
private onceRead = false
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
/** `key` names the repo in the root's map, `base` is where its hub
|
|
77
|
+
* answers: '/r/<repo>' on the home, '' on a hub of its own */
|
|
78
|
+
constructor(readonly home: HomeSt, readonly key: string, readonly base: string) {
|
|
79
|
+
this.name = key
|
|
80
|
+
// children built in the ctor body: a field initializer would run
|
|
81
|
+
// before `home` is assigned
|
|
82
|
+
this.clock = new ClockSvc()
|
|
83
|
+
this.media = new MediaSvc(this)
|
|
84
|
+
this.cols = new ColsSvc(this)
|
|
85
|
+
this.tree = new TreeSvc(this)
|
|
86
|
+
this.health = new HealthSvc(this)
|
|
87
|
+
this.checks = new ChecksSvc(this)
|
|
88
|
+
this.comfy = new ComfySvc(this)
|
|
89
|
+
this.timeline = new TimelineSvc(this)
|
|
90
|
+
this.live = new LiveSvc(this)
|
|
91
|
+
this.processes = new ProcessesSvc(this)
|
|
92
|
+
this.tests = new TestsSvc(this)
|
|
93
|
+
makeAutoObservable<this, 'beat' | 'onceRead'>(this, { onceRead: false, home: false, key: false, base: false, clock: false, media: false, cols: false, tree: false, health: false, checks: false, comfy: false, timeline: false, live: false, processes: false, tests: false, searchEl: false, beat: false })
|
|
65
94
|
}
|
|
66
95
|
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
/** an absolute hub path ('/api/…', '/media/…', '/hub.json') as this
|
|
97
|
+
* repo's hub answers it. Every request of the repo goes through here */
|
|
98
|
+
url(path: string): string {
|
|
99
|
+
return path.startsWith('/') ? `${this.base}${path}` : path
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** the repo the page shows now: only it writes the url */
|
|
103
|
+
get isShown(): boolean {
|
|
104
|
+
return this.home.shown === this
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** what the url carries for this repo: its filter, then its selection */
|
|
108
|
+
get search(): string {
|
|
109
|
+
return this.filter === 'all' ? '' : `?f=${this.filter}`
|
|
110
|
+
}
|
|
111
|
+
get hash(): string {
|
|
112
|
+
const h = this.sel?.t === 'asset' ? this.media.hash : hashFor(this.sel)
|
|
113
|
+
return h == null ? '' : `#${h}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** what is read once, the first time the repo starts or is read ahead */
|
|
117
|
+
private readOnce(): Promise<unknown> {
|
|
118
|
+
this.started = true
|
|
119
|
+
this.onceRead = true
|
|
120
|
+
return Promise.all([this.readHub(), this.checks.loadGate(), this.health.load()])
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** the repo's name and where it sits on disk, from /hub.json */
|
|
124
|
+
private readHub(): Promise<void> {
|
|
125
|
+
return fetch(this.url('/hub.json'))
|
|
69
126
|
.then((r) => r.json() as Promise<{ name: string; root?: string }>)
|
|
70
127
|
.then((h) => { this.setHub(h.name, h.root ?? '') })
|
|
71
128
|
.catch(() => {})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** the first screen of the repo, read in the background while another
|
|
132
|
+
* one shows, so its first visit has its data at once. One step at a
|
|
133
|
+
* time: the browser opens six connections to the home, and the repo on
|
|
134
|
+
* screen keeps most of them. Nothing polls: a resume takes over when it
|
|
135
|
+
* shows */
|
|
136
|
+
async prefetch(): Promise<void> {
|
|
137
|
+
if (this.started) return
|
|
138
|
+
this.started = true
|
|
139
|
+
const steps: (() => Promise<unknown>)[] = [
|
|
140
|
+
() => this.cols.poll(),
|
|
141
|
+
() => this.readOnce(),
|
|
142
|
+
() => this.processes.load(),
|
|
143
|
+
() => this.media.heartbeat(),
|
|
144
|
+
() => this.live.poll(),
|
|
145
|
+
() => this.timeline.load(),
|
|
146
|
+
]
|
|
147
|
+
for (const step of steps) {
|
|
148
|
+
// shown meanwhile: its resume polls everything, so stop here
|
|
149
|
+
if (this.polling) return
|
|
150
|
+
await step()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** start polling. The first one also reads what is read once */
|
|
155
|
+
resume(): void {
|
|
156
|
+
if (this.polling) return
|
|
157
|
+
this.polling = true
|
|
158
|
+
if (!this.onceRead) {
|
|
159
|
+
void this.readOnce()
|
|
160
|
+
this.applyHash()
|
|
161
|
+
}
|
|
72
162
|
this.clock.start()
|
|
163
|
+
this.processes.start()
|
|
73
164
|
this.media.start()
|
|
74
165
|
this.cols.start()
|
|
75
166
|
this.live.start()
|
|
76
|
-
void this.checks.loadGate()
|
|
77
|
-
void this.health.load()
|
|
78
|
-
// back, forward, a pasted link and a hand edited hash all arrive
|
|
79
|
-
// here; applyHash is idempotent, so the two events firing together
|
|
80
|
-
// for one move cost nothing
|
|
81
|
-
window.addEventListener('hashchange', () => this.applyHash())
|
|
82
|
-
window.addEventListener('popstate', () => this.applyHash())
|
|
83
|
-
this.applyHash()
|
|
84
167
|
}
|
|
168
|
+
|
|
169
|
+
/** stop polling, keep every state: a resume shows the same page */
|
|
170
|
+
pause(): void {
|
|
171
|
+
if (!this.polling) return
|
|
172
|
+
this.polling = false
|
|
173
|
+
this.clock.stop()
|
|
174
|
+
this.processes.stop()
|
|
175
|
+
this.media.stop()
|
|
176
|
+
this.cols.stop()
|
|
177
|
+
this.live.stop()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
dispose(): void {
|
|
181
|
+
this.pause()
|
|
182
|
+
this.media.dispose()
|
|
183
|
+
this.checks.dispose()
|
|
184
|
+
this.comfy.dispose()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
markSeen(): void {
|
|
188
|
+
this.seen = true
|
|
189
|
+
}
|
|
190
|
+
|
|
85
191
|
private setHub(n: string, root: string): void {
|
|
86
192
|
this.name = n
|
|
87
193
|
this.root = root
|
|
88
|
-
document.title = `${n} · hub`
|
|
89
194
|
}
|
|
90
195
|
|
|
91
196
|
setFilter(v: Filter): void {
|
|
92
197
|
this.filter = v
|
|
198
|
+
if (!this.isShown) return
|
|
93
199
|
const u = new URL(location.href)
|
|
94
200
|
v === 'all' ? u.searchParams.delete('f') : u.searchParams.set('f', v)
|
|
95
201
|
history.replaceState(null, '', u)
|
|
96
202
|
}
|
|
203
|
+
/** the atelier rewrites its tool set in place: same page, no entry */
|
|
204
|
+
replaceHash(h: string): void {
|
|
205
|
+
if (this.isShown && location.hash.slice(1) !== h) history.replaceState(null, '', `#${h}`)
|
|
206
|
+
}
|
|
97
207
|
setQuery(v: string): void {
|
|
98
208
|
this.query = v
|
|
99
209
|
}
|
|
100
210
|
|
|
101
|
-
// ── cmd+j palette ─────────────────────────────────────────────────────
|
|
102
|
-
palToggle(): void {
|
|
103
|
-
this.palOpen = !this.palOpen
|
|
104
|
-
this.palQuery = ''
|
|
105
|
-
this.palIdx = 0
|
|
106
|
-
}
|
|
107
|
-
palClose(): void {
|
|
108
|
-
this.palOpen = false
|
|
109
|
-
}
|
|
110
|
-
setPalQuery(v: string): void {
|
|
111
|
-
this.palQuery = v
|
|
112
|
-
this.palIdx = 0
|
|
113
|
-
}
|
|
114
|
-
setPalIdx(n: number): void {
|
|
115
|
-
this.palIdx = n
|
|
116
|
-
}
|
|
117
|
-
palMove(d: number): void {
|
|
118
|
-
const n = this.palResults.length
|
|
119
|
-
if (n > 0) this.palIdx = (this.palIdx + d + n) % n
|
|
120
|
-
}
|
|
121
|
-
palRun(): void {
|
|
122
|
-
const j = this.palResults[this.palIdx]
|
|
123
|
-
if (j == null) return
|
|
124
|
-
this.palOpen = false
|
|
125
|
-
j.run()
|
|
126
|
-
}
|
|
127
211
|
/** every jump target: section titles, media assets, collection instances */
|
|
128
212
|
get jumps(): Jump[] {
|
|
129
213
|
const out: Jump[] = []
|
|
@@ -140,20 +224,6 @@ export class HubSt {
|
|
|
140
224
|
}
|
|
141
225
|
return out
|
|
142
226
|
}
|
|
143
|
-
/** ranked: prefix matches first, then substring; capped at 12 */
|
|
144
|
-
get palResults(): Jump[] {
|
|
145
|
-
const q = this.palQuery.trim().toLowerCase()
|
|
146
|
-
if (q === '') return this.jumps.slice(0, 12)
|
|
147
|
-
const pre: Jump[] = []
|
|
148
|
-
const sub: Jump[] = []
|
|
149
|
-
for (const j of this.jumps) {
|
|
150
|
-
const l = j.label.toLowerCase()
|
|
151
|
-
if (l.startsWith(q)) pre.push(j)
|
|
152
|
-
else if (l.includes(q)) sub.push(j)
|
|
153
|
-
}
|
|
154
|
-
return [...pre, ...sub].slice(0, 12)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
227
|
// ── selection + hash deep-links ───────────────────────────────────────
|
|
158
228
|
selectAsset(id: string): void {
|
|
159
229
|
this.setSel({ t: 'asset', id })
|
|
@@ -182,7 +252,7 @@ export class HubSt {
|
|
|
182
252
|
setSel(sel: Sel): void {
|
|
183
253
|
this.sel = sel
|
|
184
254
|
const h = hashFor(sel)
|
|
185
|
-
if (h == null) return
|
|
255
|
+
if (h == null || !this.isShown) return
|
|
186
256
|
const cur = parseHash(currentHash())
|
|
187
257
|
const next = parseHash(h)
|
|
188
258
|
// the tool set rides after the asset id (media-svc syncHash), so an
|
|
@@ -220,6 +290,7 @@ export class HubSt {
|
|
|
220
290
|
/** the url decides the page: a reload, back, forward and a pasted link
|
|
221
291
|
* all land here, and only a hash naming something else moves the view */
|
|
222
292
|
applyHash(): void {
|
|
293
|
+
if (!this.isShown) return
|
|
223
294
|
const p = parseHash(currentHash())
|
|
224
295
|
const named = selOf(p)
|
|
225
296
|
if (named == null) return
|
|
@@ -243,8 +314,8 @@ export class HubSt {
|
|
|
243
314
|
}
|
|
244
315
|
/** first heartbeat has the asset list — only then can a hash resolve */
|
|
245
316
|
afterFirstBeat(): void {
|
|
246
|
-
if (this.
|
|
247
|
-
this.
|
|
317
|
+
if (this.beat || !this.isShown) return
|
|
318
|
+
this.beat = true
|
|
248
319
|
this.applyHash()
|
|
249
320
|
}
|
|
250
321
|
}
|
|
@@ -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 live strip under the header: unfold it and press fit` },
|
|
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' },
|