rvlib-shipkit 1.4.0 → 1.6.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 +114 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/assets/gen/comfyts-gen.ts +12 -8
- package/src/assets/server/studio-server.ts +14 -2
- package/src/check.ts +73 -103
- package/src/checks/SK001-package-json-shape.check.ts +11 -6
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +7 -3
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +4 -0
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
- package/src/checks/SK028-readme.check.ts +5 -1
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/checks/SK030-cache-audit.check.ts +34 -0
- package/src/checks/SK031-trace-audit.check.ts +39 -0
- package/src/checks/SK032-ignored-inputs.check.ts +79 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +17 -13
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_CacheAudit.ts +43 -0
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +8 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +120 -7
- package/src/cli/cmd_Home.ts +26 -19
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +20 -8
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +5 -4
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +6 -4
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_TraceAudit.ts +62 -0
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +11 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +12 -4
- package/src/collections/providers.ts +5 -3
- package/src/core/app-icon.ts +154 -0
- package/src/core/audit-report.ts +21 -0
- package/src/core/cache-audit.ts +72 -0
- package/src/core/canonical-scripts.ts +2 -2
- package/src/core/check-ctx.ts +14 -5
- package/src/core/check-runner.ts +43 -23
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/floor-resolve.ts +5 -7
- package/src/core/git-root.ts +13 -0
- package/src/core/ignored-block.ts +94 -0
- package/src/core/ignored-inputs.ts +47 -0
- package/src/core/jobs.ts +9 -2
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/one-lib.ts +59 -4
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/read-set.ts +97 -9
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +12 -9
- package/src/core/run-ci.ts +14 -5
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +48 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/trace-audit.ts +120 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +10 -14
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/gui-app.ts +24 -27
- package/src/home/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/home-start.ts +11 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/open-app.ts +2 -2
- package/src/home/pm.ts +8 -0
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/via-hub.ts +14 -0
- package/src/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/db.ts +2 -13
- package/src/ledger/gate-checks.ts +133 -61
- package/src/ledger/git-key.ts +153 -31
- package/src/ledger/git-scan.ts +5 -2
- package/src/ledger/ledger.ts +136 -21
- package/src/ledger/store.ts +14 -0
- package/src/ledger/toolchain-identity.ts +248 -45
- package/src/plan/repo-source.ts +18 -39
- package/src/problems/extract.ts +3 -0
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +46 -54
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/admin-svc.ts +36 -3
- package/src/serve/hub-app/admin-view.tsx +46 -12
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +14 -35
- package/src/serve/hub-app/col-card.tsx +1 -44
- package/src/serve/hub-app/col-tables-view.tsx +84 -11
- package/src/serve/hub-app/col-tables.ts +27 -0
- package/src/serve/hub-app/cols-svc.ts +5 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +6 -0
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +14 -2
- package/src/serve/hub-app/home-strip.css +43 -12
- package/src/serve/hub-app/home-strip.tsx +115 -64
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +32 -8
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +89 -63
- package/src/serve/hub-app/icons.tsx +11 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +15 -4
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +18 -22
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +45 -0
- package/src/serve/hub-app/project-list.css +4 -1
- package/src/serve/hub-app/project-list.tsx +6 -3
- package/src/serve/hub-app/projects-svc.ts +25 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +131 -0
- package/src/serve/hub-app/repo-st.ts +70 -14
- package/src/serve/hub-app/sidebar.tsx +17 -42
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/tree-build.ts +151 -155
- package/src/serve/hub-app/tree-row.tsx +55 -12
- package/src/serve/hub-app/tree-svc.ts +97 -56
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +7 -18
- package/src/serve/project-gutter-route.ts +22 -1
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/suite/judge-suites.ts +6 -0
- package/src/trace/interpose.c +655 -0
- package/src/trace/read-trace.ts +485 -0
- package/src/trace/tracer.ts +282 -0
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
import { makeAutoObservable, runInAction } from 'mobx'
|
|
11
11
|
import type { RepoSt } from './repo-st.ts'
|
|
12
12
|
import { applyRunEvent, idleRun, startRun, stripText, type RunProgress } from './run-strip.ts'
|
|
13
|
-
import type { GateBlock, Resource, ResourceState, SlowSuite, CheckEvent
|
|
14
|
-
import { WHEN_OPTION } from './check-words.ts'
|
|
13
|
+
import type { GateBlock, Resource, ResourceState, SlowSuite, CheckEvent } from './hub-types.ts'
|
|
15
14
|
import { lineText } from '../../core/wire.ts'
|
|
15
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
16
16
|
|
|
17
17
|
/** the colour rule, the SAME one the server applies, because a row must not
|
|
18
18
|
* change meaning between a live result and the poll that confirms it */
|
|
@@ -55,9 +55,6 @@ export class ChecksSvc {
|
|
|
55
55
|
gateBusy = false
|
|
56
56
|
/** the reason box, per check id: a check is only turned off WITH a reason */
|
|
57
57
|
reasons = new Map<string, string>()
|
|
58
|
-
/** checks whose select says never while the reason is still being typed:
|
|
59
|
-
* off is written only once it has one */
|
|
60
|
-
pendingOff = new Set<string>()
|
|
61
58
|
/** the slow suite form under the test check */
|
|
62
59
|
suite: SlowSuite = { file: '', catches: '', whyNotFast: '' }
|
|
63
60
|
|
|
@@ -94,12 +91,12 @@ export class ChecksSvc {
|
|
|
94
91
|
* silent overwrite */
|
|
95
92
|
async loadGate(): Promise<void> {
|
|
96
93
|
try {
|
|
97
|
-
const r = await
|
|
94
|
+
const r = await reach(this.st.url('/api/checks/when'))
|
|
98
95
|
if (!r.ok) throw new Error(await r.text())
|
|
99
96
|
const b = (await r.json()) as GateBlock
|
|
100
97
|
runInAction(() => { this.gate = b })
|
|
101
98
|
} catch (e) {
|
|
102
|
-
runInAction(() => { this.gateNote = `the repo config could not be read: ${
|
|
99
|
+
runInAction(() => { this.gateNote = ownTrouble(e, this.st.home.machine, (m) => `the repo config could not be read: ${m}`) })
|
|
103
100
|
}
|
|
104
101
|
}
|
|
105
102
|
|
|
@@ -114,7 +111,7 @@ export class ChecksSvc {
|
|
|
114
111
|
// the hash of the file the page last read: without it a write
|
|
115
112
|
// could land on top of an edit this page never saw
|
|
116
113
|
if (this.gate == null) await this.loadGate()
|
|
117
|
-
const r = await
|
|
114
|
+
const r = await reach(this.st.url('/api/checks/when'), { method: 'POST', body: JSON.stringify({ ...edit, expect: this.gate?.hash }) })
|
|
118
115
|
if (!r.ok) throw new Error(await r.text())
|
|
119
116
|
const b = (await r.json()) as GateBlock
|
|
120
117
|
runInAction(() => { this.gate = b; this.gateNote = said })
|
|
@@ -123,7 +120,7 @@ export class ChecksSvc {
|
|
|
123
120
|
// has read the config it just wrote
|
|
124
121
|
await this.st.cols.poll(true)
|
|
125
122
|
} catch (e) {
|
|
126
|
-
runInAction(() => { this.gateNote = `refused: ${
|
|
123
|
+
runInAction(() => { this.gateNote = ownTrouble(e, this.st.home.machine, (m) => `refused: ${m}`) })
|
|
127
124
|
// whatever the file really says now, so the next try is not
|
|
128
125
|
// refused for a hash the page has already outgrown
|
|
129
126
|
await this.loadGate()
|
|
@@ -132,37 +129,19 @@ export class ChecksSvc {
|
|
|
132
129
|
return took
|
|
133
130
|
}
|
|
134
131
|
|
|
135
|
-
/**
|
|
136
|
-
|
|
137
|
-
whenShown(u: { checkId: string; when: WhenAt }): WhenAt {
|
|
138
|
-
return this.pendingOff.has(u.checkId) ? 'off' : u.when
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** the select moved. Every place but never writes at once, and never
|
|
142
|
-
* waits for its reason */
|
|
143
|
-
chooseWhen(checkId: string, at: WhenAt): void {
|
|
144
|
-
if (at === 'off') {
|
|
145
|
-
this.pendingOff.add(checkId)
|
|
146
|
-
this.gateNote = 'never needs a reason: type it, then turn it off'
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
this.pendingOff.delete(checkId)
|
|
150
|
-
void this.setWhen(checkId, at)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async setWhen(checkId: string, at: WhenAt): Promise<void> {
|
|
132
|
+
/** turn a check off in this repo, with the reason typed in its box */
|
|
133
|
+
async setOff(checkId: string): Promise<void> {
|
|
154
134
|
const reason = this.reasonFor(checkId).trim()
|
|
155
|
-
if (
|
|
156
|
-
runInAction(() => { this.gateNote = '
|
|
135
|
+
if (reason === '') {
|
|
136
|
+
runInAction(() => { this.gateNote = 'off needs a reason: a check nobody can explain is a silenced gate' })
|
|
157
137
|
return
|
|
158
138
|
}
|
|
159
|
-
await this.writeGate({
|
|
160
|
-
runInAction(() => { this.pendingOff.delete(checkId) })
|
|
139
|
+
await this.writeGate({ off: { [checkId]: reason } }, `${checkId} is off in this repo — repo.config.ts is edited and waiting for your commit`)
|
|
161
140
|
}
|
|
162
141
|
|
|
163
|
-
/** back
|
|
164
|
-
async
|
|
165
|
-
await this.writeGate({
|
|
142
|
+
/** turn it back on: it runs where it declares again */
|
|
143
|
+
async turnOn(checkId: string): Promise<void> {
|
|
144
|
+
await this.writeGate({ off: { [checkId]: null } }, `${checkId} runs where it declares again — repo.config.ts is edited and waiting for your commit`)
|
|
166
145
|
}
|
|
167
146
|
|
|
168
147
|
async addSlow(): Promise<void> {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// Collection views: the
|
|
2
|
-
// (asks · creds · posts), and the health/status card with the streaming
|
|
1
|
+
// Collection views: the item card (asks · creds · posts), and the health/status card with the streaming
|
|
3
2
|
// check runner. Every verb posts to /api/act/* and lands in a git-tracked
|
|
4
3
|
// truth file; the truth chip names the file the item IS (on disagreement
|
|
5
4
|
// the file wins).
|
|
@@ -101,48 +100,6 @@ export const ColCard = observer(({ app, col, id }: { app: RepoSt; col: string; i
|
|
|
101
100
|
)
|
|
102
101
|
})
|
|
103
102
|
|
|
104
|
-
// collection view — one card per visible instance, the stage-side twin of a
|
|
105
|
-
// sidebar section (every section gets both views)
|
|
106
|
-
export const ColGrid = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
107
|
-
const c = app.cols
|
|
108
|
-
const col = c.sidebarCols.find((x) => x.id === id)
|
|
109
|
-
if (col == null) return <div className="stagewrap"><div className="ph">collection vanished — the truth moved on</div></div>
|
|
110
|
-
const items = c.visibleInstances(col)
|
|
111
|
-
return (
|
|
112
|
-
<div className="colgrid">
|
|
113
|
-
<div className="cghead">{col.title}<span className="cnt">{items.length}</span></div>
|
|
114
|
-
{items.length === 0 ? <div className="ph">{app.filter === 'red' ? 'no reds here' : app.filter === 'needs' ? 'nothing here needs you' : 'nothing here yet'}</div> : (
|
|
115
|
-
<div className="cgcards">
|
|
116
|
-
{items.map((i) => {
|
|
117
|
-
if (id === 'creds') {
|
|
118
|
-
const cred = credOf(i)
|
|
119
|
-
return (
|
|
120
|
-
<div key={i.id} className="cgcard" onClick={() => c.select(col.id, i.id)}>
|
|
121
|
-
<div className="cgtop"><CredHead cred={cred} /></div>
|
|
122
|
-
{cred.service != null ? <div className="creddesc">{cred.service}</div> : null}
|
|
123
|
-
{i.md != null ? <div className="md clamp" dangerouslySetInnerHTML={{ __html: renderMd(i.md) }} /> : null}
|
|
124
|
-
{i.artifact?.url != null ? (
|
|
125
|
-
<div className="credrow"><a className="mint" href={i.artifact.url} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()}>mint ↗</a></div>
|
|
126
|
-
) : null}
|
|
127
|
-
</div>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
return (
|
|
131
|
-
<div key={i.id} className="cgcard" onClick={() => c.select(col.id, i.id)}>
|
|
132
|
-
<div className="cgtop"><b className={`tl ${i.state}`}>{i.text}</b></div>
|
|
133
|
-
{/* a link click opens its tab — it must not ALSO yank into the item view */}
|
|
134
|
-
{i.md != null ? <div className="md clamp" onClick={(e) => { if ((e.target as HTMLElement).tagName === 'A') e.stopPropagation() }} dangerouslySetInnerHTML={{ __html: renderMd(i.md) }} /> : null}
|
|
135
|
-
{i.file != null ? <span className="truth">{i.file}</span> : null}
|
|
136
|
-
{i.remarks != null && i.remarks.length > 0 ? <div className="cgrem">{i.remarks[i.remarks.length - 1]}</div> : null}
|
|
137
|
-
</div>
|
|
138
|
-
)
|
|
139
|
-
})}
|
|
140
|
-
</div>
|
|
141
|
-
)}
|
|
142
|
-
</div>
|
|
143
|
-
)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
103
|
// graph item view — the graph ITSELF fills the stage, lint line as a slim header
|
|
147
104
|
const wireClass = (l: string): string => {
|
|
148
105
|
const t = l.trimStart()
|
|
@@ -3,27 +3,34 @@
|
|
|
3
3
|
// lines. What needs attention comes first; the quiet groups (passing, off,
|
|
4
4
|
// done) fold by default and each viewer's fold choice is kept. The filter
|
|
5
5
|
// input and the severity keys narrow the lines, since both read
|
|
6
|
-
// `visibleInstances`.
|
|
6
|
+
// `visibleInstances`. The checks page opens on the line that counts where
|
|
7
|
+
// its checks run. Groups, order and words: col-tables.ts.
|
|
7
8
|
import { observer } from 'mobx-react-lite'
|
|
8
9
|
import type { ReactNode } from 'react'
|
|
9
10
|
import { checkName } from './check-words.ts'
|
|
10
|
-
import { askGroups, checkGroups, dayMs, findingLine, planWord, type TableGroup } from './col-tables.ts'
|
|
11
|
+
import { askGroups, checkGroups, itemGroups, secretGroups, dayMs, findingLine, planWord, type TableGroup } from './col-tables.ts'
|
|
11
12
|
import { durationTone, fmtMs } from './dur.ts'
|
|
13
|
+
import type { CheckOwner } from './hash-route.ts'
|
|
12
14
|
import type { Resource } from './hub-types.ts'
|
|
15
|
+
import { sectionLabel } from './keys.ts'
|
|
13
16
|
import type { RepoSt } from './repo-st.ts'
|
|
14
17
|
import { TimeAgo } from './time-ago.tsx'
|
|
15
18
|
|
|
16
19
|
const emptyWords = (app: RepoSt): string => (app.filter === 'red' ? 'no reds here' : app.filter === 'needs' ? 'nothing here needs you' : app.query.trim() !== '' ? 'nothing matches the filter' : 'nothing here yet')
|
|
17
20
|
|
|
18
|
-
/** the page frame: the collection's title and count,
|
|
19
|
-
|
|
21
|
+
/** the page frame: the collection's title and count, a line under it when
|
|
22
|
+
* the page has one, then the table */
|
|
23
|
+
const Page = ({ title, n, lead, cols, empty, children }: { title: string; n: number; lead?: string; cols: ReactNode; empty: string; children: ReactNode }) => (
|
|
20
24
|
<div className="ctpage">
|
|
21
25
|
<div className="cghead">{title}<span className="cnt">{n}</span></div>
|
|
26
|
+
{lead == null || lead === '' ? null : <div className="ctlead">{lead}</div>}
|
|
22
27
|
{n === 0 ? <div className="ph">{empty}</div> : (
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
<div className="ctbox">
|
|
29
|
+
<table className="actable cttable">
|
|
30
|
+
<colgroup>{cols}</colgroup>
|
|
31
|
+
{children}
|
|
32
|
+
</table>
|
|
33
|
+
</div>
|
|
27
34
|
)}
|
|
28
35
|
</div>
|
|
29
36
|
)
|
|
@@ -73,17 +80,19 @@ const CheckRow = observer(({ app, r }: { app: RepoSt; r: Resource }) => {
|
|
|
73
80
|
)
|
|
74
81
|
})
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
/** every check, or only the ones one owner ships (`from`) */
|
|
84
|
+
export const CheckTable = observer(({ app, id, from }: { app: RepoSt; id: string; from?: CheckOwner }) => {
|
|
77
85
|
const col = app.cols.sidebarCols.find((x) => x.id === id)
|
|
78
86
|
if (col == null) return <div className="stagewrap"><div className="ph">collection vanished — the truth moved on</div></div>
|
|
79
|
-
const items = app.cols.visibleInstances(col)
|
|
87
|
+
const items = app.cols.visibleInstances(col).filter((r) => from == null || r.check?.from === from)
|
|
80
88
|
const cols = (
|
|
81
89
|
<>
|
|
82
90
|
<col className="c-dot" /><col className="c-id" /><col className="c-title" /><col className="c-age" /><col className="c-dur" /><col /><col className="c-plan" />
|
|
83
91
|
</>
|
|
84
92
|
)
|
|
85
93
|
return (
|
|
86
|
-
|
|
94
|
+
// where the checks run, counted over every check whatever the filter
|
|
95
|
+
<Page title={from == null ? col.title : sectionLabel(from)} n={items.length} lead={app.tree.whenLine} cols={cols} empty={emptyWords(app)}>
|
|
87
96
|
{checkGroups(items).map((g) => <Group key={g.key} app={app} col={col.id} g={g} span={7} row={(r) => <CheckRow key={r.id} app={app} r={r} />} />)}
|
|
88
97
|
</Page>
|
|
89
98
|
)
|
|
@@ -122,3 +131,67 @@ export const AskTable = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
|
122
131
|
</Page>
|
|
123
132
|
)
|
|
124
133
|
})
|
|
134
|
+
|
|
135
|
+
const SECRET_TONE: Record<string, string> = { missing: 't-red', minor: 'acdim', present: 't-ok' }
|
|
136
|
+
|
|
137
|
+
/** a secret's sentence without its own name in front, which the name column says */
|
|
138
|
+
const secretSentence = (r: Resource): string => (r.text.startsWith(r.title) ? r.text.slice(r.title.length).replace(/^[\s,]+/, '') : r.text)
|
|
139
|
+
|
|
140
|
+
const SecretRow = observer(({ app, r }: { app: RepoSt; r: Resource }) => {
|
|
141
|
+
const slot = r.slot ?? 'present'
|
|
142
|
+
const tone = SECRET_TONE[slot] ?? ''
|
|
143
|
+
return (
|
|
144
|
+
<tr className={`ctrow s-${slot}`} data-k={r.id} onClick={() => app.cols.select(r.collection, r.id)} title={r.text}>
|
|
145
|
+
<td className={`ctdot ${tone}`}>{slot === 'minor' ? '○' : '●'}</td>
|
|
146
|
+
<td className="ctid">{r.title}</td>
|
|
147
|
+
<td className={`ctkind ${tone}`}>{slot}</td>
|
|
148
|
+
<td className="ctfind">{secretSentence(r)}</td>
|
|
149
|
+
</tr>
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
/** the secrets page: one line per credential, never a card */
|
|
154
|
+
export const SecretTable = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
155
|
+
const col = app.cols.sidebarCols.find((x) => x.id === id)
|
|
156
|
+
if (col == null) return <div className="stagewrap"><div className="ph">collection vanished — the truth moved on</div></div>
|
|
157
|
+
const items = app.cols.visibleInstances(col)
|
|
158
|
+
const cols = (
|
|
159
|
+
<>
|
|
160
|
+
<col className="c-dot" /><col className="c-name" /><col className="c-kind" /><col />
|
|
161
|
+
</>
|
|
162
|
+
)
|
|
163
|
+
return (
|
|
164
|
+
<Page title={col.title} n={items.length} cols={cols} empty={emptyWords(app)}>
|
|
165
|
+
{secretGroups(items).map((g) => <Group key={g.key} app={app} col={col.id} g={g} span={4} row={(r) => <SecretRow key={r.id} app={app} r={r} />} />)}
|
|
166
|
+
</Page>
|
|
167
|
+
)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const ITEM_TONE: Record<string, string> = { red: 't-red', warn: 't-warn', ok: 't-ok', idle: '', quiet: 'acdim' }
|
|
171
|
+
|
|
172
|
+
const ItemRow = observer(({ app, r }: { app: RepoSt; r: Resource }) => (
|
|
173
|
+
<tr className={`ctrow s-${r.state}`} data-k={r.id} onClick={() => app.cols.select(r.collection, r.id)} title={r.file ?? r.text}>
|
|
174
|
+
<td className={`ctdot ${ITEM_TONE[r.state] ?? ''}`}>●</td>
|
|
175
|
+
<td className={`cttext ${ITEM_TONE[r.state] ?? ''}`}>{r.text}</td>
|
|
176
|
+
<td className="ctkind">{r.slot ?? ''}</td>
|
|
177
|
+
<td className="ctfind">{r.file ?? ''}</td>
|
|
178
|
+
</tr>
|
|
179
|
+
))
|
|
180
|
+
|
|
181
|
+
/** every collection without a table of its own: one line per item, never a
|
|
182
|
+
* card. What it says in full is its own page, one click away */
|
|
183
|
+
export const ItemTable = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
184
|
+
const col = app.cols.sidebarCols.find((x) => x.id === id)
|
|
185
|
+
if (col == null) return <div className="stagewrap"><div className="ph">collection vanished — the truth moved on</div></div>
|
|
186
|
+
const items = app.cols.visibleInstances(col)
|
|
187
|
+
const cols = (
|
|
188
|
+
<>
|
|
189
|
+
<col className="c-dot" /><col /><col className="c-kind" /><col className="c-file" />
|
|
190
|
+
</>
|
|
191
|
+
)
|
|
192
|
+
return (
|
|
193
|
+
<Page title={col.title} n={items.length} cols={cols} empty={emptyWords(app)}>
|
|
194
|
+
{itemGroups(items).map((g) => <Group key={g.key} app={app} col={col.id} g={g} span={4} row={(r) => <ItemRow key={r.id} app={app} r={r} />} />)}
|
|
195
|
+
</Page>
|
|
196
|
+
)
|
|
197
|
+
})
|
|
@@ -113,3 +113,30 @@ export function askGroups(rows: readonly Resource[]): TableGroup<AskGroupKey>[]
|
|
|
113
113
|
]
|
|
114
114
|
return groups.filter((g) => g.rows.length > 0)
|
|
115
115
|
}
|
|
116
|
+
|
|
117
|
+
export type SecretGroupKey = 'missing' | 'minor' | 'present'
|
|
118
|
+
|
|
119
|
+
/** the secrets in the order a reader wants them: what is missing first, a
|
|
120
|
+
* minor one nothing uses yet next, then what is present, each by name */
|
|
121
|
+
export function secretGroups(rows: readonly Resource[]): TableGroup<SecretGroupKey>[] {
|
|
122
|
+
const by = (key: SecretGroupKey): Resource[] => rows.filter((r) => (r.slot ?? 'present') === key).sort((a, b) => a.title.localeCompare(b.title))
|
|
123
|
+
const groups: TableGroup<SecretGroupKey>[] = [
|
|
124
|
+
{ key: 'missing', label: `${by('missing').length} missing`, rows: by('missing'), foldedByDefault: false },
|
|
125
|
+
{ key: 'minor', label: `${by('minor').length} minor`, rows: by('minor'), foldedByDefault: false },
|
|
126
|
+
{ key: 'present', label: `${by('present').length} present`, rows: by('present'), foldedByDefault: false },
|
|
127
|
+
]
|
|
128
|
+
return groups.filter((g) => g.rows.length > 0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type ItemGroupKey = 'red' | 'warn' | 'rest'
|
|
132
|
+
|
|
133
|
+
/** any other collection, one line per item: what is red first, then what
|
|
134
|
+
* warns, then the rest, each by its words */
|
|
135
|
+
export function itemGroups(rows: readonly Resource[]): TableGroup<ItemGroupKey>[] {
|
|
136
|
+
const keyOf = (r: Resource): ItemGroupKey => (r.state === 'red' ? 'red' : r.state === 'warn' ? 'warn' : 'rest')
|
|
137
|
+
const by = (key: ItemGroupKey): Resource[] => rows.filter((r) => keyOf(r) === key).sort((a, b) => a.text.localeCompare(b.text))
|
|
138
|
+
const word: Record<ItemGroupKey, string> = { red: 'red', warn: 'warning', rest: 'the rest' }
|
|
139
|
+
return (['red', 'warn', 'rest'] as const)
|
|
140
|
+
.map((key) => ({ key, label: `${by(key).length} ${word[key]}`, rows: by(key), foldedByDefault: false }))
|
|
141
|
+
.filter((g) => g.rows.length > 0)
|
|
142
|
+
}
|
|
@@ -8,6 +8,7 @@ import type { RepoSt } from './repo-st.ts'
|
|
|
8
8
|
import type { Col, ColMeta, HubProblem, Resource } from './hub-types.ts'
|
|
9
9
|
import type { PlanRow } from '../../plan/planner.ts'
|
|
10
10
|
import { readScreen, SCREEN_WRITE_MS, writeScreen } from './screen-cache.ts'
|
|
11
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
11
12
|
|
|
12
13
|
/** where a viewer's fold choices on the checks and asks tables are kept,
|
|
13
14
|
* one per browser and not per repo: `checks:pass` open is a habit */
|
|
@@ -132,7 +133,7 @@ export class ColsSvc {
|
|
|
132
133
|
* the server keeps warm */
|
|
133
134
|
async poll(fresh = false): Promise<void> {
|
|
134
135
|
try {
|
|
135
|
-
const r = await
|
|
136
|
+
const r = await reach(this.st.url(fresh ? '/api/resources.json?fresh=1' : '/api/resources.json'))
|
|
136
137
|
if (!r.ok) throw new Error(await r.text())
|
|
137
138
|
const j = (await r.json()) as Answer
|
|
138
139
|
runInAction(() => {
|
|
@@ -148,7 +149,7 @@ export class ColsSvc {
|
|
|
148
149
|
} catch (e) {
|
|
149
150
|
// a dead server must SAY so, in the header, never leave a tree
|
|
150
151
|
// that reads like a healthy repo. The last rows stay, dated by it
|
|
151
|
-
runInAction(() => { this.fetchErr = `the page cannot reach this hub: ${
|
|
152
|
+
runInAction(() => { this.fetchErr = ownTrouble(e, this.st.home.machine, (m) => `the page cannot reach this hub: ${m}`) || null })
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
|
|
@@ -237,12 +238,12 @@ export class ColsSvc {
|
|
|
237
238
|
private async act(path: string, body: Record<string, unknown>): Promise<boolean> {
|
|
238
239
|
this.actErr = ''
|
|
239
240
|
try {
|
|
240
|
-
const r = await
|
|
241
|
+
const r = await reach(this.st.url(`/api/act/${path}`), { method: 'POST', body: JSON.stringify(body) })
|
|
241
242
|
if (!r.ok) throw new Error(await r.text())
|
|
242
243
|
await this.poll(true)
|
|
243
244
|
return true
|
|
244
245
|
} catch (e) {
|
|
245
|
-
runInAction(() => { this.actErr = (e
|
|
246
|
+
runInAction(() => { this.actErr = ownTrouble(e, this.st.home.machine) })
|
|
246
247
|
return false
|
|
247
248
|
}
|
|
248
249
|
}
|
|
@@ -22,6 +22,7 @@ import { serverNow } from './live-svc.ts'
|
|
|
22
22
|
import { ROWS, type RowSize } from './timeline-geometry.ts'
|
|
23
23
|
import { ageWords } from './time-ago.tsx'
|
|
24
24
|
import type { HomeSt } from './home-st.ts'
|
|
25
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
25
26
|
|
|
26
27
|
export type DockSize = 'line' | 'slim' | 'open'
|
|
27
28
|
|
|
@@ -296,7 +297,7 @@ export class DockSt {
|
|
|
296
297
|
if (this.inFlight) return
|
|
297
298
|
this.inFlight = true
|
|
298
299
|
try {
|
|
299
|
-
const r = await
|
|
300
|
+
const r = await reach('/api/house-runs.json')
|
|
300
301
|
if (!r.ok) throw new Error(await r.text())
|
|
301
302
|
const v = (await r.json()) as HouseView
|
|
302
303
|
runInAction(() => {
|
|
@@ -306,7 +307,7 @@ export class DockSt {
|
|
|
306
307
|
this.error = ''
|
|
307
308
|
})
|
|
308
309
|
} catch (e) {
|
|
309
|
-
runInAction(() => { this.error = `the runs could not be read: ${
|
|
310
|
+
runInAction(() => { this.error = ownTrouble(e, this.home.machine, (m) => `the runs could not be read: ${m}`) })
|
|
310
311
|
} finally {
|
|
311
312
|
this.inFlight = false
|
|
312
313
|
}
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// then the status as small pills that fold into +N rather than wrap. The
|
|
5
5
|
// state is dock-st.ts; the height is the page's panel (hub-app.tsx).
|
|
6
6
|
import { observer } from 'mobx-react-lite'
|
|
7
|
+
import { useEffect, useState } from 'react'
|
|
8
|
+
import { Tip } from './floating.tsx'
|
|
9
|
+
import { Icon, ICON } from './icons.tsx'
|
|
7
10
|
import type { HomeSt } from './home-st.ts'
|
|
8
11
|
import { CycleButton } from './cycle-button.tsx'
|
|
9
12
|
import { DOCK_KEY } from './keys.ts'
|
|
@@ -15,6 +18,38 @@ import type { PlacedGroup } from './timeline-geometry.ts'
|
|
|
15
18
|
/** what the dock is, on its size button's hover */
|
|
16
19
|
export const DOCK_HELP = `every check shipkit runs in every repo, whoever started it: a page, an agent in a terminal, the plan. The line, the pills alone; slim, the last two minutes, one group of lanes per repo; open, the last ten, zoomed by the wheel and panned by a drag, its height dragged at the top edge`
|
|
17
20
|
|
|
21
|
+
const COPIED_MS = 1500
|
|
22
|
+
|
|
23
|
+
/** the page's own address, to paste somewhere: the app window has no url bar */
|
|
24
|
+
export const CopyUrl = () => {
|
|
25
|
+
const [href, setHref] = useState(() => globalThis.location?.href ?? '')
|
|
26
|
+
const [said, setSaid] = useState<string | null>(null)
|
|
27
|
+
// the router moves the address with pushState and replaceState, which no
|
|
28
|
+
// event reports: read it when the pointer or the focus arrives
|
|
29
|
+
const sync = () => setHref(location.href)
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (said == null) return
|
|
32
|
+
const t = setTimeout(() => setSaid(null), COPIED_MS)
|
|
33
|
+
return () => clearTimeout(t)
|
|
34
|
+
}, [said])
|
|
35
|
+
const click = async () => {
|
|
36
|
+
try {
|
|
37
|
+
if (globalThis.navigator?.clipboard == null) throw new Error('this page has no clipboard')
|
|
38
|
+
await navigator.clipboard.writeText(location.href)
|
|
39
|
+
setSaid('copied')
|
|
40
|
+
} catch (e) {
|
|
41
|
+
setSaid(`could not copy: ${(e as Error).message}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
<Tip className="dcopy" instant placement="top-end" content={<><div>{said ?? 'click to copy this page\'s url'}</div><code>{href}</code></>}>
|
|
46
|
+
<button className={`ghost dcopyb${said === 'copied' ? ' ok' : ''}`} aria-label="copy this page's url" onMouseEnter={sync} onFocus={sync} onClick={() => void click()}>
|
|
47
|
+
<Icon d={ICON.link} />
|
|
48
|
+
</button>
|
|
49
|
+
</Tip>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
18
53
|
/** a group's label: the repo's icon and its name in its colour */
|
|
19
54
|
const GroupLabel = observer(({ app, g }: { app: HomeSt; g: PlacedGroup }) => {
|
|
20
55
|
const p = app.projects.list.find((x) => x.name === g.key)
|
|
@@ -55,6 +90,7 @@ export const Dock = observer(({ app }: { app: HomeSt }) => {
|
|
|
55
90
|
<CycleButton options={DOCK_SIZES} value={d.size} onChange={(s) => d.setSize(s)} shortcut={DOCK_KEY} title={DOCK_HELP} />
|
|
56
91
|
<PillRow items={d.pills.map((p) => ({ key: p.key, title: p.title, node: <PillFace app={app} p={p} /> }))} />
|
|
57
92
|
{d.error !== '' ? <span className="warnline">{d.error}</span> : null}
|
|
93
|
+
<CopyUrl />
|
|
58
94
|
</div>
|
|
59
95
|
{d.size === 'line' ? null : <div className="dbody">
|
|
60
96
|
{v == null ? null : groups.length === 0
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
.dhead{display:flex;gap:var(--s2);align-items:center;height:22px;flex:none;padding:0 var(--s3)}
|
|
7
7
|
/* the status pills take the room left, and fold rather than wrap */
|
|
8
8
|
.dhead .pchips{flex:1}
|
|
9
|
+
/* the page's url to the clipboard, at the bottom right corner */
|
|
10
|
+
.dcopy{flex:none;display:flex;margin-left:auto}
|
|
11
|
+
.dcopyb{display:flex;align-items:center;padding:0 var(--s0);color:var(--dim)}
|
|
12
|
+
.dcopyb:hover{color:var(--ink)}
|
|
13
|
+
.dcopyb.ok{color:var(--ok)}
|
|
14
|
+
.dcopyb .hico{width:13px;height:13px;color:inherit}
|
|
9
15
|
.dbody{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:0 var(--s3) var(--s1);scrollbar-width:thin;scrollbar-color:var(--line) transparent}
|
|
10
16
|
.dempty{padding:var(--s1) 0;color:var(--dim)}
|
|
11
17
|
.dock .tlax.compact{height:auto;padding-top:0}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// THE way the page says trouble: cards floating at the top right, just under
|
|
2
|
+
// the bar, taking no layout room, so one coming or going moves nothing, and
|
|
3
|
+
// nothing drawn while all is well. A bar never carries trouble text. Styles:
|
|
4
|
+
// home-strip.css .htoasts.
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
|
|
7
|
+
export type Card = {
|
|
8
|
+
key: string
|
|
9
|
+
tone: 'red' | 'warn'
|
|
10
|
+
/** a bold lead word or two ("web crashed"), before the words */
|
|
11
|
+
head?: ReactNode
|
|
12
|
+
body?: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** the cards, in a zero height box placed right after the bar, so they
|
|
16
|
+
* float just under it whatever the bar's height */
|
|
17
|
+
export const FloatCards = ({ cards }: { cards: Card[] }) => {
|
|
18
|
+
if (cards.length === 0) return null
|
|
19
|
+
return (
|
|
20
|
+
<div className="hanchor">
|
|
21
|
+
<div className="htoasts">
|
|
22
|
+
{cards.map((c) => (
|
|
23
|
+
<div key={c.key} className={c.tone === 'warn' ? 'htoast warn' : 'htoast'} role="alert">
|
|
24
|
+
{c.head == null ? null : <><b>{c.head}</b> </>}{c.body}
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// (flip to stay in the window, shift off its edges, an offset from the
|
|
4
4
|
// trigger), a portal lifts it out of every box that clips, and hover opens
|
|
5
5
|
// it after a short delay, so a pointer passing by never flickers one open.
|
|
6
|
-
//
|
|
7
|
-
|
|
6
|
+
// A popover (Pop) opens on a click instead, and Escape or a click outside
|
|
7
|
+
// closes it. Nothing here is positioned by hand.
|
|
8
|
+
import { autoUpdate, flip, FloatingFocusManager, FloatingPortal, offset, safePolygon, shift, useClick, useDismiss, useFloating, useFocus, useHover, useInteractions, useRole, type Placement } from '@floating-ui/react'
|
|
8
9
|
import { observer, useLocalObservable } from 'mobx-react-lite'
|
|
9
10
|
import type { HTMLAttributes, ReactNode } from 'react'
|
|
10
11
|
|
|
@@ -25,13 +26,15 @@ type TipProps = Omit<HTMLAttributes<HTMLSpanElement>, 'content'> & {
|
|
|
25
26
|
fallbacks?: Placement[]
|
|
26
27
|
/** the pointer can move into it (a log to scroll): it stays open on the way */
|
|
27
28
|
interactive?: boolean
|
|
29
|
+
/** opens the moment the pointer arrives, for a trigger whose only words are its tip */
|
|
30
|
+
instant?: boolean
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/** a trigger and what floats over it on hover or focus */
|
|
31
|
-
export const Tip = observer(({ content, children, tipClass = 'ftip', placement = 'top', fallbacks, interactive = false, ...rest }: TipProps) => {
|
|
34
|
+
export const Tip = observer(({ content, children, tipClass = 'ftip', placement = 'top', fallbacks, interactive = false, instant = false, ...rest }: TipProps) => {
|
|
32
35
|
const st = useLocalObservable(() => ({ open: false, set(v: boolean) { this.open = v } }))
|
|
33
36
|
const f = useFloating({ open: st.open, onOpenChange: (v) => st.set(v), placement, strategy: 'fixed', middleware: middleware(6, fallbacks), whileElementsMounted: autoUpdate })
|
|
34
|
-
const hover = useHover(f.context, { delay: { open: OPEN_MS, close: interactive ? 100 : 0 }, handleClose: interactive ? safePolygon() : null })
|
|
37
|
+
const hover = useHover(f.context, { delay: { open: instant ? 0 : OPEN_MS, close: interactive ? 100 : 0 }, handleClose: interactive ? safePolygon() : null })
|
|
35
38
|
const focus = useFocus(f.context)
|
|
36
39
|
const dismiss = useDismiss(f.context)
|
|
37
40
|
const role = useRole(f.context, { role: 'tooltip' })
|
|
@@ -47,3 +50,37 @@ export const Tip = observer(({ content, children, tipClass = 'ftip', placement =
|
|
|
47
50
|
</>
|
|
48
51
|
)
|
|
49
52
|
})
|
|
53
|
+
|
|
54
|
+
type PopProps = {
|
|
55
|
+
/** what the trigger button shows */
|
|
56
|
+
label: ReactNode
|
|
57
|
+
className?: string
|
|
58
|
+
'aria-label'?: string
|
|
59
|
+
popClass?: string
|
|
60
|
+
placement?: Placement
|
|
61
|
+
/** what floats, given the way to close it (an entry that acts closes it) */
|
|
62
|
+
children: (close: () => void) => ReactNode
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** a button that opens what floats under it on a click; focus moves into
|
|
66
|
+
* it, and Escape or a click outside closes it */
|
|
67
|
+
export const Pop = observer(({ label, className, popClass = 'fpop', placement = 'bottom-start', children, ...rest }: PopProps) => {
|
|
68
|
+
const st = useLocalObservable(() => ({ open: false, set(v: boolean) { this.open = v } }))
|
|
69
|
+
const f = useFloating({ open: st.open, onOpenChange: (v) => st.set(v), placement, strategy: 'fixed', middleware: middleware(4), whileElementsMounted: autoUpdate })
|
|
70
|
+
const click = useClick(f.context)
|
|
71
|
+
const dismiss = useDismiss(f.context)
|
|
72
|
+
const role = useRole(f.context, { role: 'dialog' })
|
|
73
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role])
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<button type="button" ref={f.refs.setReference} className={className} {...getReferenceProps(rest)}>{label}</button>
|
|
77
|
+
{st.open ? (
|
|
78
|
+
<FloatingPortal>
|
|
79
|
+
<FloatingFocusManager context={f.context} modal={false}>
|
|
80
|
+
<div ref={f.refs.setFloating} className={popClass} style={f.floatingStyles} {...getFloatingProps()}>{children(() => st.set(false))}</div>
|
|
81
|
+
</FloatingFocusManager>
|
|
82
|
+
</FloatingPortal>
|
|
83
|
+
) : null}
|
|
84
|
+
</>
|
|
85
|
+
)
|
|
86
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// A repo's git page state: one repo's git, read from the daemon while the
|
|
2
|
+
// page is shown (`/api/home/git.json`), a week of commits and HEAD's last
|
|
3
|
+
// one past that. The activity page reads every repo for a window; this reads
|
|
4
|
+
// one, whatever that window is.
|
|
5
|
+
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
6
|
+
import type { ActivityRow } from '../../home/activity-shape.ts'
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
9
|
+
|
|
10
|
+
export const GIT_POLL_MS = 30_000
|
|
11
|
+
|
|
12
|
+
export class GitSt {
|
|
13
|
+
row: ActivityRow | null = null
|
|
14
|
+
error = ''
|
|
15
|
+
private timer: ReturnType<typeof setInterval> | null = null
|
|
16
|
+
|
|
17
|
+
constructor(private st: RepoSt) {
|
|
18
|
+
makeAutoObservable<this, 'st' | 'timer'>(this, { st: false, timer: false, row: observableRef })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async load(): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
const r = await reach(`/api/home/git.json?repo=${encodeURIComponent(this.st.key)}`, { signal: AbortSignal.timeout(30_000) })
|
|
24
|
+
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
25
|
+
const j = (await r.json()) as ActivityRow
|
|
26
|
+
runInAction(() => { this.row = j; this.error = '' })
|
|
27
|
+
} catch (e) {
|
|
28
|
+
runInAction(() => { this.error = ownTrouble(e, this.st.home.machine, (m) => `read git: ${m}`) })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** reads while the page is shown */
|
|
33
|
+
start(): void {
|
|
34
|
+
if (this.timer != null) return
|
|
35
|
+
void this.load()
|
|
36
|
+
this.timer = setInterval(() => void this.load(), GIT_POLL_MS)
|
|
37
|
+
}
|
|
38
|
+
stop(): void {
|
|
39
|
+
if (this.timer != null) clearInterval(this.timer)
|
|
40
|
+
this.timer = null
|
|
41
|
+
}
|
|
42
|
+
}
|