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
|
@@ -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
|
+
}
|
|
@@ -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(() => {
|
|
@@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'
|
|
|
8
8
|
import type { FailingFile, FileTime, LastRun, Phases, Shape, TestCheckRow, TestCheckView, TestsRootView } from '../../collections/test-time.ts'
|
|
9
9
|
import { CommandLine, LastRunValue, Row, Says, Section, Timings } from './check-view.tsx'
|
|
10
10
|
import { durationTone, fmtMs } from './dur.ts'
|
|
11
|
-
import type {
|
|
11
|
+
import type { RepoSt } from './repo-st.ts'
|
|
12
12
|
import { TimeAgo } from './time-ago.tsx'
|
|
13
13
|
|
|
14
14
|
const pct = (n: number | null): string => (n == null ? '' : `${Math.round(n * 100)}%`)
|
|
@@ -22,13 +22,13 @@ const TREND_TONE = { up: 'bad', down: 'fast', flat: 'dim' } as const
|
|
|
22
22
|
|
|
23
23
|
export const SHAPE_WORD: Record<Shape, string> = { tail: 'long tail', volume: 'volume', overhead: 'overhead' }
|
|
24
24
|
|
|
25
|
-
const FileLink = ({ app, file, name }: { app:
|
|
25
|
+
const FileLink = ({ app, file, name }: { app: RepoSt; file: string; name: string }) => (
|
|
26
26
|
<a href="#" title={`${file}: its own page`} onClick={(e) => { e.preventDefault(); e.stopPropagation(); app.tests.openFile(file) }}>{name}</a>
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
// ── failing now ─────────────────────────────────────────────────────────
|
|
30
30
|
|
|
31
|
-
const Failing = observer(({ app, rows, showCheck }: { app:
|
|
31
|
+
const Failing = observer(({ app, rows, showCheck }: { app: RepoSt; rows: FailingFile[]; showCheck: boolean }) => (
|
|
32
32
|
<Section title={`failing now (${rows.length})`} help="the files whose last recorded run failed: the same verdict that colours them red in the tree and lists them on the runs page tab">
|
|
33
33
|
{rows.length === 0
|
|
34
34
|
? <div className="dim">nothing: every file passed its last recorded run</div>
|
|
@@ -50,7 +50,7 @@ const Failing = observer(({ app, rows, showCheck }: { app: HubSt; rows: FailingF
|
|
|
50
50
|
|
|
51
51
|
// ── where the time goes ─────────────────────────────────────────────────
|
|
52
52
|
|
|
53
|
-
const Summary = observer(({ app, l }: { app:
|
|
53
|
+
const Summary = observer(({ app, l }: { app: RepoSt; l: LastRun }) => (
|
|
54
54
|
<div className="tsum">
|
|
55
55
|
<div className="tsline">
|
|
56
56
|
<span><b>{l.files}</b> files</span>
|
|
@@ -121,7 +121,7 @@ const PhaseFacts = ({ p, wallMs }: { p: Phases; wallMs: number | null }) => {
|
|
|
121
121
|
)
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
const FileRows = observer(({ app, files }: { app:
|
|
124
|
+
const FileRows = observer(({ app, files }: { app: RepoSt; files: FileTime[] }) => (
|
|
125
125
|
<div className="rlist">
|
|
126
126
|
<div className="rhead rtime">
|
|
127
127
|
<span className="tleft">file</span>
|
|
@@ -152,13 +152,13 @@ const FileRows = observer(({ app, files }: { app: HubSt; files: FileTime[] }) =>
|
|
|
152
152
|
|
|
153
153
|
// ── the pages ────────────────────────────────────────────────────────────
|
|
154
154
|
|
|
155
|
-
const Loading = observer(({ app, check }: { app:
|
|
155
|
+
const Loading = observer(({ app, check }: { app: RepoSt; check: string | null }) => {
|
|
156
156
|
const err = app.tests.error(check)
|
|
157
157
|
if (err !== '') return <div className="warnline">✗ {err}</div>
|
|
158
158
|
return <div className="dim">{app.tests.isLoading(check) ? 'reading the record…' : 'nothing loaded yet'}</div>
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
export const TestCheckPage = observer(({ app, check }: { app:
|
|
161
|
+
export const TestCheckPage = observer(({ app, check }: { app: RepoSt; check: string }) => {
|
|
162
162
|
const i = app.cols.find('checks', `check:${check}`)
|
|
163
163
|
const u = i?.check ?? null
|
|
164
164
|
const v: TestCheckView | null = app.tests.check(check)
|
|
@@ -194,7 +194,7 @@ export const TestCheckPage = observer(({ app, check }: { app: HubSt; check: stri
|
|
|
194
194
|
)
|
|
195
195
|
})
|
|
196
196
|
|
|
197
|
-
const CheckRows = observer(({ app, rows }: { app:
|
|
197
|
+
const CheckRows = observer(({ app, rows }: { app: RepoSt; rows: TestCheckRow[] }) => (
|
|
198
198
|
<div className="rlist">
|
|
199
199
|
<div className="rhead rcheckt">
|
|
200
200
|
<span className="tleft">check</span>
|
|
@@ -217,7 +217,7 @@ const CheckRows = observer(({ app, rows }: { app: HubSt; rows: TestCheckRow[] })
|
|
|
217
217
|
</div>
|
|
218
218
|
))
|
|
219
219
|
|
|
220
|
-
export const TestsRootPage = observer(({ app }: { app:
|
|
220
|
+
export const TestsRootPage = observer(({ app }: { app: RepoSt }) => {
|
|
221
221
|
const v: TestsRootView | null = app.tests.root()
|
|
222
222
|
return (
|
|
223
223
|
<div className="colcard checkcard">
|
|
@@ -234,5 +234,5 @@ export const TestsRootPage = observer(({ app }: { app: HubSt }) => {
|
|
|
234
234
|
)
|
|
235
235
|
})
|
|
236
236
|
|
|
237
|
-
export const TestsPage = observer(({ app, check }: { app:
|
|
237
|
+
export const TestsPage = observer(({ app, check }: { app: RepoSt; check: string | null }) =>
|
|
238
238
|
check == null ? <TestsRootPage app={app} /> : <TestCheckPage app={app} check={check} />)
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// run. Geometry is timeline-geometry.ts, view state chart-view.ts; this file
|
|
6
6
|
// only draws.
|
|
7
7
|
import { observer } from 'mobx-react-lite'
|
|
8
|
+
import { autoUpdate, FloatingPortal, useFloating } from '@floating-ui/react'
|
|
8
9
|
import { useEffect, useLayoutEffect, useRef } from 'react'
|
|
10
|
+
import { middleware } from './floating.tsx'
|
|
9
11
|
import type { RepoLane } from '../../ledger/live-timeline.ts'
|
|
10
12
|
import type { TimelineBar } from '../../ledger/run-timeline.ts'
|
|
11
13
|
import type { ChartTip, ChartView } from './chart-view.ts'
|
|
@@ -15,7 +17,7 @@ import { CACHED } from '../../core/outcome-words.ts'
|
|
|
15
17
|
|
|
16
18
|
/** who started a run, in words. A starter this build does not name reads as
|
|
17
19
|
* the word the record holds */
|
|
18
|
-
export const BY_WORD: Record<string, string> = { cli: 'terminal', hub: 'hub', schedule: '
|
|
20
|
+
export const BY_WORD: Record<string, string> = { cli: 'terminal', hub: 'hub', schedule: 'the plan', save: 'a save', unknown: 'before the record' }
|
|
19
21
|
export const byWords = (by: string): string => BY_WORD[by] ?? by
|
|
20
22
|
|
|
21
23
|
/** the one tone a bar wears: running in the accent, then the verdict, a
|
|
@@ -35,8 +37,8 @@ export const clockWords = (t: number): string => new Date(t).toLocaleTimeString(
|
|
|
35
37
|
* when it ended or that it still grows */
|
|
36
38
|
export const gapTipWords = (g: GapSlice): string => (g.live ? `since ${clockWords(g.t0)}, nothing ran` : `${clockWords(g.t0)} to ${clockWords(g.t1)}, nothing ran`)
|
|
37
39
|
|
|
38
|
-
/** a point on the viewport
|
|
39
|
-
*
|
|
40
|
+
/** a point on the viewport the tooltip floats from, out of every box of
|
|
41
|
+
* the strip that clips */
|
|
40
42
|
export type TipAt = { x: number; y: number }
|
|
41
43
|
|
|
42
44
|
/** what hovering a bar says: its check, its verdict with its duration or its
|
|
@@ -48,22 +50,32 @@ export const barTip = (b: PlacedBar, now: number, at: TipAt): ChartTip => ({
|
|
|
48
50
|
lines: [verdictWords(b, now), `${clockWords(b.start)} · ${b.label} · ${byWords(b.by)}`],
|
|
49
51
|
})
|
|
50
52
|
|
|
51
|
-
/** right of the pointer, or left of it past the middle of the window, so a
|
|
52
|
-
* tooltip over the running bars at the right edge stays on screen */
|
|
53
|
-
export const tipStyle = (t: TipAt, viewportW: number): { left: number; top: number } | { right: number; top: number } =>
|
|
54
|
-
t.x > viewportW / 2 ? { right: viewportW - t.x, top: t.y } : { left: t.x, top: t.y }
|
|
55
|
-
|
|
56
53
|
/** under the element the pointer entered, at the pointer's x */
|
|
57
54
|
const tipAtEvent = (e: React.MouseEvent<Element>): TipAt => ({ x: e.clientX, y: e.currentTarget.getBoundingClientRect().bottom + 4 })
|
|
58
55
|
|
|
56
|
+
/** what a tooltip says: its head in its tone, then its lines */
|
|
57
|
+
export const TipBody = ({ t }: { t: ChartTip }) => (
|
|
58
|
+
<>
|
|
59
|
+
<b className={`tl ${t.tone}`}>{t.head}</b>
|
|
60
|
+
{t.lines.map((l) => <span key={l}>{l}</span>)}
|
|
61
|
+
</>
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
/** the hovered mark's words, floated from the point the pointer entered at
|
|
65
|
+
* (floating.tsx middleware: it flips and shifts to stay in the window) */
|
|
59
66
|
const Tip = observer(({ view }: { view: ChartView }) => {
|
|
60
67
|
const t = view.hover
|
|
68
|
+
const f = useFloating({ open: t != null, placement: 'bottom-start', strategy: 'fixed', middleware: middleware(0), whileElementsMounted: autoUpdate })
|
|
69
|
+
const x = t?.x ?? 0
|
|
70
|
+
const y = t?.y ?? 0
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
f.refs.setPositionReference({ getBoundingClientRect: () => ({ x, y, left: x, top: y, right: x, bottom: y, width: 0, height: 0 }) })
|
|
73
|
+
}, [f.refs, x, y])
|
|
61
74
|
if (t == null) return null
|
|
62
75
|
return (
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
</div>
|
|
76
|
+
<FloatingPortal>
|
|
77
|
+
<div ref={f.refs.setFloating} className="tlaxtip" style={f.floatingStyles}><TipBody t={t} /></div>
|
|
78
|
+
</FloatingPortal>
|
|
67
79
|
)
|
|
68
80
|
})
|
|
69
81
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { testId } from '../../collections/collection-types.ts'
|
|
7
7
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
8
8
|
import type { Timeline } from '../../ledger/run-timeline.ts'
|
|
9
|
-
import type {
|
|
9
|
+
import type { RepoSt } from './repo-st.ts'
|
|
10
10
|
|
|
11
11
|
export type { Timeline }
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ export class TimelineSvc {
|
|
|
18
18
|
* the reader closes it, every other one is shut until opened */
|
|
19
19
|
opened = new Set<string>()
|
|
20
20
|
closed = new Set<string>()
|
|
21
|
-
constructor(private st:
|
|
21
|
+
constructor(private st: RepoSt) {
|
|
22
22
|
makeAutoObservable<this, 'st'>(this, { st: false, data: observableRef })
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -46,10 +46,11 @@ export class TimelineSvc {
|
|
|
46
46
|
this.st.cols.select('tests', testId(file))
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
/** `fresh` after a run: the record it just wrote, not the warm answer */
|
|
50
|
+
async load(fresh = false): Promise<void> {
|
|
50
51
|
runInAction(() => { this.loading = true })
|
|
51
52
|
try {
|
|
52
|
-
const r = await fetch('/api/timeline.json')
|
|
53
|
+
const r = await fetch(this.st.url(fresh ? '/api/timeline.json?fresh=1' : '/api/timeline.json'))
|
|
53
54
|
if (!r.ok) throw new Error(await r.text())
|
|
54
55
|
const t = (await r.json()) as Timeline
|
|
55
56
|
runInAction(() => { this.data = t; this.error = '' })
|
|
@@ -14,7 +14,7 @@ import type { InvocationSummary, SlowCheck, SuiteToFix, Timeline, TimelineBar, T
|
|
|
14
14
|
import { byWords, verdictWords } from './timeline-chart.tsx'
|
|
15
15
|
import { durationTone, fmtMs } from './dur.ts'
|
|
16
16
|
import { Help } from './help.tsx'
|
|
17
|
-
import type {
|
|
17
|
+
import type { RepoSt } from './repo-st.ts'
|
|
18
18
|
import { TimeAgo, stampWords } from './time-ago.tsx'
|
|
19
19
|
import { checkName, editorHref } from './check-words.ts'
|
|
20
20
|
import { RUNS_TABS, runsTab, runsTabId, type RunsTab } from './runs-tabs.ts'
|
|
@@ -40,7 +40,7 @@ export function tabBadge(tab: RunsTab, d: Timeline): TabBadge | null {
|
|
|
40
40
|
|
|
41
41
|
/** who started a run, as a tag short enough for one fixed column. A starter
|
|
42
42
|
* this build does not name reads as the word the record holds */
|
|
43
|
-
export const BY_TAG: Record<string, string> = { cli: 'terminal', hub: 'hub', schedule: '
|
|
43
|
+
export const BY_TAG: Record<string, string> = { cli: 'terminal', hub: 'hub', schedule: 'plan', save: 'save', unknown: 'unknown' }
|
|
44
44
|
export const byTag = (by: string): string => BY_TAG[by] ?? by
|
|
45
45
|
|
|
46
46
|
/** a check's verdict, as the label's colour class: the tree's `tl` tones */
|
|
@@ -90,7 +90,7 @@ export function childOrder(bars: readonly TimelineBar[]): TimelineBar[] {
|
|
|
90
90
|
|
|
91
91
|
/** one check of a run, a child line on the run's grid: its word sits in the
|
|
92
92
|
* column its run counts it in */
|
|
93
|
-
const CheckLine = ({ app, b, name }: { app:
|
|
93
|
+
const CheckLine = ({ app, b, name }: { app: RepoSt; b: TimelineBar; name: ChildName }) => (
|
|
94
94
|
<div className={`rline rchild${b.status === 'fail' ? ' red' : ''}`} onClick={() => app.timeline.openCheck(b.id)}
|
|
95
95
|
title={[name.description ?? '', `${b.id}: ${verdictWords(b)}`, stampWords(b.start)].filter((x) => x !== '').join('\n')}>
|
|
96
96
|
<span />
|
|
@@ -102,7 +102,7 @@ const CheckLine = ({ app, b, name }: { app: HubSt; b: TimelineBar; name: ChildNa
|
|
|
102
102
|
</div>
|
|
103
103
|
)
|
|
104
104
|
|
|
105
|
-
const RunLine = observer(({ app, inv, n }: { app:
|
|
105
|
+
const RunLine = observer(({ app, inv, n }: { app: RepoSt; inv: TimelineInvocation; n: number }) => {
|
|
106
106
|
const open = app.timeline.isOpen(inv.runId, n)
|
|
107
107
|
return (
|
|
108
108
|
<>
|
|
@@ -122,12 +122,12 @@ const RunLine = observer(({ app, inv, n }: { app: HubSt; inv: TimelineInvocation
|
|
|
122
122
|
|
|
123
123
|
/** the checks of an unfolded run, in a box of their own that scrolls: a full
|
|
124
124
|
* gate is fifty checks, which would push every other run off the page */
|
|
125
|
-
const RunChecks = observer(({ app, bars }: { app:
|
|
125
|
+
const RunChecks = observer(({ app, bars }: { app: RepoSt; bars: TimelineBar[] }) => {
|
|
126
126
|
const names = checkNames(app.cols.resources)
|
|
127
127
|
return <div className="rkids">{childOrder(bars).map((b) => <CheckLine key={`${b.id}:${b.start}`} app={app} b={b} name={nameOf(names, b.id)} />)}</div>
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
-
const Runs = ({ app, rows }: { app:
|
|
130
|
+
const Runs = ({ app, rows }: { app: RepoSt; rows: TimelineInvocation[] }) => (
|
|
131
131
|
rows.length === 0 ? <div className="rempty">no run on record yet</div> : <>{rows.map((inv, n) => <RunLine key={inv.runId} app={app} inv={inv} n={n} />)}</>
|
|
132
132
|
)
|
|
133
133
|
|
|
@@ -142,7 +142,7 @@ const inUnit = (r: SuiteToFix): string => {
|
|
|
142
142
|
return at < 0 ? r.file : r.file.slice(at + r.unit.length + 2)
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
const TestFiles = ({ app, rows, recorded }: { app:
|
|
145
|
+
const TestFiles = ({ app, rows, recorded }: { app: RepoSt; rows: SuiteToFix[]; recorded: boolean }) => {
|
|
146
146
|
if (!recorded) return <div className="rempty">no test file run recorded yet: the next run of a test check records one</div>
|
|
147
147
|
if (rows.length === 0) return <div className="rempty">every test file passed and fits the budget</div>
|
|
148
148
|
return (
|
|
@@ -166,7 +166,7 @@ const TestFiles = ({ app, rows, recorded }: { app: HubSt; rows: SuiteToFix[]; re
|
|
|
166
166
|
const FLAG_WORD = { over: 'at its ceiling', near: 'past half its ceiling', budget: 'over its budget' } as const
|
|
167
167
|
|
|
168
168
|
/** two numbers side by side need their names once, above the column */
|
|
169
|
-
const Slowest = ({ app, rows }: { app:
|
|
169
|
+
const Slowest = ({ app, rows }: { app: RepoSt; rows: SlowCheck[] }) => {
|
|
170
170
|
if (rows.length === 0) return <div className="rempty">nothing ran yet</div>
|
|
171
171
|
return (
|
|
172
172
|
<>
|
|
@@ -183,7 +183,7 @@ const Slowest = ({ app, rows }: { app: HubSt; rows: SlowCheck[] }) => {
|
|
|
183
183
|
)
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
const Longest = ({ app, rows }: { app:
|
|
186
|
+
const Longest = ({ app, rows }: { app: RepoSt; rows: InvocationSummary[] }) => {
|
|
187
187
|
if (rows.length === 0) return <div className="rempty">no run on record yet</div>
|
|
188
188
|
return (
|
|
189
189
|
<>
|
|
@@ -202,7 +202,7 @@ const Longest = ({ app, rows }: { app: HubSt; rows: InvocationSummary[] }) => {
|
|
|
202
202
|
)
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
const Tabs = observer(({ app, on }: { app:
|
|
205
|
+
const Tabs = observer(({ app, on }: { app: RepoSt; on: RunsTab }) => {
|
|
206
206
|
const t = app.timeline
|
|
207
207
|
const d = t.data
|
|
208
208
|
return (
|
|
@@ -218,12 +218,12 @@ const Tabs = observer(({ app, on }: { app: HubSt; on: RunsTab }) => {
|
|
|
218
218
|
})}
|
|
219
219
|
<Help text={RUNS_TABS.find((x) => x.key === on)!.help} />
|
|
220
220
|
<span className="sp" />
|
|
221
|
-
<button disabled={t.loading} onClick={() => void t.load()}>{t.loading ? 'reading…' : 'reload'}</button>
|
|
221
|
+
<button disabled={t.loading} onClick={() => void t.load(true)}>{t.loading ? 'reading…' : 'reload'}</button>
|
|
222
222
|
</div>
|
|
223
223
|
)
|
|
224
224
|
})
|
|
225
225
|
|
|
226
|
-
export const TimelineView = observer(({ app }: { app:
|
|
226
|
+
export const TimelineView = observer(({ app }: { app: RepoSt }) => {
|
|
227
227
|
const t = app.timeline
|
|
228
228
|
useEffect(() => { void t.load() }, [t])
|
|
229
229
|
const d = t.data
|
|
@@ -232,7 +232,7 @@ export const TimelineView = observer(({ app }: { app: HubSt }) => {
|
|
|
232
232
|
<div className="colcard tlcard">
|
|
233
233
|
<Tabs app={app} on={on} />
|
|
234
234
|
{t.error !== '' ? <div className="warnline">{t.error}</div> : null}
|
|
235
|
-
{d == null ? <div className="dim"
|
|
235
|
+
{d == null ? <div className="dim twait"><span className="spin" />reading the run record…</div> : (
|
|
236
236
|
<div className={`rlist ${on}`}>
|
|
237
237
|
{on === 'runs' ? <Runs app={app} rows={d.invocations} />
|
|
238
238
|
: on === 'files' ? <TestFiles app={app} rows={d.testFiles} recorded={d.suitesRecorded} />
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// around. No row carries prose: the words are the tooltip and the page it opens.
|
|
5
5
|
import { observer } from 'mobx-react-lite'
|
|
6
6
|
import type { CSSProperties } from 'react'
|
|
7
|
-
import type {
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
8
|
import { GLYPH, markOf, rowName, rowSlots, type TreeRow } from './tree-build.ts'
|
|
9
9
|
|
|
10
10
|
/** the fold marker: a chevron, right when the branch is shut and down when
|
|
@@ -15,7 +15,7 @@ export const Caret = ({ open }: { open: boolean }): React.ReactElement => (
|
|
|
15
15
|
</svg>
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
export const TreeRowView = observer(({ app, row }: { app:
|
|
18
|
+
export const TreeRowView = observer(({ app, row }: { app: RepoSt; row: TreeRow }) => {
|
|
19
19
|
const t = app.tree
|
|
20
20
|
const u = app.checks
|
|
21
21
|
const running = row.checks.some((id) => u.isRunning(id))
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// newline separated paths, text encodable and hand editable, and comes back
|
|
5
5
|
// on reopen.
|
|
6
6
|
import { makeAutoObservable } from 'mobx'
|
|
7
|
-
import type {
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
8
|
import type { Resource } from './hub-types.ts'
|
|
9
9
|
import { buildPathTree, buildTypeTree, dirKeys, flattenTree, whenCounts, type TreeNode, type TreeRow } from './tree-build.ts'
|
|
10
10
|
|
|
@@ -37,7 +37,7 @@ export class TreeSvc {
|
|
|
37
37
|
/** the row key under the keyboard cursor */
|
|
38
38
|
cursor: string | null = null
|
|
39
39
|
|
|
40
|
-
constructor(private st:
|
|
40
|
+
constructor(private st: RepoSt) {
|
|
41
41
|
this.tab = readStore(TAB_KEY) === 'type' ? 'type' : 'path'
|
|
42
42
|
for (const p of readStore(EXPANDED_KEY).split('\n')) if (p !== '') this.expanded.add(p)
|
|
43
43
|
makeAutoObservable<this, 'st'>(this, { st: false })
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// time, embedded from its own /graph/<id> route. A tab is an item of the
|
|
3
3
|
// wikis collection, so the open wiki is in the url like any other page.
|
|
4
4
|
import { observer } from 'mobx-react-lite'
|
|
5
|
-
import type {
|
|
5
|
+
import type { RepoSt } from './repo-st.ts'
|
|
6
6
|
import type { Resource } from './hub-types.ts'
|
|
7
7
|
|
|
8
8
|
/** the collection id, and the address segment of every wiki */
|
|
@@ -11,10 +11,10 @@ export const WIKIS = 'wikis'
|
|
|
11
11
|
/** `state` colours the tab label the way the tree colours the wiki's row */
|
|
12
12
|
export type WikiTab = { id: string; label: string; state: Resource['state']; on: boolean; go: () => void }
|
|
13
13
|
|
|
14
|
-
const wikisOf = (app:
|
|
14
|
+
const wikisOf = (app: RepoSt): Resource[] => app.cols.resources.filter((r) => r.collection === WIKIS)
|
|
15
15
|
|
|
16
16
|
/** the tabs over the page, in the order the server mounted the wikis */
|
|
17
|
-
export function wikiTabs(app:
|
|
17
|
+
export function wikiTabs(app: RepoSt): WikiTab[] {
|
|
18
18
|
const sel = app.sel
|
|
19
19
|
return wikisOf(app).map((r) => ({
|
|
20
20
|
id: r.id,
|
|
@@ -27,13 +27,13 @@ export function wikiTabs(app: HubSt): WikiTab[] {
|
|
|
27
27
|
|
|
28
28
|
/** the header link: the first wiki, or the bare section while the first
|
|
29
29
|
* poll has not listed any */
|
|
30
|
-
export function openWikis(app:
|
|
30
|
+
export function openWikis(app: RepoSt): void {
|
|
31
31
|
const first = wikisOf(app)[0]
|
|
32
32
|
if (first != null) app.cols.select(WIKIS, first.id)
|
|
33
33
|
else app.showCol(WIKIS)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export const WikisView = observer(({ app, id }: { app:
|
|
36
|
+
export const WikisView = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
37
37
|
const i = app.cols.find(WIKIS, id)
|
|
38
38
|
const tabs = (
|
|
39
39
|
<span className="seg">
|
|
@@ -47,7 +47,11 @@ export const WikisView = observer(({ app, id }: { app: HubSt; id: string }) => {
|
|
|
47
47
|
{tabs}
|
|
48
48
|
<span className={`gvtext tl ${i.state}`}>{i.text}</span>
|
|
49
49
|
</div>
|
|
50
|
-
{i.href == null ? <div className="ph">this wiki carries no page to embed</div> :
|
|
50
|
+
{i.href == null ? <div className="ph">this wiki carries no page to embed</div> : (
|
|
51
|
+
// the spinner sits under the frame: the graph page paints its
|
|
52
|
+
// own background over it the moment it has something to show
|
|
53
|
+
<div className="gvframe"><span className="gvwait"><span className="spin" />reading the wiki…</span><iframe src={app.url(i.href)} title={i.title} /></div>
|
|
54
|
+
)}
|
|
51
55
|
</div>
|
|
52
56
|
)
|
|
53
57
|
})
|