rvlib-shipkit 0.38.0 → 1.0.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 +371 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +216 -18
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
//
|
|
2
|
-
// the act-in-place verbs.
|
|
3
|
-
//
|
|
1
|
+
// Resources service — ONE flat list for the whole repo (/api/resources.json)
|
|
2
|
+
// plus the act-in-place verbs. The collections are a GROUPING of that array,
|
|
3
|
+
// computed here, never a second fetch. Every act hits a git-tracked truth
|
|
4
|
+
// file through /api/act/*; errors come back as 400 text shown verbatim on
|
|
5
|
+
// the card.
|
|
4
6
|
import { makeAutoObservable, runInAction } from 'mobx'
|
|
5
7
|
import type { HubSt } from './hub-st.ts'
|
|
6
|
-
import type { Col,
|
|
8
|
+
import type { Col, ColMeta, Resource, Schedule } from './hub-types.ts'
|
|
7
9
|
|
|
8
10
|
export class ColsSvc {
|
|
9
|
-
|
|
11
|
+
/** exactly what the server last sent; `resources` is this plus the live
|
|
12
|
+
* results of a run in flight */
|
|
13
|
+
fetched: Resource[] = []
|
|
14
|
+
colMeta: ColMeta[] = []
|
|
15
|
+
/** the hub's daily health pass timer, as the last poll saw it */
|
|
16
|
+
schedule: Schedule | null = null
|
|
10
17
|
actErr = ''
|
|
11
18
|
remarkDraft = ''
|
|
12
19
|
proofDraft = ''
|
|
@@ -28,17 +35,37 @@ export class ColsSvc {
|
|
|
28
35
|
|
|
29
36
|
async poll(): Promise<void> {
|
|
30
37
|
try {
|
|
31
|
-
const r = await fetch('/api/
|
|
38
|
+
const r = await fetch('/api/resources.json')
|
|
32
39
|
if (!r.ok) throw new Error(await r.text())
|
|
33
|
-
const j = (await r.json()) as { collections:
|
|
34
|
-
runInAction(() => {
|
|
40
|
+
const j = (await r.json()) as { collections: ColMeta[]; resources: Resource[]; schedule?: Schedule | null }
|
|
41
|
+
runInAction(() => {
|
|
42
|
+
this.colMeta = j.collections
|
|
43
|
+
this.fetched = j.resources
|
|
44
|
+
this.schedule = j.schedule ?? null
|
|
45
|
+
})
|
|
35
46
|
} catch (e) {
|
|
47
|
+
// a dead server must SAY so on the tree, never render an empty
|
|
48
|
+
// repo that reads like a healthy one
|
|
49
|
+
const text = `resources fetch broke: ${(e as Error).message}`
|
|
36
50
|
runInAction(() => {
|
|
37
|
-
this.
|
|
51
|
+
this.colMeta = [{ id: 'hub', title: 'hub', media: 'health' }]
|
|
52
|
+
this.fetched = [{ id: 'hub:resources-broken', collection: 'hub', type: 'health', path: 'health/hub-fetch', virtual: true, title: text, text, state: 'red', severity: 'red' }]
|
|
38
53
|
})
|
|
39
54
|
}
|
|
40
55
|
}
|
|
41
56
|
|
|
57
|
+
/** the one array, with whatever a RUNNING gate has already reported laid
|
|
58
|
+
* over it: a row recolours the moment its check finishes, rather than on
|
|
59
|
+
* the next ten second poll */
|
|
60
|
+
get resources(): Resource[] {
|
|
61
|
+
return this.st.checks.overlay(this.fetched)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** the collections, as a grouping of the one array */
|
|
65
|
+
get cols(): Col[] {
|
|
66
|
+
return this.colMeta.map((m) => ({ ...m, instances: this.resources.filter((r) => r.collection === m.id) }))
|
|
67
|
+
}
|
|
68
|
+
|
|
42
69
|
/** media instances render richer through the media sidebar — the
|
|
43
70
|
* collection twin would be a duplicate surface (kept in `cols` so the
|
|
44
71
|
* asset card can show its remarks) */
|
|
@@ -46,10 +73,10 @@ export class ColsSvc {
|
|
|
46
73
|
return this.cols.filter((c) => c.id !== 'media')
|
|
47
74
|
}
|
|
48
75
|
mediaRemarks(assetId: string): string[] {
|
|
49
|
-
return this.
|
|
76
|
+
return this.resources.find((i) => i.id === `media:${assetId}`)?.remarks ?? []
|
|
50
77
|
}
|
|
51
78
|
|
|
52
|
-
needsItem = (i:
|
|
79
|
+
needsItem = (i: Resource): boolean => i.severity != null
|
|
53
80
|
isOpen(c: Col): boolean {
|
|
54
81
|
// a live search must SHOW its matches — folds yield while it runs
|
|
55
82
|
if (this.st.query.trim() !== '') return true
|
|
@@ -69,9 +96,9 @@ export class ColsSvc {
|
|
|
69
96
|
return c.instances.some((i) => i.severity === 'red')
|
|
70
97
|
}
|
|
71
98
|
get mediaHasRed(): boolean {
|
|
72
|
-
return this.
|
|
99
|
+
return this.resources.some((i) => i.collection === 'media' && i.severity === 'red')
|
|
73
100
|
}
|
|
74
|
-
visibleInstances(c: Col):
|
|
101
|
+
visibleInstances(c: Col): Resource[] {
|
|
75
102
|
const f = this.st.filter
|
|
76
103
|
const base = f === 'all' ? c.instances
|
|
77
104
|
: c.instances.filter((i) => (f === 'red' ? i.severity === 'red' : this.needsItem(i)))
|
|
@@ -80,17 +107,14 @@ export class ColsSvc {
|
|
|
80
107
|
return base.filter((i) => i.text.toLowerCase().includes(q) || (i.file ?? '').toLowerCase().includes(q))
|
|
81
108
|
}
|
|
82
109
|
get redCount(): number {
|
|
83
|
-
return this.
|
|
84
|
-
}
|
|
85
|
-
find(colId: string, id: string): ColInstance | null {
|
|
86
|
-
return this.cols.find((c) => c.id === colId)?.instances.find((i) => i.id === id) ?? null
|
|
110
|
+
return this.resources.filter((i) => i.severity === 'red').length
|
|
87
111
|
}
|
|
88
|
-
|
|
89
|
-
return this.
|
|
112
|
+
find(colId: string, id: string): Resource | null {
|
|
113
|
+
return this.resources.find((i) => i.collection === colId && i.id === id) ?? null
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
select(colId: string, id: string): void {
|
|
93
|
-
this.st.
|
|
117
|
+
this.st.setSel({ t: 'item', col: colId, id })
|
|
94
118
|
this.actErr = ''
|
|
95
119
|
this.remarkDraft = ''
|
|
96
120
|
this.proofDraft = ''
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Comfy service — starts the repo's comfy-ts panel on demand and speaks the
|
|
2
|
+
// documented host protocol to the frame (comfy-ts src/cli/serve/hostProtocol.ts).
|
|
3
|
+
// The panel is CROSS-ORIGIN (its own port), so everything the hub knows about
|
|
4
|
+
// it is something the panel SAID: the header never reports a state it guessed
|
|
5
|
+
// from how the iframe looks.
|
|
6
|
+
import { makeAutoObservable, runInAction } from 'mobx'
|
|
7
|
+
import type { HubSt } from './hub-st.ts'
|
|
8
|
+
|
|
9
|
+
export type ComfyBox = { url: string; up: boolean; err: string | null }
|
|
10
|
+
/** what the panel says, narrowed to the messages this host acts on */
|
|
11
|
+
type PanelMsg = { comfyTs?: unknown; module?: unknown; draft?: unknown; code?: unknown; message?: unknown }
|
|
12
|
+
|
|
13
|
+
/** `ready` never arriving is not the same as a dead panel: an older comfy-ts
|
|
14
|
+
* serves a working ui and speaks no protocol, so the chip says which it is */
|
|
15
|
+
const HANDSHAKE_MS = 10_000
|
|
16
|
+
|
|
17
|
+
export class ComfySvc {
|
|
18
|
+
/** the iframe src, set ONCE per hub session: re-pointing it remounts the
|
|
19
|
+
* panel and throws away its gallery, so a workflow switch is a message */
|
|
20
|
+
mountUrl = ''
|
|
21
|
+
/** bumped on a restart: it is the iframe's react key, so a fresh panel
|
|
22
|
+
* process gets a fresh frame instead of one pointing at a dead port */
|
|
23
|
+
mountNonce = 0
|
|
24
|
+
state: 'idle' | 'starting' | 'live' | 'silent' | 'dead' = 'idle'
|
|
25
|
+
err = ''
|
|
26
|
+
box: ComfyBox | null = null
|
|
27
|
+
/** the selection as the PANEL reports it — the hub follows, never the reverse */
|
|
28
|
+
live: { module: string; draft: string } | null = null
|
|
29
|
+
/** the panel over the ASSET page, so tuning starts where the asset already
|
|
30
|
+
* is instead of sending him to another section to find the same frame */
|
|
31
|
+
overlay = false
|
|
32
|
+
/** a draft asked for before the panel answered: the deep link carries the
|
|
33
|
+
* workflow only, so the draft rides a message once the handshake lands */
|
|
34
|
+
private wantDraft = ''
|
|
35
|
+
/** the workflow the mount url carries — the panel opens on it by deep link,
|
|
36
|
+
* so asking for it again by message would reset its draft for nothing */
|
|
37
|
+
private mountedFor = ''
|
|
38
|
+
private frame: HTMLIFrameElement | null = null
|
|
39
|
+
private handshake: ReturnType<typeof setTimeout> | null = null
|
|
40
|
+
|
|
41
|
+
constructor(private st: HubSt) {
|
|
42
|
+
makeAutoObservable<this, 'st' | 'frame' | 'handshake'>(this, { st: false, frame: false, handshake: false })
|
|
43
|
+
window.addEventListener('message', (e) => this.onMessage(e))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setFrame(el: HTMLIFrameElement | null): void {
|
|
47
|
+
this.frame = el
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** the comfy row's ONE action: mount the panel, or move the mounted one */
|
|
51
|
+
async open(id: string): Promise<void> {
|
|
52
|
+
if (this.mountUrl === '') return await this.start(id)
|
|
53
|
+
if (id !== (this.live?.module ?? this.mountedFor)) this.post({ comfyTs: 'set-selection', module: id, draft: 'default' })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** an asset page's own action: tune THIS asset's workflow without leaving
|
|
57
|
+
* the asset. The draft is the KIND's, one file shared by every asset of
|
|
58
|
+
* that kind, and the asset's own prompt and seed land on top of it at run
|
|
59
|
+
* time */
|
|
60
|
+
async tune(module: string, draft: string): Promise<void> {
|
|
61
|
+
runInAction(() => {
|
|
62
|
+
this.overlay = true
|
|
63
|
+
this.wantDraft = draft
|
|
64
|
+
})
|
|
65
|
+
if (this.mountUrl === '') return await this.start(module)
|
|
66
|
+
this.selectWanted(module)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
closeOverlay(): void {
|
|
70
|
+
this.overlay = false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** the panel is live already, so the move is a message and the gallery stays */
|
|
74
|
+
private selectWanted(module: string): void {
|
|
75
|
+
const draft = this.wantDraft === '' ? 'default' : this.wantDraft
|
|
76
|
+
if (this.live?.module === module && this.live.draft === draft) return
|
|
77
|
+
this.post({ comfyTs: 'set-selection', module, draft })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async probeBox(): Promise<void> {
|
|
81
|
+
const box = await fetch('/api/comfy/box')
|
|
82
|
+
.then((r) => (r.ok ? (r.json() as Promise<ComfyBox>) : null))
|
|
83
|
+
.catch(() => null)
|
|
84
|
+
runInAction(() => {
|
|
85
|
+
this.box = box
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** an edited .cflow.ts module reaches the panel only through a new process:
|
|
90
|
+
* comfy-ts serve imports every module once, at boot */
|
|
91
|
+
async restart(): Promise<void> {
|
|
92
|
+
const id = this.live?.module ?? this.mountedFor
|
|
93
|
+
if (id === '') return
|
|
94
|
+
runInAction(() => {
|
|
95
|
+
this.live = null
|
|
96
|
+
this.mountUrl = ''
|
|
97
|
+
this.mountNonce++
|
|
98
|
+
})
|
|
99
|
+
await this.start(id, true)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async start(id: string, restart = false): Promise<void> {
|
|
103
|
+
if (this.state === 'starting' && !restart) return
|
|
104
|
+
runInAction(() => {
|
|
105
|
+
this.state = 'starting'
|
|
106
|
+
this.err = ''
|
|
107
|
+
})
|
|
108
|
+
void this.probeBox()
|
|
109
|
+
try {
|
|
110
|
+
const r = await fetch('/api/comfy/panel', { method: 'POST', body: JSON.stringify({ restart }) })
|
|
111
|
+
if (!r.ok) throw new Error(await r.text())
|
|
112
|
+
const up = (await r.json()) as { url: string }
|
|
113
|
+
runInAction(() => {
|
|
114
|
+
this.mountedFor = id
|
|
115
|
+
this.mountUrl = `${up.url}/?workflow=${encodeURIComponent(id)}`
|
|
116
|
+
})
|
|
117
|
+
this.armHandshake()
|
|
118
|
+
} catch (e) {
|
|
119
|
+
runInAction(() => {
|
|
120
|
+
this.state = 'dead'
|
|
121
|
+
this.err = (e as Error).message
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private armHandshake(): void {
|
|
127
|
+
if (this.handshake != null) clearTimeout(this.handshake)
|
|
128
|
+
this.handshake = setTimeout(
|
|
129
|
+
() =>
|
|
130
|
+
runInAction(() => {
|
|
131
|
+
if (this.state === 'starting') this.state = 'silent'
|
|
132
|
+
}),
|
|
133
|
+
HANDSHAKE_MS,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private post(msg: Record<string, unknown>): void {
|
|
138
|
+
const w = this.frame?.contentWindow
|
|
139
|
+
if (w == null || this.mountUrl === '') return
|
|
140
|
+
w.postMessage(msg, new URL(this.mountUrl).origin)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private onMessage(e: MessageEvent): void {
|
|
144
|
+
// the frame we mounted and nobody else: another embed on this page
|
|
145
|
+
// speaking first must never drive this header
|
|
146
|
+
if (this.frame == null || e.source !== this.frame.contentWindow) return
|
|
147
|
+
const m = e.data as PanelMsg | null
|
|
148
|
+
if (m == null || typeof m.comfyTs !== 'string') return
|
|
149
|
+
runInAction(() => {
|
|
150
|
+
if (m.comfyTs === 'ready') {
|
|
151
|
+
this.state = 'live'
|
|
152
|
+
this.err = ''
|
|
153
|
+
if (this.wantDraft !== '') this.selectWanted(this.mountedFor)
|
|
154
|
+
} else if (m.comfyTs === 'error' && m.code === 'boot') {
|
|
155
|
+
this.state = 'dead'
|
|
156
|
+
this.err = typeof m.message === 'string' ? m.message : 'the panel could not boot'
|
|
157
|
+
} else if (m.comfyTs === 'selection' && typeof m.module === 'string' && typeof m.draft === 'string') {
|
|
158
|
+
this.state = 'live'
|
|
159
|
+
this.live = { module: m.module, draft: m.draft }
|
|
160
|
+
if (m.draft === this.wantDraft) this.wantDraft = ''
|
|
161
|
+
// over an asset page the panel is a popup ON that asset: following
|
|
162
|
+
// its selection here would navigate off the asset it was opened from
|
|
163
|
+
if (this.overlay) return
|
|
164
|
+
// a switch made INSIDE the panel moves the hub's selected row,
|
|
165
|
+
// so the two never show different workflows
|
|
166
|
+
const id = `comfy:${m.module}`
|
|
167
|
+
if (this.st.sel?.t !== 'item' || this.st.sel.id !== id) this.st.cols.select('comfy', id)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// comfy item view — comfy-ts's own panel fills the stage, the header states
|
|
2
|
+
// what the hub cannot see from the outside: whether the panel handshook, and
|
|
3
|
+
// whether the box a run needs is answering.
|
|
4
|
+
import { observer } from 'mobx-react-lite'
|
|
5
|
+
import { useEffect } from 'react'
|
|
6
|
+
import type { ComfySvc } from './comfy-svc.ts'
|
|
7
|
+
import type { HubSt } from './hub-st.ts'
|
|
8
|
+
|
|
9
|
+
const PANEL_CHIP: Record<ComfySvc['state'], { label: string; cls: string; title: string }> = {
|
|
10
|
+
idle: { label: 'idle', cls: '', title: 'nothing started yet' },
|
|
11
|
+
starting: { label: 'starting', cls: '', title: 'the hub is starting comfy-ts serve' },
|
|
12
|
+
live: { label: 'live', cls: 'ok', title: 'the panel handshook over the host protocol' },
|
|
13
|
+
silent: { label: 'no handshake', cls: '', title: 'the panel never sent ready — it still works, this hub just cannot follow its selection' },
|
|
14
|
+
dead: { label: 'failed', cls: 'miss', title: 'the panel could not start' },
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// the same panel over an asset page. A popup, so the asset, its rolls and its
|
|
18
|
+
// prompt stay on screen behind it while a value is tried
|
|
19
|
+
export const ComfyOverlay = observer(({ app }: { app: HubSt }) => {
|
|
20
|
+
const c = app.comfy
|
|
21
|
+
if (!c.overlay) return null
|
|
22
|
+
const chip = PANEL_CHIP[c.state]
|
|
23
|
+
const box = c.box
|
|
24
|
+
return (
|
|
25
|
+
<div className="cfov" onClick={() => c.closeOverlay()}>
|
|
26
|
+
<div className="cfbox" onClick={(e) => e.stopPropagation()}>
|
|
27
|
+
<div className="gvhead">
|
|
28
|
+
<span className="gvtext">{c.live?.module ?? ''}{c.live != null ? ` · ${c.live.draft}` : ''}</span>
|
|
29
|
+
<span className={`stchip ${chip.cls}`} title={chip.title}>{chip.label}</span>
|
|
30
|
+
{box != null ? (
|
|
31
|
+
<span className={`stchip ${box.up ? 'ok' : 'miss'}`} title={box.err ?? box.url}>
|
|
32
|
+
{box.up ? 'box up' : `box unreachable — ${box.url}`}
|
|
33
|
+
</span>
|
|
34
|
+
) : null}
|
|
35
|
+
<button className="ghost" title="restart the panel process — the only way an edited .cflow.ts module reaches it"
|
|
36
|
+
onClick={() => void c.restart()}>restart panel</button>
|
|
37
|
+
<button className="ghost" onClick={() => c.closeOverlay()}>close</button>
|
|
38
|
+
</div>
|
|
39
|
+
{c.err !== '' ? <div className="err">✗ {c.err}</div> : null}
|
|
40
|
+
{c.mountUrl !== '' ? (
|
|
41
|
+
<iframe key={c.mountNonce} ref={(el) => c.setFrame(el)} src={c.mountUrl} title="comfy-ts panel" />
|
|
42
|
+
) : (
|
|
43
|
+
<div className="ph">{c.state === 'dead' ? 'the panel did not start' : 'starting the comfy-ts panel…'}</div>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
export const ComfyView = observer(({ app, id }: { app: HubSt; id: string }) => {
|
|
51
|
+
const c = app.comfy
|
|
52
|
+
const wf = id.replace(/^comfy:/, '')
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
void c.open(wf)
|
|
55
|
+
}, [c, wf])
|
|
56
|
+
const chip = PANEL_CHIP[c.state]
|
|
57
|
+
const box = c.box
|
|
58
|
+
return (
|
|
59
|
+
<div className="comfyview">
|
|
60
|
+
<div className="gvhead">
|
|
61
|
+
<span className="gvtext">{c.live?.module ?? wf}{c.live != null ? ` · ${c.live.draft}` : ''}</span>
|
|
62
|
+
<span className={`stchip ${chip.cls}`} title={chip.title}>{chip.label}</span>
|
|
63
|
+
{box != null ? (
|
|
64
|
+
<span className={`stchip ${box.up ? 'ok' : 'miss'}`} title={box.err ?? box.url}>
|
|
65
|
+
{box.up ? 'box up' : `box unreachable — ${box.url}`}
|
|
66
|
+
</span>
|
|
67
|
+
) : null}
|
|
68
|
+
<button className="ghost" title="re-probe the box" onClick={() => void c.probeBox()}>↻</button>
|
|
69
|
+
<button className="ghost" title="restart the panel process — the only way an edited .cflow.ts module reaches it"
|
|
70
|
+
onClick={() => void c.restart()}>restart panel</button>
|
|
71
|
+
</div>
|
|
72
|
+
{c.err !== '' ? <div className="err">✗ {c.err}</div> : null}
|
|
73
|
+
{box?.err != null ? <div className="err">✗ {box.err}</div> : null}
|
|
74
|
+
{c.mountUrl !== '' ? (
|
|
75
|
+
<iframe key={c.mountNonce} ref={(el) => c.setFrame(el)} src={c.mountUrl} title="comfy-ts panel" />
|
|
76
|
+
) : (
|
|
77
|
+
<div className="ph">{c.state === 'dead' ? 'the panel did not start' : 'starting the comfy-ts panel…'}</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ONE duration unit for the whole hub, and ONE tone table under it.
|
|
2
|
+
//
|
|
3
|
+
// Every number the page prints is milliseconds, grouped by a thin space, so a
|
|
4
|
+
// column of them compares by eye. Mixing `4.2s` into a column of `412ms` made
|
|
5
|
+
// the two biggest numbers on the page look like the two smallest.
|
|
6
|
+
//
|
|
7
|
+
// The tone is here rather than in a component because the tree row and the
|
|
8
|
+
// check page both colour the same number, and two tables would eventually say
|
|
9
|
+
// two things about one measurement.
|
|
10
|
+
|
|
11
|
+
/** the thousands separator: a thin space, so the digits stay a block */
|
|
12
|
+
export const THIN = ' '
|
|
13
|
+
|
|
14
|
+
/** green · plain ink · orange · red */
|
|
15
|
+
export type DurTone = 'fast' | 'plain' | 'slow' | 'bad'
|
|
16
|
+
|
|
17
|
+
const FAST_MS = 500
|
|
18
|
+
const PLAIN_MS = 2_000
|
|
19
|
+
const SLOW_MS = 10_000
|
|
20
|
+
|
|
21
|
+
/** past this share of its kill time a check is red whatever the table says:
|
|
22
|
+
* the next slow machine trips it */
|
|
23
|
+
const CEILING_SHARE = 0.5
|
|
24
|
+
|
|
25
|
+
const group = (n: number): string => {
|
|
26
|
+
const s = `${n}`
|
|
27
|
+
let out = ''
|
|
28
|
+
for (let i = 0; i < s.length; i++) out += (i > 0 && (s.length - i) % 3 === 0 ? THIN : '') + s[i]!
|
|
29
|
+
return out
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const fmtMs = (ms: number | null | undefined): string => (ms == null ? '' : `${group(Math.round(ms))}ms`)
|
|
33
|
+
|
|
34
|
+
export function durationTone(ms: number | null | undefined, ceilingMs?: number | null): DurTone | null {
|
|
35
|
+
if (ms == null) return null
|
|
36
|
+
if (ceilingMs != null && ceilingMs > 0 && ms > ceilingMs * CEILING_SHARE) return 'bad'
|
|
37
|
+
if (ms < FAST_MS) return 'fast'
|
|
38
|
+
if (ms < PLAIN_MS) return 'plain'
|
|
39
|
+
if (ms < SLOW_MS) return 'slow'
|
|
40
|
+
return 'bad'
|
|
41
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// THE ADDRESS of what is on screen, both ways, and nothing else. Pure on
|
|
2
|
+
// purpose: the page's selection rules are worth pinning, and HubSt cannot be
|
|
3
|
+
// instantiated without a browser.
|
|
4
|
+
//
|
|
5
|
+
// The media forms are older than the rest and stay byte for byte what they
|
|
6
|
+
// were, because they are pasted around as share links: `#<assetId>` with an
|
|
7
|
+
// optional `?tools=a,b`, and `#cat-<Category>`.
|
|
8
|
+
|
|
9
|
+
/** every selection the page can show. Each one has an address, so nothing
|
|
10
|
+
* that decides which page is on screen lives outside the url */
|
|
11
|
+
import { RUNS_ID } from '../../collections/collection-types.ts'
|
|
12
|
+
import { runsTab, runsTabId } from './runs-tabs.ts'
|
|
13
|
+
|
|
14
|
+
export type HashSel =
|
|
15
|
+
| { t: 'asset'; id: string }
|
|
16
|
+
| { t: 'cat'; cat: string }
|
|
17
|
+
| { t: 'col'; id: string }
|
|
18
|
+
| { t: 'item'; col: string; id: string }
|
|
19
|
+
| { t: 'status' }
|
|
20
|
+
/** where the test checks' time goes: every one of them (check null), or
|
|
21
|
+
* one test check (`test`, `test:<unit>`) */
|
|
22
|
+
| { t: 'tests'; check: string | null }
|
|
23
|
+
|
|
24
|
+
export type ParsedHash = {
|
|
25
|
+
kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'none'
|
|
26
|
+
/** the collection, for an item. null for everything else */
|
|
27
|
+
col: string | null
|
|
28
|
+
id: string
|
|
29
|
+
/** the media tool set, when the address carried one. An empty array is a
|
|
30
|
+
* choice (every tool off), null is no statement at all */
|
|
31
|
+
tools: string[] | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ITEM = 'item:'
|
|
35
|
+
const COL = 'col:'
|
|
36
|
+
const CAT = 'cat-'
|
|
37
|
+
/** prefixed, so no asset id can shadow it */
|
|
38
|
+
const STATUS = 'page:status'
|
|
39
|
+
const TESTS = 'page:tests'
|
|
40
|
+
|
|
41
|
+
/** what this selection puts in the url. null only for no selection at all,
|
|
42
|
+
* and then the hash is left alone rather than wiped */
|
|
43
|
+
export function hashFor(sel: HashSel | null): string | null {
|
|
44
|
+
if (sel == null) return null
|
|
45
|
+
switch (sel.t) {
|
|
46
|
+
case 'asset': return sel.id
|
|
47
|
+
case 'cat': return `${CAT}${sel.cat}`
|
|
48
|
+
case 'col': return `${COL}${sel.id}`
|
|
49
|
+
case 'item': return `${ITEM}${sel.col}:${sel.id}`
|
|
50
|
+
case 'status': return STATUS
|
|
51
|
+
case 'tests': return sel.check == null ? TESTS : `${TESTS}:${sel.check}`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseHash(raw: string): ParsedHash {
|
|
56
|
+
const none: ParsedHash = { kind: 'none', col: null, id: '', tools: null }
|
|
57
|
+
if (raw === '') return none
|
|
58
|
+
if (raw === STATUS) return { kind: 'status', col: null, id: '', tools: null }
|
|
59
|
+
if (raw === TESTS) return { kind: 'tests', col: null, id: '', tools: null }
|
|
60
|
+
// a check id carries colons of its own (`test:rvlib-shipkit`)
|
|
61
|
+
if (raw.startsWith(`${TESTS}:`)) {
|
|
62
|
+
const check = raw.slice(TESTS.length + 1)
|
|
63
|
+
return check === '' ? none : { kind: 'tests', col: null, id: check, tools: null }
|
|
64
|
+
}
|
|
65
|
+
if (raw.startsWith(ITEM)) {
|
|
66
|
+
const rest = raw.slice(ITEM.length)
|
|
67
|
+
const cut = rest.indexOf(':')
|
|
68
|
+
// an id carries colons of its own (`check:SK004`), so only the FIRST
|
|
69
|
+
// separator belongs to this format
|
|
70
|
+
if (cut <= 0) return none
|
|
71
|
+
const col = rest.slice(0, cut)
|
|
72
|
+
const id = rest.slice(cut + 1)
|
|
73
|
+
return id === '' ? none : { kind: 'item', col, id, tools: null }
|
|
74
|
+
}
|
|
75
|
+
if (raw.startsWith(COL)) {
|
|
76
|
+
const id = raw.slice(COL.length)
|
|
77
|
+
return id === '' ? none : { kind: 'col', col: null, id, tools: null }
|
|
78
|
+
}
|
|
79
|
+
if (raw.startsWith(CAT)) {
|
|
80
|
+
const cat = raw.slice(CAT.length)
|
|
81
|
+
return cat === '' ? none : { kind: 'cat', col: null, id: cat, tools: null }
|
|
82
|
+
}
|
|
83
|
+
const qi = raw.indexOf('?')
|
|
84
|
+
const id = qi === -1 ? raw : raw.slice(0, qi)
|
|
85
|
+
if (id === '') return none
|
|
86
|
+
const tools = qi === -1 ? null : new URLSearchParams(raw.slice(qi + 1)).get('tools')
|
|
87
|
+
return { kind: 'asset', col: null, id, tools: tools == null ? null : tools === '' ? [] : tools.split(',') }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** the selection an address names, the inverse of hashFor. The media tool
|
|
91
|
+
* set is not a selection: it rides in `ParsedHash.tools` */
|
|
92
|
+
export function selOf(p: ParsedHash): HashSel | null {
|
|
93
|
+
switch (p.kind) {
|
|
94
|
+
case 'none': return null
|
|
95
|
+
case 'asset': return { t: 'asset', id: p.id }
|
|
96
|
+
case 'cat': return { t: 'cat', cat: p.id }
|
|
97
|
+
case 'col': return { t: 'col', id: p.id }
|
|
98
|
+
case 'item': return { t: 'item', col: p.col!, id: p.id }
|
|
99
|
+
case 'status': return { t: 'status' }
|
|
100
|
+
case 'tests': return { t: 'tests', check: p.id === '' ? null : p.id }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** the one address of what a selection opens. A runs tab this build does
|
|
105
|
+
* not name opens the runs tab, so its address is the runs tab's: a stale or
|
|
106
|
+
* hand typed tab never stays in the url while another tab shows */
|
|
107
|
+
export function canonicalSel(sel: HashSel): HashSel {
|
|
108
|
+
if (sel.t !== 'item' || sel.col !== RUNS_ID) return sel
|
|
109
|
+
const id = runsTabId(runsTab(sel.id))
|
|
110
|
+
return id === sel.id ? sel : { ...sel, id }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** the hash as the page reads it: no `#`, percent escapes undone */
|
|
114
|
+
export function currentHash(): string {
|
|
115
|
+
const raw = location.hash.slice(1)
|
|
116
|
+
try {
|
|
117
|
+
return decodeURIComponent(raw)
|
|
118
|
+
} catch {
|
|
119
|
+
return raw
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// The header over every page, and the live strip under it. The header is the
|
|
2
|
+
// repo name, one link per core page, and on the right the two runs that are
|
|
3
|
+
// not one check with what is running now. The strip is the one run chart
|
|
4
|
+
// (timeline-chart.tsx) over a rolling window that ends now: folded, a thin
|
|
5
|
+
// bar of the last two minutes; unfolded, the full chart over ten, zoomable.
|
|
6
|
+
// Each fold has ONE height whatever runs, so a run never moves the page.
|
|
7
|
+
import { observer } from 'mobx-react-lite'
|
|
8
|
+
import { RUNS_ID } from '../../collections/collection-types.ts'
|
|
9
|
+
import { Help } from './help.tsx'
|
|
10
|
+
import { openWikis, WIKIS } from './wikis-view.tsx'
|
|
11
|
+
import type { HubSt } from './hub-st.ts'
|
|
12
|
+
import { byWords, TimelineChart } from './timeline-chart.tsx'
|
|
13
|
+
|
|
14
|
+
export type CorePage = { id: string; label: string; on: boolean; go: () => void }
|
|
15
|
+
|
|
16
|
+
/** the pages reached for most, in the order they are reached for. A page this
|
|
17
|
+
* repo does not have is not a link */
|
|
18
|
+
export function corePages(app: HubSt): CorePage[] {
|
|
19
|
+
const has = (col: string): boolean => app.cols.colMeta.some((c) => c.id === col)
|
|
20
|
+
const sel = app.sel
|
|
21
|
+
const onCol = (col: string): boolean => (sel?.t === 'col' && sel.id === col) || (sel?.t === 'item' && sel.col === col)
|
|
22
|
+
const out: CorePage[] = []
|
|
23
|
+
if (has('checks')) out.push({ id: 'checks', label: 'checks', on: onCol('checks'), go: () => app.showCol('checks') })
|
|
24
|
+
// a page, never a resource: every hub keeps a run record, so it is
|
|
25
|
+
// always here
|
|
26
|
+
out.push({ id: 'runs', label: 'runs', on: onCol('runs'), go: () => app.cols.select('runs', RUNS_ID) })
|
|
27
|
+
if (has('asks')) out.push({ id: 'asks', label: 'asks', on: onCol('asks'), go: () => app.showCol('asks') })
|
|
28
|
+
const firstCat = app.media.assets[0]?.cat
|
|
29
|
+
if (firstCat != null) out.push({ id: 'media', label: 'media', on: sel?.t === 'asset' || sel?.t === 'cat', go: () => app.showCat(firstCat) })
|
|
30
|
+
if (has(WIKIS)) out.push({ id: WIKIS, label: 'wikis', on: onCol(WIKIS), go: () => openWikis(app) })
|
|
31
|
+
return out
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** what is running, in one line: the hub's own run first, since it says the
|
|
35
|
+
* most, else the newest run the record has in flight */
|
|
36
|
+
const RunState = observer(({ app }: { app: HubSt }) => {
|
|
37
|
+
const own = app.checks.strip
|
|
38
|
+
const run = app.live.view?.runs[0]
|
|
39
|
+
const text = own !== '' ? own : run != null ? `${run.label} · ${run.done} of ${run.total}${run.reds > 0 ? ` · ${run.reds} red so far` : ''}` : 'idle'
|
|
40
|
+
const going = app.checks.busy || run != null
|
|
41
|
+
return (
|
|
42
|
+
<span className={`hstate${going ? ' on' : ''}`} title={text}>
|
|
43
|
+
{going ? <span className="spin" /> : null}
|
|
44
|
+
<span className="htext">{text}</span>
|
|
45
|
+
</span>
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const HeaderBar = observer(({ app }: { app: HubSt }) => {
|
|
50
|
+
const u = app.checks
|
|
51
|
+
return (
|
|
52
|
+
<header className="hdr">
|
|
53
|
+
<span className="hname">{app.name !== '' ? app.name : 'hub'}</span>
|
|
54
|
+
<nav className="hnav">
|
|
55
|
+
{corePages(app).map((p) => (
|
|
56
|
+
<button key={p.id} className={p.on ? 'on' : ''} onClick={p.go}>{p.label}</button>
|
|
57
|
+
))}
|
|
58
|
+
</nav>
|
|
59
|
+
<span className="sp" />
|
|
60
|
+
<RunState app={app} />
|
|
61
|
+
<button className="primary" disabled={u.busy} onClick={() => void u.run()}>run the health pass</button>
|
|
62
|
+
<button disabled={u.busy} onClick={() => void u.run(undefined, { gate: true, label: 'the gate' })}>run the gate</button>
|
|
63
|
+
</header>
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/** the key that folds and unfolds the strip, printed on its chevron */
|
|
68
|
+
export const LIVE_KEY = 'l'
|
|
69
|
+
|
|
70
|
+
/** the strip's own row, in px: the one height the folded strip has */
|
|
71
|
+
export const LIVE_ROW_H = 30
|
|
72
|
+
|
|
73
|
+
/** what the strip is, in the (?) beside its chevron */
|
|
74
|
+
export const LIVE_HELP = "every check shipkit runs, whoever started it: this hub, an agent in a terminal, the daily pass, here and in the other repos. Folded, the last two minutes: a bar grows at the right while its check runs and slides left once it ends, in its verdict colour. Unfolded, the whole record, opened on the last ten minutes: the run bands, the time axis, and over the lanes how many checks ran at once, with the cap dashed and the stretches past it in orange. It follows now: the right edge is the present and the view slides with it. The wheel zooms and a drag pans; leaving the right edge stops the view where it is so a bar can be read, and ● live, the k key or panning back to the right edge follows now again. While a tooltip is open the view stands still. fit shows every run on record. A lane at the bottom is another repo with something running; a click opens its hub when it serves. The count is the checks in flight against the runner's cap"
|
|
75
|
+
|
|
76
|
+
const Chevron = ({ open }: { open: boolean }) => (
|
|
77
|
+
<svg className={open ? 'cvt open' : 'cvt'} viewBox="0 0 24 24" width="11" height="11" aria-hidden="true">
|
|
78
|
+
<path d="M9 5l7 7-7 7" fill="none" stroke="currentColor" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round" />
|
|
79
|
+
</svg>
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
export const LiveStrip = observer(({ app }: { app: HubSt }) => {
|
|
83
|
+
const l = app.live
|
|
84
|
+
const v = l.view
|
|
85
|
+
const run = v?.runs[0]
|
|
86
|
+
const hidden = l.chart.layout?.hidden ?? 0
|
|
87
|
+
const waits = v == null ? '' : [
|
|
88
|
+
v.queued > 0 ? `${v.queued} queued for a free slot` : '',
|
|
89
|
+
...v.blocked.map((w) => `${w.id} waits on ${w.deps.join(', ')}`),
|
|
90
|
+
].filter((x) => x !== '').join('\n')
|
|
91
|
+
const who = run == null ? 'nothing running' : `${run.label}`
|
|
92
|
+
const chart = (
|
|
93
|
+
<TimelineChart view={l.chart} compact={!l.open} repos={v?.repos ?? []} follow zoomable={l.open}
|
|
94
|
+
onBar={(b) => app.cols.select('checks', `check:${b.id}`)}
|
|
95
|
+
onRepo={(r) => void l.openRepo(r.repo)} />
|
|
96
|
+
)
|
|
97
|
+
return (
|
|
98
|
+
<div className={`live${l.open ? ' open' : ''}${run == null ? ' idle' : ''}`} style={l.open ? { height: LIVE_ROW_H + l.held.h } : undefined}>
|
|
99
|
+
<div className="lrow">
|
|
100
|
+
<button className="lfold" onClick={() => l.toggle()} title={`${l.open ? 'fold' : 'unfold'} the live timeline (${LIVE_KEY})`}>
|
|
101
|
+
<Chevron open={l.open} /><span className="sk">{LIVE_KEY}</span>
|
|
102
|
+
</button>
|
|
103
|
+
<Help text={LIVE_HELP} />
|
|
104
|
+
<span className="lwho" title={run == null ? '' : `${run.label}, started from the ${byWords(run.by)}${v != null && v.runs.length > 1 ? `\n${v.runs.length - 1} more in flight here` : ''}`}>
|
|
105
|
+
{who}{run == null ? null : <i>{byWords(run.by)}</i>}{v != null && v.runs.length > 1 ? <b>+{v.runs.length - 1}</b> : null}
|
|
106
|
+
</span>
|
|
107
|
+
{l.open ? <span className="sp" /> : <div className="lchart">{chart}</div>}
|
|
108
|
+
{l.note !== '' ? <span className="lnote">{l.note}</span> : null}
|
|
109
|
+
{hidden > 0 ? <span className="lmore" title="check runs that did not fit in the folded strip: unfold it to see them">+{hidden}</span> : null}
|
|
110
|
+
<span className="lbusy" title={waits === '' ? 'checks in flight in this repo, against the runner cap' : waits}>
|
|
111
|
+
in flight <span className="dnum">{v?.busy ?? 0}</span> / cap <span className="dnum">{v?.cap ?? 0}</span>
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
{l.open ? <div className="lopen">{chart}</div> : null}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
})
|