rvlib-shipkit 1.3.0 → 1.5.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 +105 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +12 -2
- package/src/check.ts +73 -98
- package/src/checks/SK001-package-json-shape.check.ts +26 -4
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- 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 +94 -74
- 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 +20 -6
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
- 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 +34 -9
- package/src/checks/SK028-readme.check.ts +10 -2
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- 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 +10 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- 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 +4 -3
- 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_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +24 -4
- package/src/collections/providers.ts +6 -2
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +63 -25
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +31 -7
- package/src/core/layout.ts +2 -0
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/motherhome.ts +88 -66
- 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/retention.ts +0 -14
- package/src/core/run-check.ts +14 -14
- package/src/core/run-ci.ts +11 -7
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -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 +34 -9
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +30 -6
- package/src/home/home-routes.ts +14 -2
- package/src/home/home-start.ts +33 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +11 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +60 -6
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +115 -61
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/store.ts +11 -0
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/ledger/world-cache.ts +17 -15
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +36 -56
- 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 +4 -4
- package/src/serve/hub-app/admin-view.tsx +13 -11
- 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 +16 -36
- package/src/serve/hub-app/col-tables-view.tsx +131 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +33 -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 +7 -1
- 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 +15 -3
- package/src/serve/hub-app/home-strip.css +69 -19
- package/src/serve/hub-app/home-strip.tsx +192 -118
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +21 -5
- package/src/serve/hub-app/hub-types.ts +11 -5
- package/src/serve/hub-app/hub.css +88 -44
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +11 -8
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +34 -49
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- 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 +38 -0
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/projects-svc.ts +15 -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 +117 -0
- package/src/serve/hub-app/repo-st.ts +47 -10
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- 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 +3 -16
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/warm-cell.ts +49 -16
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- 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
|
@@ -1,45 +1,42 @@
|
|
|
1
1
|
// ONE GATE CHECK, on its own page, as ONE compact block: the id as a tag, the
|
|
2
2
|
// title, the sentence it wrote about itself, the exact command it runs, then
|
|
3
3
|
// one grid of facts, each a dim label and a short value whose explanation is
|
|
4
|
-
// the row's `(?)
|
|
4
|
+
// the row's `(?)`, then where it runs: one line per context. No box holds a single field. Navigation is the tree on the
|
|
5
5
|
// left, so there is no back link.
|
|
6
6
|
import { observer } from 'mobx-react-lite'
|
|
7
7
|
import { useEffect, useRef } from 'react'
|
|
8
8
|
import { CACHED } from '../../core/outcome-words.ts'
|
|
9
|
+
import { cellMeaning, MATRIX_AXES, RUNS_LEGEND } from '../../core/runs-matrix.ts'
|
|
9
10
|
import { durationTone, fmtMs } from './dur.ts'
|
|
10
11
|
import { Help, TERMS } from './help.tsx'
|
|
11
12
|
import type { RepoSt } from './repo-st.ts'
|
|
12
|
-
import type { SlowSuite, CheckFacts, RecentRun
|
|
13
|
+
import type { SlowSuite, CheckFacts, RecentRun } from './hub-types.ts'
|
|
13
14
|
import { renderMd, renderMdInline } from './md.ts'
|
|
14
15
|
import { nextRun } from './next-run.ts'
|
|
15
16
|
import { ProseHits } from './prose-hits-view.tsx'
|
|
16
17
|
import { stampWords, TimeAgo, TimeAhead } from './time-ago.tsx'
|
|
17
|
-
import { checkName, commandOf, docPath, editorHref, lastRun, rememberedFact, repoPath, whatFacts,
|
|
18
|
+
import { checkName, commandOf, docPath, editorHref, lastRun, rememberedFact, repoPath, whatFacts, type ShortPath } from './check-words.ts'
|
|
18
19
|
|
|
19
20
|
/** which declaration is answering, so the switch never looks like it wrote
|
|
20
21
|
* something it did not. The CHIP is the short one, its help is the sentence */
|
|
21
22
|
const SOURCE_CHIP: Record<CheckFacts['whenSource'], string> = {
|
|
22
|
-
|
|
23
|
+
off: 'repo.config.ts',
|
|
24
|
+
when: 'when (retired)',
|
|
25
|
+
ruleExempt: 'ruleExempt (retired)',
|
|
23
26
|
'check-file': 'the check file',
|
|
24
|
-
ruleExempt: 'ruleExempt',
|
|
25
|
-
optIn: 'optIn',
|
|
26
|
-
'gate-export': 'gate = false',
|
|
27
27
|
defaults: 'defaults',
|
|
28
|
-
default: '
|
|
28
|
+
default: 'derived',
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const SOURCE_WORD: Record<CheckFacts['whenSource'], string> = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
ruleExempt: '
|
|
35
|
-
|
|
36
|
-
'gate-export': 'from `export const gate = false` in its own file',
|
|
32
|
+
off: 'turned off in the `off` block of `repo.config.ts`, which this switch writes',
|
|
33
|
+
when: 'turned off by the retired `when` key, which `shipkit ci` ports to `off` (SK029)',
|
|
34
|
+
ruleExempt: 'turned off by the retired `ruleExempt` key, which `shipkit ci` ports to `off` (SK029)',
|
|
35
|
+
'check-file': 'declared by the check file itself: every context it runs in, no default',
|
|
37
36
|
defaults: 'from the `defaults` block, which removes the floor slot entirely',
|
|
38
|
-
default: '
|
|
37
|
+
default: 'a row with no file of its own: the code that derives it writes every context',
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
const WHEN_VALUES: WhenAt[] = ['always', 'release', 'health', 'off']
|
|
42
|
-
|
|
43
40
|
const VERDICT_WORD: Record<'pass' | 'warn' | 'fail' | 'skip', string> = {
|
|
44
41
|
pass: 'passed', warn: 'warned', fail: 'failed', skip: 'skipped',
|
|
45
42
|
}
|
|
@@ -126,44 +123,67 @@ export const CommandLine = ({ app, u }: { app: RepoSt; u: CheckFacts }) => {
|
|
|
126
123
|
)
|
|
127
124
|
}
|
|
128
125
|
|
|
129
|
-
// ──
|
|
126
|
+
// ── where it runs ────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/** every context, one line each: the value the check declared and what it
|
|
129
|
+
* means in the legend's words. The cache closes it, the off switch ends it */
|
|
130
|
+
const WhereItRuns = observer(({ app, u }: { app: RepoSt; u: CheckFacts }) => {
|
|
131
|
+
const r = u.runs
|
|
132
|
+
return (
|
|
133
|
+
<Section title="where it runs" help="every context a check declares, with no default. The when page of this repo shows every check at once, and `sk when` prints the same">
|
|
134
|
+
{r.off != null
|
|
135
|
+
? <div className="wline">runs nowhere: {r.off}</div>
|
|
136
|
+
: <div className="cruns">
|
|
137
|
+
{MATRIX_AXES.map((k) => {
|
|
138
|
+
const c = r.cells[k]
|
|
139
|
+
const a = RUNS_LEGEND.find((x) => x.axis === k)!
|
|
140
|
+
return (
|
|
141
|
+
<div key={k} className={`crun${c.on ? ' on' : ''}`}>
|
|
142
|
+
<span className="ck" title={`${k}: ${a.context.replace(/`/g, '')}. From ${c.source}`}>{a.short}</span>
|
|
143
|
+
<span className="crv">{c.on ? c.text : 'no'}</span>
|
|
144
|
+
<span className="crm">{cellMeaning(k, c.text)}</span>
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
})}
|
|
148
|
+
<div className="crun cache">
|
|
149
|
+
<span className="ck" title={`cache: how an answer is kept. ${r.cache}`}>cache</span>
|
|
150
|
+
<span className="crv">{r.cache.split(/[\s:]/)[0]}</span>
|
|
151
|
+
<span className="crm">{cellMeaning('cache', r.cache)}</span>
|
|
152
|
+
</div>
|
|
153
|
+
</div>}
|
|
154
|
+
{r.derivedWhy == null ? null : <div className="note">no file declares this row: {r.derivedWhy}</div>}
|
|
155
|
+
<div className="cfacts"><OffSwitch app={app} u={u} /></div>
|
|
156
|
+
</Section>
|
|
157
|
+
)
|
|
158
|
+
})
|
|
130
159
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
*
|
|
135
|
-
|
|
160
|
+
// ── the off switch ───────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/** the one thing this repo says about a check: that it is OFF here, with the
|
|
163
|
+
* reason. It writes `repo.config.ts` through the managed block and leaves the
|
|
164
|
+
* file dirty for its owner to review */
|
|
165
|
+
const OffSwitch = observer(({ app, u }: { app: RepoSt; u: CheckFacts }) => {
|
|
136
166
|
const s = app.checks
|
|
137
167
|
const g = s.gate
|
|
138
|
-
const shown = s.whenShown(u)
|
|
139
168
|
const reason = s.reasonFor(u.checkId)
|
|
169
|
+
const off = u.runs.off
|
|
140
170
|
return (
|
|
141
|
-
<Row k="
|
|
171
|
+
<Row k="off in this repo" wide help="where this check runs is its own declaration; the one thing this repo says is that it is OFF here, with why. The switch writes the `off` block of repo.config.ts and leaves the file for you to commit">
|
|
142
172
|
<span className="cwhen">
|
|
143
|
-
<
|
|
144
|
-
{WHEN_VALUES.map((v) => <option key={v} value={v}>{WHEN_OPTION[v]}</option>)}
|
|
145
|
-
</select>
|
|
146
|
-
<Help text={
|
|
147
|
-
<span className="legend">
|
|
148
|
-
{WHEN_VALUES.map((v) => <span key={v} className="lrow"><b>{WHEN_OPTION[v]}</b> ({v}): {TERMS[v]}</span>)}
|
|
149
|
-
</span>
|
|
150
|
-
} />
|
|
173
|
+
{off != null ? <span className="tl red">off: {off}</span> : <span className="dim">runs where it declares</span>}
|
|
151
174
|
<span className="stchip">{SOURCE_CHIP[u.whenSource]}</span>
|
|
152
175
|
<MdHelp text={SOURCE_WORD[u.whenSource]} />
|
|
153
|
-
{
|
|
154
|
-
? <button className="wclear" disabled={s.gateBusy} onClick={() => void s.clearWhen(u.checkId)}>drop the entry</button>
|
|
155
|
-
: null}
|
|
176
|
+
<a href="#" onClick={(e) => { e.preventDefault(); app.showWhen() }}>what runs when</a>
|
|
156
177
|
</span>
|
|
157
|
-
{
|
|
158
|
-
? <span className="cblock">
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
</
|
|
165
|
-
|
|
166
|
-
: null}
|
|
178
|
+
{off != null && u.whenSource === 'off'
|
|
179
|
+
? <span className="cblock"><button disabled={s.gateBusy} onClick={() => void s.turnOn(u.checkId)}>turn it back on</button></span>
|
|
180
|
+
: off == null
|
|
181
|
+
? <span className="cblock">
|
|
182
|
+
<input className="wreason" placeholder="why this repo turns it off (required, it rides in the config)"
|
|
183
|
+
value={reason} onChange={(e) => s.setReason(u.checkId, e.target.value)} />
|
|
184
|
+
<button disabled={s.gateBusy || reason.trim() === ''} onClick={() => void s.setOff(u.checkId)}>turn it off here</button>
|
|
185
|
+
</span>
|
|
186
|
+
: null}
|
|
167
187
|
{g != null && g.state === 'stale'
|
|
168
188
|
? <span className="cblock warnline">the managed block in {g.path} was hand edited: reconcile it, then the switch works again</span>
|
|
169
189
|
: null}
|
|
@@ -321,7 +341,6 @@ export const CheckCard = observer(({ app, id }: { app: RepoSt; id: string }) =>
|
|
|
321
341
|
{whatFacts(u).map((f) => (
|
|
322
342
|
<Row key={f.k} k={f.k} help={f.help}>{f.tone == null ? f.v : <span className={`tl ${f.tone}`}>{f.v}</span>}</Row>
|
|
323
343
|
))}
|
|
324
|
-
<RunWhen app={app} u={u} />
|
|
325
344
|
<Remembered u={u} />
|
|
326
345
|
{/* the help never depends on the second, so it reads the wall
|
|
327
346
|
clock and the row does not repaint on every tick */}
|
|
@@ -338,6 +357,7 @@ export const CheckCard = observer(({ app, id }: { app: RepoSt; id: string }) =>
|
|
|
338
357
|
{u.group == null ? null : <Row k="group" help="a label several checks share, so `--skip <group>` parks all of them at once">{u.group}</Row>}
|
|
339
358
|
<Timings u={u} />
|
|
340
359
|
</div>
|
|
360
|
+
<WhereItRuns app={app} u={u} />
|
|
341
361
|
{u.build && u.checkId === 'test' ? <SlowSuites app={app} /> : null}
|
|
342
362
|
{u.checkId === 'SK014' ? <ProseHits app={app} /> : null}
|
|
343
363
|
<LastOutput app={app} lines={u.lastLines} />
|
|
@@ -49,17 +49,7 @@ export function rememberedFact(u: CachePop): RememberedFact {
|
|
|
49
49
|
const cadence = CADENCE[u.when ?? 'always']
|
|
50
50
|
if (u.never != null)
|
|
51
51
|
return { v: 'never', inputs: [], help: `never cached, so it runs ${cadence}${u.never === '' ? '' : `: ${u.never}`}` }
|
|
52
|
-
return { v: 'never', inputs: [], help: `nothing
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** the four places a check runs, named by what they do. The config keeps its
|
|
56
|
-
* own words (`always`, `release`, `health`, `off`), and these are only what
|
|
57
|
-
* the select under `run when` shows */
|
|
58
|
-
export const WHEN_OPTION: Record<WhenAt, string> = {
|
|
59
|
-
always: 'always',
|
|
60
|
-
release: 'before a release',
|
|
61
|
-
health: 'health pass only',
|
|
62
|
-
off: 'never',
|
|
52
|
+
return { v: 'never', inputs: [], help: `nothing keys it, so it runs ${cadence}. A check file keys it with \`cache: { by: 'files' | 'reads' | 'ttl' | 'fact' }\`` }
|
|
63
53
|
}
|
|
64
54
|
|
|
65
55
|
/** what a check IS: three FACTS, each a short value a reader scans, with the
|
|
@@ -10,8 +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 {
|
|
13
|
+
import type { GateBlock, Resource, ResourceState, SlowSuite, CheckEvent } from './hub-types.ts'
|
|
14
|
+
import { lineText } from '../../core/wire.ts'
|
|
15
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
15
16
|
|
|
16
17
|
/** the colour rule, the SAME one the server applies, because a row must not
|
|
17
18
|
* change meaning between a live result and the poll that confirms it */
|
|
@@ -54,9 +55,6 @@ export class ChecksSvc {
|
|
|
54
55
|
gateBusy = false
|
|
55
56
|
/** the reason box, per check id: a check is only turned off WITH a reason */
|
|
56
57
|
reasons = new Map<string, string>()
|
|
57
|
-
/** checks whose select says never while the reason is still being typed:
|
|
58
|
-
* off is written only once it has one */
|
|
59
|
-
pendingOff = new Set<string>()
|
|
60
58
|
/** the slow suite form under the test check */
|
|
61
59
|
suite: SlowSuite = { file: '', catches: '', whyNotFast: '' }
|
|
62
60
|
|
|
@@ -93,12 +91,12 @@ export class ChecksSvc {
|
|
|
93
91
|
* silent overwrite */
|
|
94
92
|
async loadGate(): Promise<void> {
|
|
95
93
|
try {
|
|
96
|
-
const r = await
|
|
94
|
+
const r = await reach(this.st.url('/api/checks/when'))
|
|
97
95
|
if (!r.ok) throw new Error(await r.text())
|
|
98
96
|
const b = (await r.json()) as GateBlock
|
|
99
97
|
runInAction(() => { this.gate = b })
|
|
100
98
|
} catch (e) {
|
|
101
|
-
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}`) })
|
|
102
100
|
}
|
|
103
101
|
}
|
|
104
102
|
|
|
@@ -113,7 +111,7 @@ export class ChecksSvc {
|
|
|
113
111
|
// the hash of the file the page last read: without it a write
|
|
114
112
|
// could land on top of an edit this page never saw
|
|
115
113
|
if (this.gate == null) await this.loadGate()
|
|
116
|
-
const r = await
|
|
114
|
+
const r = await reach(this.st.url('/api/checks/when'), { method: 'POST', body: JSON.stringify({ ...edit, expect: this.gate?.hash }) })
|
|
117
115
|
if (!r.ok) throw new Error(await r.text())
|
|
118
116
|
const b = (await r.json()) as GateBlock
|
|
119
117
|
runInAction(() => { this.gate = b; this.gateNote = said })
|
|
@@ -122,7 +120,7 @@ export class ChecksSvc {
|
|
|
122
120
|
// has read the config it just wrote
|
|
123
121
|
await this.st.cols.poll(true)
|
|
124
122
|
} catch (e) {
|
|
125
|
-
runInAction(() => { this.gateNote = `refused: ${
|
|
123
|
+
runInAction(() => { this.gateNote = ownTrouble(e, this.st.home.machine, (m) => `refused: ${m}`) })
|
|
126
124
|
// whatever the file really says now, so the next try is not
|
|
127
125
|
// refused for a hash the page has already outgrown
|
|
128
126
|
await this.loadGate()
|
|
@@ -131,37 +129,19 @@ export class ChecksSvc {
|
|
|
131
129
|
return took
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
whenShown(u: { checkId: string; when: WhenAt }): WhenAt {
|
|
137
|
-
return this.pendingOff.has(u.checkId) ? 'off' : u.when
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** the select moved. Every place but never writes at once, and never
|
|
141
|
-
* waits for its reason */
|
|
142
|
-
chooseWhen(checkId: string, at: WhenAt): void {
|
|
143
|
-
if (at === 'off') {
|
|
144
|
-
this.pendingOff.add(checkId)
|
|
145
|
-
this.gateNote = 'never needs a reason: type it, then turn it off'
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
this.pendingOff.delete(checkId)
|
|
149
|
-
void this.setWhen(checkId, at)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
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> {
|
|
153
134
|
const reason = this.reasonFor(checkId).trim()
|
|
154
|
-
if (
|
|
155
|
-
runInAction(() => { this.gateNote = '
|
|
135
|
+
if (reason === '') {
|
|
136
|
+
runInAction(() => { this.gateNote = 'off needs a reason: a check nobody can explain is a silenced gate' })
|
|
156
137
|
return
|
|
157
138
|
}
|
|
158
|
-
await this.writeGate({
|
|
159
|
-
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`)
|
|
160
140
|
}
|
|
161
141
|
|
|
162
|
-
/** back
|
|
163
|
-
async
|
|
164
|
-
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`)
|
|
165
145
|
}
|
|
166
146
|
|
|
167
147
|
async addSlow(): Promise<void> {
|
|
@@ -199,7 +179,7 @@ export class ChecksSvc {
|
|
|
199
179
|
state: stateOf(e),
|
|
200
180
|
severity: e.status === 'fail' ? (e.advisory ? 'yellow' : 'red') : e.status === 'warn' ? 'yellow' : undefined,
|
|
201
181
|
check: { ...u, status: e.status, replayed: e.replayed, lastMs: e.replayed ? null : Math.round(e.ms), lastAt: Date.now(), advisory: e.advisory },
|
|
202
|
-
md: e.lines.length === 0 ? r.md : `\`\`\`\n${e.lines.map((l) => (l.kind === 'fix-fail' || l.kind === 'fix-warn' ? ` fix: ${l
|
|
182
|
+
md: e.lines.length === 0 ? r.md : `\`\`\`\n${e.lines.map((l) => (l.kind === 'fix-fail' || l.kind === 'fix-warn' ? ` fix: ${lineText(l)}` : l.kind === 'fail' ? `FAIL: ${lineText(l)}` : l.kind === 'warn' ? `WARN ${lineText(l)}` : l.kind === 'skip' ? `skip: ${lineText(l)}` : `ok: ${lineText(l)}`)).join('\n')}\n\`\`\``,
|
|
203
183
|
}
|
|
204
184
|
})
|
|
205
185
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// The checks page and the asks page: one dense table each, in the activity
|
|
2
|
+
// page's table style, one line per item under group headers that count their
|
|
3
|
+
// lines. What needs attention comes first; the quiet groups (passing, off,
|
|
4
|
+
// done) fold by default and each viewer's fold choice is kept. The filter
|
|
5
|
+
// input and the severity keys narrow the lines, since both read
|
|
6
|
+
// `visibleInstances`. The checks page opens on the line that counts where
|
|
7
|
+
// its checks run. Groups, order and words: col-tables.ts.
|
|
8
|
+
import { observer } from 'mobx-react-lite'
|
|
9
|
+
import type { ReactNode } from 'react'
|
|
10
|
+
import { checkName } from './check-words.ts'
|
|
11
|
+
import { askGroups, checkGroups, dayMs, findingLine, planWord, type TableGroup } from './col-tables.ts'
|
|
12
|
+
import { durationTone, fmtMs } from './dur.ts'
|
|
13
|
+
import type { CheckOwner } from './hash-route.ts'
|
|
14
|
+
import type { Resource } from './hub-types.ts'
|
|
15
|
+
import { sectionLabel } from './keys.ts'
|
|
16
|
+
import type { RepoSt } from './repo-st.ts'
|
|
17
|
+
import { TimeAgo } from './time-ago.tsx'
|
|
18
|
+
|
|
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')
|
|
20
|
+
|
|
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 }) => (
|
|
24
|
+
<div className="ctpage">
|
|
25
|
+
<div className="cghead">{title}<span className="cnt">{n}</span></div>
|
|
26
|
+
{lead == null || lead === '' ? null : <div className="ctlead">{lead}</div>}
|
|
27
|
+
{n === 0 ? <div className="ph">{empty}</div> : (
|
|
28
|
+
<table className="actable cttable">
|
|
29
|
+
<colgroup>{cols}</colgroup>
|
|
30
|
+
{children}
|
|
31
|
+
</table>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
/** one group: its header line (a click folds it), then its lines unless folded */
|
|
37
|
+
const Group = observer(<K extends string>({ app, col, g, span, row }: { app: RepoSt; col: string; g: TableGroup<K>; span: number; row: (r: Resource) => ReactNode }) => {
|
|
38
|
+
const key = `${col}:${g.key}`
|
|
39
|
+
const folded = app.cols.groupFolded(key, g.foldedByDefault)
|
|
40
|
+
return (
|
|
41
|
+
<tbody className="ctgroup">
|
|
42
|
+
<tr className={`ctgrp g-${g.key}`} data-k={g.key} onClick={() => app.cols.toggleGroup(key, g.foldedByDefault)}>
|
|
43
|
+
<td colSpan={span}><span className="ctcaret">{folded ? '▸' : '▾'}</span>{g.label}</td>
|
|
44
|
+
</tr>
|
|
45
|
+
{folded ? null : g.rows.map(row)}
|
|
46
|
+
</tbody>
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const DOT: Record<string, { tone: string; mark: string }> = {
|
|
51
|
+
fail: { tone: 't-red', mark: '●' },
|
|
52
|
+
warn: { tone: 't-warn', mark: '●' },
|
|
53
|
+
pass: { tone: 't-ok', mark: '●' },
|
|
54
|
+
skip: { tone: 'acdim', mark: '●' },
|
|
55
|
+
never: { tone: 'acdim', mark: '○' },
|
|
56
|
+
off: { tone: 'acdim', mark: '○' },
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const CheckRow = observer(({ app, r }: { app: RepoSt; r: Resource }) => {
|
|
60
|
+
const c = r.check!
|
|
61
|
+
const name = checkName(c.checkId, r.title)
|
|
62
|
+
const dot = c.when === 'off' ? DOT.off! : c.status == null ? DOT.never! : DOT[c.status]!
|
|
63
|
+
const finding = findingLine(c)
|
|
64
|
+
const tone = durationTone(c.lastMs, c.timeoutMs, c.budgetMs)
|
|
65
|
+
return (
|
|
66
|
+
<tr className={`ctrow s-${c.status ?? 'never'}`} data-k={c.checkId} onClick={() => app.cols.select(r.collection, r.id)} title={c.description}>
|
|
67
|
+
<td className={`ctdot ${dot.tone}`}>{dot.mark}</td>
|
|
68
|
+
<td className={`ctid ${c.status === 'fail' ? 't-red' : c.status === 'warn' ? 't-warn' : ''}`}>{c.checkId}</td>
|
|
69
|
+
<td className="cttitle">{name.title === c.checkId ? '' : name.title}</td>
|
|
70
|
+
<td className="acwhen">{c.lastAt == null ? '' : <TimeAgo clock={app.clock} at={c.lastAt} />}</td>
|
|
71
|
+
<td className="ctdur">
|
|
72
|
+
{c.lastMs == null ? null : <span className={`dnum ${tone ?? ''}`}>{fmtMs(c.lastMs)}</span>}
|
|
73
|
+
<span className="ctbudget">{` / ${fmtMs(c.budgetMs)}`}</span>
|
|
74
|
+
</td>
|
|
75
|
+
<td className="ctfind" title={c.lastLines.length > 0 ? c.lastLines.join('\n') : undefined}>{finding}</td>
|
|
76
|
+
<td className="ctplan">{planWord(c)}</td>
|
|
77
|
+
</tr>
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
/** every check, or only the ones one owner ships (`from`) */
|
|
82
|
+
export const CheckTable = observer(({ app, id, from }: { app: RepoSt; id: string; from?: CheckOwner }) => {
|
|
83
|
+
const col = app.cols.sidebarCols.find((x) => x.id === id)
|
|
84
|
+
if (col == null) return <div className="stagewrap"><div className="ph">collection vanished — the truth moved on</div></div>
|
|
85
|
+
const items = app.cols.visibleInstances(col).filter((r) => from == null || r.check?.from === from)
|
|
86
|
+
const cols = (
|
|
87
|
+
<>
|
|
88
|
+
<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" />
|
|
89
|
+
</>
|
|
90
|
+
)
|
|
91
|
+
return (
|
|
92
|
+
// where the checks run, counted over every check whatever the filter
|
|
93
|
+
<Page title={from == null ? col.title : sectionLabel(from)} n={items.length} lead={app.tree.whenLine} cols={cols} empty={emptyWords(app)}>
|
|
94
|
+
{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} />} />)}
|
|
95
|
+
</Page>
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const AskRow = observer(({ app, r }: { app: RepoSt; r: Resource }) => {
|
|
100
|
+
const a = r.ask
|
|
101
|
+
const done = r.id.startsWith('ask:done/')
|
|
102
|
+
const at = dayMs(done ? (a?.closed ?? a?.created) : a?.created)
|
|
103
|
+
const blocks = a == null || a.blocks === '-' ? '' : a.blocks
|
|
104
|
+
return (
|
|
105
|
+
<tr className={`ctrow s-${done ? 'done' : 'open'}`} data-k={r.id} onClick={() => app.cols.select(r.collection, r.id)} title={r.file}>
|
|
106
|
+
<td className={`ctkind ${done ? 'acdim' : ''}`}>{a?.kind ?? r.slot ?? ''}</td>
|
|
107
|
+
<td className={`cttitle ${done ? 'acdim' : ''}`}>{r.title}</td>
|
|
108
|
+
<td className="acwhen">{at == null ? '' : <TimeAgo clock={app.clock} at={at} />}</td>
|
|
109
|
+
<td className="ctblocks" title={blocks === '' ? undefined : blocks}>{blocks}</td>
|
|
110
|
+
{done ? <td /> : a?.probe != null
|
|
111
|
+
? <td className="ctprobe t-ok" title={`closes itself once \`${a.probe}\` exits 0`}>auto close</td>
|
|
112
|
+
: <td className="ctprobe acdim" title="no probe: done by hand">manual</td>}
|
|
113
|
+
</tr>
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
export const AskTable = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
118
|
+
const col = app.cols.sidebarCols.find((x) => x.id === id)
|
|
119
|
+
if (col == null) return <div className="stagewrap"><div className="ph">collection vanished — the truth moved on</div></div>
|
|
120
|
+
const items = app.cols.visibleInstances(col)
|
|
121
|
+
const cols = (
|
|
122
|
+
<>
|
|
123
|
+
<col className="c-kind" /><col /><col className="c-age" /><col className="c-blocks" /><col className="c-probe" />
|
|
124
|
+
</>
|
|
125
|
+
)
|
|
126
|
+
return (
|
|
127
|
+
<Page title={col.title} n={items.length} cols={cols} empty={emptyWords(app)}>
|
|
128
|
+
{askGroups(items).map((g) => <Group key={g.key} app={app} col={col.id} g={g} span={5} row={(r) => <AskRow key={r.id} app={app} r={r} />} />)}
|
|
129
|
+
</Page>
|
|
130
|
+
)
|
|
131
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The checks page and the asks page as dense tables: which group each line
|
|
2
|
+
// belongs to, the order inside a group, and every word a line says. Pure, so
|
|
3
|
+
// a test pins them rather than a screenshot (col-tables-view.tsx draws them).
|
|
4
|
+
import { parseDuration } from '../../core/wire.ts'
|
|
5
|
+
import { spanShort } from '../../plan/plan-timeline.ts'
|
|
6
|
+
import type { CachePop } from './check-words.ts'
|
|
7
|
+
import type { CheckFacts, Resource } from './hub-types.ts'
|
|
8
|
+
|
|
9
|
+
/** one group of lines under its header. `key` is what the viewer's fold
|
|
10
|
+
* choice is remembered under, with the collection before it */
|
|
11
|
+
export type TableGroup<K extends string> = { key: K; label: string; rows: Resource[]; foldedByDefault: boolean }
|
|
12
|
+
|
|
13
|
+
export type CheckGroupKey = 'fail' | 'warn' | 'never' | 'pass' | 'off'
|
|
14
|
+
|
|
15
|
+
/** the groups in the order a reader wants them: what needs attention first */
|
|
16
|
+
const CHECK_GROUPS: { key: CheckGroupKey; word: string; folded: boolean }[] = [
|
|
17
|
+
{ key: 'fail', word: 'failing', folded: false },
|
|
18
|
+
{ key: 'warn', word: 'warning', folded: false },
|
|
19
|
+
{ key: 'never', word: 'never ran', folded: false },
|
|
20
|
+
{ key: 'pass', word: 'passing', folded: true },
|
|
21
|
+
{ key: 'off', word: 'off', folded: true },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
/** a skipped check had nothing to look at, which is nothing to fix: it sits
|
|
25
|
+
* with the passing ones, its dot grey */
|
|
26
|
+
export function checkGroupOf(c: CheckFacts): CheckGroupKey {
|
|
27
|
+
if (c.when === 'off') return 'off'
|
|
28
|
+
if (c.status === 'fail') return 'fail'
|
|
29
|
+
if (c.status === 'warn') return 'warn'
|
|
30
|
+
if (c.status == null) return 'never'
|
|
31
|
+
return 'pass'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const idOf = (r: Resource): string => r.check?.checkId ?? r.id
|
|
35
|
+
|
|
36
|
+
/** most recent first; a check with no result by its id */
|
|
37
|
+
const byRecent = (a: Resource, b: Resource): number => (b.check?.lastAt ?? -1) - (a.check?.lastAt ?? -1) || idOf(a).localeCompare(idOf(b))
|
|
38
|
+
|
|
39
|
+
export function checkGroups(rows: readonly Resource[]): TableGroup<CheckGroupKey>[] {
|
|
40
|
+
const withFacts = rows.filter((r) => r.check != null)
|
|
41
|
+
return CHECK_GROUPS.map((g) => {
|
|
42
|
+
const mine = withFacts.filter((r) => checkGroupOf(r.check!) === g.key).sort(byRecent)
|
|
43
|
+
return { key: g.key, label: `${mine.length} ${g.word}`, rows: mine, foldedByDefault: g.folded }
|
|
44
|
+
}).filter((g) => g.rows.length > 0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** a wire line as plain text: the markdown ticks it is rendered with go */
|
|
48
|
+
const plain = (l: string): string => l.replace(/`/g, '').trim()
|
|
49
|
+
|
|
50
|
+
const escape = (x: string): string => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
51
|
+
|
|
52
|
+
/** a line without what its row already shows: the verdict word (the dot
|
|
53
|
+
* says it) and the check's own id (the id column says it) */
|
|
54
|
+
export function strippedLine(line: string, id: string): string {
|
|
55
|
+
const lead = new RegExp(`^(?:(?:FAIL|WARN|✗|⚠):?\\s*|\\[${escape(id)}\\]\\s*|${escape(id)}(?:\\s+—\\s+|:\\s+|\\s+|$))`)
|
|
56
|
+
let l = line
|
|
57
|
+
for (let m = l.match(lead); m != null && m[0] !== ''; m = l.match(lead)) l = l.slice(m[0].length)
|
|
58
|
+
return l.trim()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** the first line of the last run that says something: a failure or a
|
|
62
|
+
* warning first, else any line that is not the bare `ok: <id>` header */
|
|
63
|
+
export function findingLine(c: CheckFacts): string {
|
|
64
|
+
const lines = c.lastLines.map(plain).filter((l) => l !== '')
|
|
65
|
+
const loud = lines.filter((l) => /^(FAIL|WARN|✗|⚠)/.test(l))
|
|
66
|
+
const rest = lines.filter((l) => !/^(FAIL|WARN|✗|⚠|ok\b|skip\b)/.test(l))
|
|
67
|
+
for (const l of [...loud, ...rest]) {
|
|
68
|
+
const s = strippedLine(l, c.checkId)
|
|
69
|
+
if (s !== '') return s
|
|
70
|
+
}
|
|
71
|
+
return ''
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** how a check is planned, as one short word a column scans: what makes it
|
|
75
|
+
* run again (a file change, a span of time, never again) or how often it
|
|
76
|
+
* runs when it remembers nothing */
|
|
77
|
+
export function planWord(u: CachePop): string {
|
|
78
|
+
if (u.when === 'off') return 'off'
|
|
79
|
+
if ((u.inputs ?? []).length > 0 || u.reads === true) return 'on change'
|
|
80
|
+
if (u.ttl === 'forever') return 'once'
|
|
81
|
+
if (u.ttl != null && u.ttl !== '') {
|
|
82
|
+
try { return `every ${spanShort(parseDuration(u.ttl))}` } catch { return `every ${u.ttl}` }
|
|
83
|
+
}
|
|
84
|
+
return u.when === 'release' ? 'every release' : u.when === 'health' ? 'health pass' : 'every run'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type AskGroupKey = 'open' | 'done'
|
|
88
|
+
|
|
89
|
+
/** a `YYYY-MM-DD` stamp as ms, null when the file carries none */
|
|
90
|
+
export const dayMs = (d: string | null | undefined): number | null => {
|
|
91
|
+
if (d == null) return null
|
|
92
|
+
const t = Date.parse(d)
|
|
93
|
+
return Number.isNaN(t) ? null : t
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const NOTHING = new Set(['', '-'])
|
|
97
|
+
const blocksOf = (r: Resource): string => r.ask?.blocks ?? '-'
|
|
98
|
+
|
|
99
|
+
/** open asks by what they block (an ask that blocks nothing last), the
|
|
100
|
+
* oldest first inside one; done asks after, the last closed first */
|
|
101
|
+
export function askGroups(rows: readonly Resource[]): TableGroup<AskGroupKey>[] {
|
|
102
|
+
const isDone = (r: Resource): boolean => r.id.startsWith('ask:done/')
|
|
103
|
+
const open = rows.filter((r) => !isDone(r)).sort((a, b) => {
|
|
104
|
+
const na = NOTHING.has(blocksOf(a)) ? 1 : 0
|
|
105
|
+
const nb = NOTHING.has(blocksOf(b)) ? 1 : 0
|
|
106
|
+
return na - nb || blocksOf(a).localeCompare(blocksOf(b)) || (dayMs(a.ask?.created) ?? 0) - (dayMs(b.ask?.created) ?? 0) || a.id.localeCompare(b.id)
|
|
107
|
+
})
|
|
108
|
+
const closedAt = (r: Resource): number => dayMs(r.ask?.closed) ?? dayMs(r.ask?.created) ?? 0
|
|
109
|
+
const done = rows.filter(isDone).sort((a, b) => closedAt(b) - closedAt(a) || a.id.localeCompare(b.id))
|
|
110
|
+
const groups: TableGroup<AskGroupKey>[] = [
|
|
111
|
+
{ key: 'open', label: `${open.length} open`, rows: open, foldedByDefault: false },
|
|
112
|
+
{ key: 'done', label: `${done.length} done`, rows: done, foldedByDefault: true },
|
|
113
|
+
]
|
|
114
|
+
return groups.filter((g) => g.rows.length > 0)
|
|
115
|
+
}
|
|
@@ -8,6 +8,21 @@ 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'
|
|
12
|
+
|
|
13
|
+
/** where a viewer's fold choices on the checks and asks tables are kept,
|
|
14
|
+
* one per browser and not per repo: `checks:pass` open is a habit */
|
|
15
|
+
export const TABLE_FOLDS_KEY = 'hub:table-folds'
|
|
16
|
+
|
|
17
|
+
const readFolds = (): Map<string, boolean> => {
|
|
18
|
+
try {
|
|
19
|
+
const v = JSON.parse(globalThis.localStorage?.getItem(TABLE_FOLDS_KEY) ?? '{}') as unknown
|
|
20
|
+
if (v == null || typeof v !== 'object') return new Map()
|
|
21
|
+
return new Map(Object.entries(v).filter((e): e is [string, boolean] => typeof e[1] === 'boolean'))
|
|
22
|
+
} catch {
|
|
23
|
+
return new Map()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
11
26
|
|
|
12
27
|
/** what /api/resources.json answers */
|
|
13
28
|
type Answer = { collections: ColMeta[]; resources: Resource[]; planned?: PlanRow[] | null; hub?: HubProblem[] }
|
|
@@ -43,6 +58,9 @@ export class ColsSvc {
|
|
|
43
58
|
proofDraft = ''
|
|
44
59
|
/** user fold choices win; default = open when the section holds needs */
|
|
45
60
|
foldOverride = new Map<string, boolean>()
|
|
61
|
+
/** the viewer's fold choices on the table groups (`checks:pass`,
|
|
62
|
+
* `asks:done`), true = folded; a group with none folds by its default */
|
|
63
|
+
tableFolds = readFolds()
|
|
46
64
|
private timer: ReturnType<typeof setInterval> | null = null
|
|
47
65
|
|
|
48
66
|
constructor(private st: RepoSt) {
|
|
@@ -115,7 +133,7 @@ export class ColsSvc {
|
|
|
115
133
|
* the server keeps warm */
|
|
116
134
|
async poll(fresh = false): Promise<void> {
|
|
117
135
|
try {
|
|
118
|
-
const r = await
|
|
136
|
+
const r = await reach(this.st.url(fresh ? '/api/resources.json?fresh=1' : '/api/resources.json'))
|
|
119
137
|
if (!r.ok) throw new Error(await r.text())
|
|
120
138
|
const j = (await r.json()) as Answer
|
|
121
139
|
runInAction(() => {
|
|
@@ -131,7 +149,7 @@ export class ColsSvc {
|
|
|
131
149
|
} catch (e) {
|
|
132
150
|
// a dead server must SAY so, in the header, never leave a tree
|
|
133
151
|
// that reads like a healthy repo. The last rows stay, dated by it
|
|
134
|
-
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 })
|
|
135
153
|
}
|
|
136
154
|
}
|
|
137
155
|
|
|
@@ -177,6 +195,17 @@ export class ColsSvc {
|
|
|
177
195
|
openSection(c: Col): void {
|
|
178
196
|
this.foldOverride.set(c.id, true)
|
|
179
197
|
}
|
|
198
|
+
/** a table group is folded: a live search shows every match, and a group
|
|
199
|
+
* the viewer never touched folds by its default */
|
|
200
|
+
groupFolded(key: string, byDefault: boolean): boolean {
|
|
201
|
+
if (this.st.query.trim() !== '') return false
|
|
202
|
+
return this.tableFolds.get(key) ?? byDefault
|
|
203
|
+
}
|
|
204
|
+
toggleGroup(key: string, byDefault: boolean): void {
|
|
205
|
+
if (this.st.query.trim() !== '') return
|
|
206
|
+
this.tableFolds.set(key, !this.groupFolded(key, byDefault))
|
|
207
|
+
try { globalThis.localStorage?.setItem(TABLE_FOLDS_KEY, JSON.stringify(Object.fromEntries(this.tableFolds))) } catch { /* private window: the choice lasts the tab */ }
|
|
208
|
+
}
|
|
180
209
|
/** rail red dot — the section holds at least one red item */
|
|
181
210
|
hasRed(c: Col): boolean {
|
|
182
211
|
return c.instances.some((i) => i.severity === 'red')
|
|
@@ -209,12 +238,12 @@ export class ColsSvc {
|
|
|
209
238
|
private async act(path: string, body: Record<string, unknown>): Promise<boolean> {
|
|
210
239
|
this.actErr = ''
|
|
211
240
|
try {
|
|
212
|
-
const r = await
|
|
241
|
+
const r = await reach(this.st.url(`/api/act/${path}`), { method: 'POST', body: JSON.stringify(body) })
|
|
213
242
|
if (!r.ok) throw new Error(await r.text())
|
|
214
243
|
await this.poll(true)
|
|
215
244
|
return true
|
|
216
245
|
} catch (e) {
|
|
217
|
-
runInAction(() => { this.actErr = (e
|
|
246
|
+
runInAction(() => { this.actErr = ownTrouble(e, this.st.home.machine) })
|
|
218
247
|
return false
|
|
219
248
|
}
|
|
220
249
|
}
|