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.
Files changed (137) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/package.json +3 -1
  3. package/src/check.ts +24 -0
  4. package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
  5. package/src/checks/SK023-stores-under-cap.check.ts +49 -0
  6. package/src/checks/SK024-identity-colors.check.ts +49 -0
  7. package/src/cli/cmd_Check.ts +26 -1
  8. package/src/cli/cmd_Ci.ts +18 -5
  9. package/src/cli/cmd_Home.ts +170 -0
  10. package/src/cli.ts +22 -2
  11. package/src/comfy/comfy-panel.ts +1 -16
  12. package/src/core/attach.ts +14 -9
  13. package/src/core/check-file-names.ts +8 -0
  14. package/src/core/check-runner.ts +40 -18
  15. package/src/core/discover-checks.ts +36 -18
  16. package/src/core/identity-color.ts +203 -0
  17. package/src/core/identity.ts +91 -0
  18. package/src/core/job-follow.ts +102 -0
  19. package/src/core/job-run.ts +27 -0
  20. package/src/core/jobs.ts +87 -27
  21. package/src/core/legacy-shapes.ts +1 -14
  22. package/src/core/machine-db.ts +83 -0
  23. package/src/core/one-lib.ts +85 -0
  24. package/src/core/port-holders.ts +43 -0
  25. package/src/core/retention.ts +213 -0
  26. package/src/core/run-check.ts +6 -2
  27. package/src/core/run-ci.ts +3 -0
  28. package/src/core/slots.ts +5 -0
  29. package/src/core/statement-end.ts +17 -0
  30. package/src/home/attach.ts +95 -0
  31. package/src/home/autostart.ts +108 -0
  32. package/src/home/global-units.ts +86 -0
  33. package/src/home/gui-app/ShipkitGui.swift +159 -0
  34. package/src/home/gui-app.ts +99 -0
  35. package/src/home/handoff.ts +51 -0
  36. package/src/home/home-launchd.ts +104 -0
  37. package/src/home/home-paths.ts +88 -0
  38. package/src/home/home-routes.ts +208 -0
  39. package/src/home/home-start.ts +105 -0
  40. package/src/home/home-state.ts +43 -0
  41. package/src/home/home-words.ts +12 -0
  42. package/src/home/hubs.ts +286 -0
  43. package/src/home/lib-watch.ts +139 -0
  44. package/src/home/machine-stores.ts +115 -0
  45. package/src/home/open-app.ts +16 -0
  46. package/src/home/pm-config.ts +37 -0
  47. package/src/home/pm.ts +141 -0
  48. package/src/home/resources.ts +338 -0
  49. package/src/home/run-daemon.ts +137 -0
  50. package/src/home/run-pm.ts +149 -0
  51. package/src/home/run-unit-action.ts +25 -0
  52. package/src/home/run-web.ts +37 -0
  53. package/src/home/serve-daemon.ts +31 -0
  54. package/src/home/serve-web.ts +172 -0
  55. package/src/home/supervise.ts +346 -0
  56. package/src/home/via-hub.ts +142 -0
  57. package/src/home/widgets.ts +102 -0
  58. package/src/ledger/check-stats.ts +19 -45
  59. package/src/ledger/db.ts +69 -17
  60. package/src/ledger/gate-checks.ts +5 -0
  61. package/src/ledger/git-scan.ts +31 -7
  62. package/src/ledger/ledger.ts +40 -14
  63. package/src/ledger/run-log.ts +58 -23
  64. package/src/ledger/store.ts +228 -103
  65. package/src/ledger/world-cache.ts +15 -13
  66. package/src/plan/cadence.ts +237 -0
  67. package/src/plan/planner.ts +303 -0
  68. package/src/plan/repo-source.ts +100 -0
  69. package/src/problems/problem-line.ts +1 -1
  70. package/src/repo-config.ts +51 -2
  71. package/src/serve/hub-app/check-view.tsx +9 -9
  72. package/src/serve/hub-app/check-words.ts +1 -1
  73. package/src/serve/hub-app/checks-svc.ts +12 -8
  74. package/src/serve/hub-app/col-card.tsx +4 -4
  75. package/src/serve/hub-app/cols-svc.ts +92 -13
  76. package/src/serve/hub-app/comfy-svc.ts +12 -6
  77. package/src/serve/hub-app/comfy-view.tsx +3 -3
  78. package/src/serve/hub-app/floating.tsx +49 -0
  79. package/src/serve/hub-app/geometry-view.tsx +4 -4
  80. package/src/serve/hub-app/hash-route.ts +1 -1
  81. package/src/serve/hub-app/header-bar.tsx +44 -12
  82. package/src/serve/hub-app/health-svc.ts +3 -3
  83. package/src/serve/hub-app/help.tsx +10 -7
  84. package/src/serve/hub-app/home-st.ts +241 -0
  85. package/src/serve/hub-app/home-strip.css +70 -0
  86. package/src/serve/hub-app/home-strip.tsx +245 -0
  87. package/src/serve/hub-app/home.html +17 -0
  88. package/src/serve/hub-app/hub-app.tsx +201 -93
  89. package/src/serve/hub-app/hub-types.ts +0 -3
  90. package/src/serve/hub-app/hub.css +43 -23
  91. package/src/serve/hub-app/icons.tsx +14 -0
  92. package/src/serve/hub-app/index.html +3 -1
  93. package/src/serve/hub-app/keys.ts +25 -0
  94. package/src/serve/hub-app/layout-st.ts +55 -0
  95. package/src/serve/hub-app/live-svc.ts +12 -9
  96. package/src/serve/hub-app/machine-st.ts +270 -0
  97. package/src/serve/hub-app/media-svc.ts +63 -44
  98. package/src/serve/hub-app/media-view.tsx +20 -20
  99. package/src/serve/hub-app/next-run.ts +24 -30
  100. package/src/serve/hub-app/palette-st.ts +88 -0
  101. package/src/serve/hub-app/planned-st.ts +171 -0
  102. package/src/serve/hub-app/planned-view.tsx +109 -0
  103. package/src/serve/hub-app/planned.css +39 -0
  104. package/src/serve/hub-app/processes-svc.ts +63 -0
  105. package/src/serve/hub-app/project-list.css +97 -0
  106. package/src/serve/hub-app/project-list.tsx +167 -0
  107. package/src/serve/hub-app/projects-svc.ts +139 -17
  108. package/src/serve/hub-app/prose-hits-st.ts +3 -3
  109. package/src/serve/hub-app/prose-hits-view.tsx +6 -6
  110. package/src/serve/hub-app/{hub-st.ts → repo-st.ts} +155 -84
  111. package/src/serve/hub-app/runs-tabs.ts +1 -1
  112. package/src/serve/hub-app/screen-cache.ts +74 -0
  113. package/src/serve/hub-app/test-view.tsx +4 -4
  114. package/src/serve/hub-app/tests-svc.ts +3 -3
  115. package/src/serve/hub-app/tests-view.tsx +10 -10
  116. package/src/serve/hub-app/timeline-chart.tsx +24 -12
  117. package/src/serve/hub-app/timeline-svc.ts +5 -4
  118. package/src/serve/hub-app/timeline-view.tsx +13 -13
  119. package/src/serve/hub-app/tree-row.tsx +2 -2
  120. package/src/serve/hub-app/tree-svc.ts +2 -2
  121. package/src/serve/hub-app/wikis-view.tsx +10 -6
  122. package/src/serve/hub-runtime.ts +542 -0
  123. package/src/serve/project-gutter-route.ts +21 -0
  124. package/src/serve/project-gutter.ts +3 -2
  125. package/src/serve/repo-processes.ts +89 -0
  126. package/src/serve/run-serve.ts +110 -402
  127. package/src/serve/serve-hub.ts +142 -44
  128. package/src/serve/stale-restart.ts +5 -0
  129. package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
  130. package/src/serve/warm-cell.ts +95 -0
  131. package/src/shipkit-files.ts +114 -0
  132. package/src/shipkit.ts +2 -1
  133. package/src/suite/kept-timings.ts +1 -1
  134. package/src/wiki/graph-positions.ts +1 -1
  135. package/src/serve/health-schedule.ts +0 -102
  136. package/src/serve/hub-app/project-gutter.css +0 -65
  137. 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 { HubSt } from './hub-st.ts'
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: HubSt; r: TestRun; slow: boolean }) => (
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: HubSt; s: SuiteFacts }) => {
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: HubSt; id: string }) => {
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 { HubSt } from './hub-st.ts'
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: HubSt) {
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 { HubSt } from './hub-st.ts'
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: HubSt; file: string; name: string }) => (
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: HubSt; rows: FailingFile[]; showCheck: boolean }) => (
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: HubSt; l: LastRun }) => (
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: HubSt; files: FileTime[] }) => (
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: HubSt; check: string | null }) => {
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: HubSt; check: string }) => {
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: HubSt; rows: TestCheckRow[] }) => (
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: HubSt }) => {
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: HubSt; check: string | null }) =>
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: 'daily pass', save: 'save trigger', unknown: 'before the record' }
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: the tooltip is fixed there, out of every box
39
- * of the strip that clips */
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
- <div className="tlaxtip" style={tipStyle(t, globalThis.innerWidth ?? Number.POSITIVE_INFINITY)}>
64
- <b className={`tl ${t.tone}`}>{t.head}</b>
65
- {t.lines.map((l) => <span key={l}>{l}</span>)}
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 { HubSt } from './hub-st.ts'
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: HubSt) {
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
- async load(): Promise<void> {
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 { HubSt } from './hub-st.ts'
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: 'daily', save: 'save', unknown: 'unknown' }
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: HubSt; b: TimelineBar; name: ChildName }) => (
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: HubSt; inv: TimelineInvocation; n: number }) => {
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: HubSt; bars: TimelineBar[] }) => {
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: HubSt; rows: TimelineInvocation[] }) => (
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: HubSt; rows: SuiteToFix[]; recorded: boolean }) => {
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: HubSt; rows: SlowCheck[] }) => {
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: HubSt; rows: InvocationSummary[] }) => {
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: HubSt; on: RunsTab }) => {
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: HubSt }) => {
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">reading the run record…</div> : (
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 { HubSt } from './hub-st.ts'
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: HubSt; row: TreeRow }) => {
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 { HubSt } from './hub-st.ts'
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: HubSt) {
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 { HubSt } from './hub-st.ts'
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: HubSt): Resource[] => app.cols.resources.filter((r) => r.collection === WIKIS)
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: HubSt): WikiTab[] {
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: HubSt): void {
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: HubSt; id: string }) => {
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> : <iframe src={i.href} title={i.title} />}
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
  })