rvlib-shipkit 1.5.0 → 1.7.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 +54 -0
- package/package.json +1 -1
- package/src/assets/gen/comfyts-gen.ts +12 -8
- package/src/assets/server/studio-server.ts +14 -2
- package/src/check.ts +5 -8
- package/src/checks/SK001-package-json-shape.check.ts +5 -4
- package/src/checks/SK003-claude-md-shape.check.ts +2 -2
- 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_Assets.ts +10 -6
- package/src/cli/cmd_CacheAudit.ts +43 -0
- package/src/cli/cmd_CrossRepoActivity.ts +2 -1
- package/src/cli/cmd_GlobalProcesses.ts +4 -3
- package/src/cli/cmd_Home.ts +3 -2
- package/src/cli/cmd_Insights.ts +78 -0
- package/src/cli/cmd_Keys.ts +12 -1
- package/src/cli/cmd_Overview.ts +1 -1
- package/src/cli/cmd_Serve.ts +2 -1
- package/src/cli/cmd_TraceAudit.ts +62 -0
- package/src/cli.ts +6 -0
- package/src/collections/collection-types.ts +3 -0
- 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 +1 -1
- package/src/core/check-ctx.ts +14 -5
- package/src/core/check-runner.ts +6 -3
- 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/one-lib.ts +59 -4
- package/src/core/read-set.ts +97 -9
- package/src/core/run-ci.ts +6 -1
- package/src/core/run-fixers.ts +8 -2
- package/src/core/trace-audit.ts +129 -0
- package/src/home/gui-app/ShipkitGui.swift +125 -41
- package/src/home/gui-app.ts +24 -27
- package/src/home/home-routes.ts +15 -0
- package/src/home/home-start.ts +11 -5
- package/src/home/insights-shape.ts +124 -0
- package/src/home/insights.ts +90 -0
- package/src/home/open-app.ts +2 -2
- package/src/home/serve-web.ts +3 -3
- package/src/home/via-hub.ts +14 -0
- package/src/home/widgets.ts +12 -2
- package/src/ledger/db.ts +2 -13
- package/src/ledger/gate-checks.ts +21 -3
- package/src/ledger/git-key.ts +148 -30
- package/src/ledger/git-scan.ts +5 -2
- package/src/ledger/ledger.ts +131 -16
- package/src/ledger/store.ts +14 -0
- package/src/ledger/toolchain-identity.ts +210 -25
- package/src/problems/extract.ts +3 -0
- package/src/repo-config.ts +18 -1
- package/src/serve/hub-app/admin-svc.ts +36 -3
- package/src/serve/hub-app/admin-view.tsx +33 -1
- package/src/serve/hub-app/col-card.tsx +1 -44
- package/src/serve/hub-app/col-tables-view.tsx +71 -5
- package/src/serve/hub-app/col-tables.ts +27 -0
- package/src/serve/hub-app/home-st.ts +13 -6
- package/src/serve/hub-app/home-strip.css +7 -9
- package/src/serve/hub-app/home-strip.tsx +8 -17
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +20 -4
- package/src/serve/hub-app/hub.css +24 -20
- package/src/serve/hub-app/icons.tsx +1 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/insights-st.ts +111 -0
- package/src/serve/hub-app/insights-view.tsx +112 -0
- package/src/serve/hub-app/insights.css +24 -0
- package/src/serve/hub-app/keys.ts +11 -3
- package/src/serve/hub-app/machine-st.ts +23 -0
- package/src/serve/hub-app/palette-st.ts +2 -1
- package/src/serve/hub-app/processes-view.tsx +16 -9
- package/src/serve/hub-app/project-list.css +4 -1
- package/src/serve/hub-app/project-list.tsx +9 -5
- package/src/serve/hub-app/projects-svc.ts +11 -1
- package/src/serve/hub-app/repo-sections.ts +31 -17
- package/src/serve/hub-app/repo-st.ts +25 -6
- package/src/serve/hub-app/sidebar.tsx +5 -5
- package/src/serve/hub-app/tree-build.ts +2 -0
- package/src/serve/hub-app/tree-row.tsx +18 -5
- package/src/serve/hub-app/tree-svc.ts +27 -7
- package/src/serve/hub-runtime.ts +4 -2
- package/src/serve/project-gutter-route.ts +22 -1
- package/src/shipkit-files.ts +3 -2
- 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
|
@@ -1,17 +1,50 @@
|
|
|
1
1
|
// A repo's admin page, as state: what administers the repo rather than works
|
|
2
|
-
// in it. Archive asks first (the confirmation is this state, so it survives a
|
|
2
|
+
// in it. Its identity colours, with the app icon they give it. Archive asks first (the confirmation is this state, so it survives a
|
|
3
3
|
// re-render and a test drives it headless), reload and keep active go to the
|
|
4
4
|
// machine by the repo's key. On a hub of its own there is no machine, and no
|
|
5
5
|
// admin page.
|
|
6
|
-
import { makeAutoObservable } from 'mobx'
|
|
6
|
+
import { makeAutoObservable, runInAction } from 'mobx'
|
|
7
|
+
import type { Identity } from '../../core/identity-color.ts'
|
|
7
8
|
import type { RepoSt } from './repo-st.ts'
|
|
8
9
|
|
|
10
|
+
/** how long a picker rests before the preview draws its colours */
|
|
11
|
+
export const PREVIEW_REST_MS = 150
|
|
12
|
+
|
|
9
13
|
export class AdminSvc {
|
|
10
14
|
/** the archive button was pressed: its confirmation shows in its place */
|
|
11
15
|
confirming = false
|
|
16
|
+
/** the colours the preview keeps while a picker moves, so a drag draws a
|
|
17
|
+
* few icons, not one per frame; null follows the repo's colours */
|
|
18
|
+
held: Identity | null = null
|
|
19
|
+
/** the repo has no png mark, or it would not draw: the app wears the default icon */
|
|
20
|
+
noIcon = false
|
|
21
|
+
private rest: ReturnType<typeof setTimeout> | null = null
|
|
12
22
|
|
|
13
23
|
constructor(private st: RepoSt) {
|
|
14
|
-
makeAutoObservable<this, 'st'>(this, { st: false })
|
|
24
|
+
makeAutoObservable<this, 'st' | 'rest'>(this, { st: false, rest: false })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** by key: the project list and the icon route know a repo by its
|
|
28
|
+
* folder, and its config can name it otherwise */
|
|
29
|
+
get identity(): Identity {
|
|
30
|
+
return this.st.home.projects.identityOf(this.st.key)
|
|
31
|
+
}
|
|
32
|
+
/** one colour, shown at once and saved to repo.config.ts once it rests */
|
|
33
|
+
setColour(patch: Partial<Identity>): void {
|
|
34
|
+
this.held ??= this.identity
|
|
35
|
+
this.st.home.projects.setIdentity(this.st.key, patch)
|
|
36
|
+
if (this.rest != null) clearTimeout(this.rest)
|
|
37
|
+
this.rest = setTimeout(() => runInAction(() => { this.held = null }), PREVIEW_REST_MS)
|
|
38
|
+
}
|
|
39
|
+
get previewSrc(): string {
|
|
40
|
+
const id = this.held ?? this.identity
|
|
41
|
+
return `/api/projects/app-icon/${encodeURIComponent(this.st.key)}?light=${encodeURIComponent(id.colorLight)}&dark=${encodeURIComponent(id.colorDark)}`
|
|
42
|
+
}
|
|
43
|
+
previewFailed(): void {
|
|
44
|
+
this.noIcon = true
|
|
45
|
+
}
|
|
46
|
+
previewLoaded(): void {
|
|
47
|
+
this.noIcon = false
|
|
15
48
|
}
|
|
16
49
|
|
|
17
50
|
askArchive(): void {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// THE ADMIN PAGE of a repo (`#page:admin`): what administers the repo rather
|
|
2
|
-
// than works in it
|
|
2
|
+
// than works in it. Its identity first (the colours, and the app icon they
|
|
3
|
+
// draw, core/app-icon.ts), then one row per control, each with the one sentence that says
|
|
3
4
|
// what it does. State in admin-svc.ts. Only on the home: a hub of its own has
|
|
4
5
|
// no machine to administer it.
|
|
5
6
|
import { observer } from 'mobx-react-lite'
|
|
@@ -16,6 +17,36 @@ const Row = ({ title, says, children }: { title: string; says: string; children:
|
|
|
16
17
|
</div>
|
|
17
18
|
)
|
|
18
19
|
|
|
20
|
+
/** the repo's two colours, and what they look like: its app icon and its name on both pages */
|
|
21
|
+
const IdentityBox = observer(({ app }: { app: RepoSt }) => {
|
|
22
|
+
const a = app.admin
|
|
23
|
+
const id = a.identity
|
|
24
|
+
const name = app.name
|
|
25
|
+
return (
|
|
26
|
+
<div className="adbox adid">
|
|
27
|
+
<div className="adicon">
|
|
28
|
+
<img src={a.previewSrc} alt={`${name} app icon`} width={128} height={128} hidden={a.noIcon}
|
|
29
|
+
onLoad={() => a.previewLoaded()} onError={() => a.previewFailed()} />
|
|
30
|
+
{a.noIcon ? <span className="dim">no png mark (.shipkit/identity/icon.png, public/logo.png or public/favicon.png): its app wears the default icon</span> : null}
|
|
31
|
+
</div>
|
|
32
|
+
<div className="adtext">
|
|
33
|
+
<b>identity</b>
|
|
34
|
+
<span className="dim">the repo's two colours, written to repo.config.ts as identity. The app icon sets the mark on the one it stands out on most, and each app redraws it at its next open.</span>
|
|
35
|
+
<label className="adpick">
|
|
36
|
+
<input type="color" value={id.colorDark} onChange={(e) => a.setColour({ colorDark: e.currentTarget.value })} />
|
|
37
|
+
<span className="adsample dark" style={{ color: id.colorDark }}>{name}</span>
|
|
38
|
+
<span className="dim">on a dark page, colorDark {id.colorDark}</span>
|
|
39
|
+
</label>
|
|
40
|
+
<label className="adpick">
|
|
41
|
+
<input type="color" value={id.colorLight} onChange={(e) => a.setColour({ colorLight: e.currentTarget.value })} />
|
|
42
|
+
<span className="adsample light" style={{ color: id.colorLight }}>{name}</span>
|
|
43
|
+
<span className="dim">on a light page, colorLight {id.colorLight}</span>
|
|
44
|
+
</label>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
19
50
|
export const AdminPage = observer(({ app }: { app: RepoSt }) => {
|
|
20
51
|
const a = app.admin
|
|
21
52
|
const w = app.home.machine?.worker(app.key)
|
|
@@ -26,6 +57,7 @@ export const AdminPage = observer(({ app }: { app: RepoSt }) => {
|
|
|
26
57
|
<span className="tid">admin</span>
|
|
27
58
|
<h3 className="ctitle">{name}</h3>
|
|
28
59
|
</div>
|
|
60
|
+
<IdentityBox app={app} />
|
|
29
61
|
<div className="adbox">
|
|
30
62
|
<Row title="reload the hub" says={`stops ${name}'s hub in the daemon and loads it again, its repo.config.ts read fresh and its processes started again. The daemon already does it on its own when repo.config.ts changes: this is for a hub that is stuck.${w == null ? '' : ` Its hub is ${w.status} now.`}`}>
|
|
31
63
|
<button className="ghost" onClick={() => void a.reload()}>reload</button>
|
|
@@ -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()
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { observer } from 'mobx-react-lite'
|
|
9
9
|
import type { ReactNode } from 'react'
|
|
10
10
|
import { checkName } from './check-words.ts'
|
|
11
|
-
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'
|
|
12
12
|
import { durationTone, fmtMs } from './dur.ts'
|
|
13
13
|
import type { CheckOwner } from './hash-route.ts'
|
|
14
14
|
import type { Resource } from './hub-types.ts'
|
|
@@ -25,10 +25,12 @@ const Page = ({ title, n, lead, cols, empty, children }: { title: string; n: num
|
|
|
25
25
|
<div className="cghead">{title}<span className="cnt">{n}</span></div>
|
|
26
26
|
{lead == null || lead === '' ? null : <div className="ctlead">{lead}</div>}
|
|
27
27
|
{n === 0 ? <div className="ph">{empty}</div> : (
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
<div className="ctbox">
|
|
29
|
+
<table className="actable cttable">
|
|
30
|
+
<colgroup>{cols}</colgroup>
|
|
31
|
+
{children}
|
|
32
|
+
</table>
|
|
33
|
+
</div>
|
|
32
34
|
)}
|
|
33
35
|
</div>
|
|
34
36
|
)
|
|
@@ -129,3 +131,67 @@ export const AskTable = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
|
129
131
|
</Page>
|
|
130
132
|
)
|
|
131
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
|
+
}
|
|
@@ -18,6 +18,7 @@ import { DockSt } from './dock-st.ts'
|
|
|
18
18
|
import { PaletteSt } from './palette-st.ts'
|
|
19
19
|
import { PlannedSt } from './planned-st.ts'
|
|
20
20
|
import { ActivitySt } from './activity-st.ts'
|
|
21
|
+
import { InsightsSt } from './insights-st.ts'
|
|
21
22
|
import { ProjectsSvc } from './projects-svc.ts'
|
|
22
23
|
import { parseFilter, RepoSt } from './repo-st.ts'
|
|
23
24
|
import { openSection, repoSections, sectionOfRepo } from './repo-sections.ts'
|
|
@@ -25,11 +26,11 @@ import { openSection, repoSections, sectionOfRepo } from './repo-sections.ts'
|
|
|
25
26
|
/** the longest one repo's read ahead holds the next one back */
|
|
26
27
|
export const PREFETCH_WAIT_MS = 10_000
|
|
27
28
|
|
|
28
|
-
/** what the page shows: the machine overview, the plan, the activity, or one repo */
|
|
29
|
-
export type Page = 'overview' | 'planned' | 'activity' | 'archived' | 'repo'
|
|
29
|
+
/** what the page shows: the machine overview, the plan, the activity, the insights, the archived repos, or one repo */
|
|
30
|
+
export type Page = 'overview' | 'planned' | 'activity' | 'insights' | 'archived' | 'repo'
|
|
30
31
|
|
|
31
32
|
/** the address of each page of the home that is no repo's */
|
|
32
|
-
export const PLACE_PATH = { overview: '/', planned: '/planned', activity: '/activity', archived: '/archived' } as const
|
|
33
|
+
export const PLACE_PATH = { overview: '/', planned: '/planned', activity: '/activity', insights: '/insights', archived: '/archived' } as const
|
|
33
34
|
|
|
34
35
|
/** where a repo's hub answers on the home */
|
|
35
36
|
export const repoBase = (name: string): string => `/r/${encodeURIComponent(name)}`
|
|
@@ -46,6 +47,7 @@ export function routeOf(pathname: string): { page: Page; repo: string | null } {
|
|
|
46
47
|
if (m != null) return { page: 'repo', repo: decodeURIComponent(m[1]!) }
|
|
47
48
|
if (/^\/planned\/?$/.test(pathname)) return { page: 'planned', repo: null }
|
|
48
49
|
if (/^\/activity\/?$/.test(pathname)) return { page: 'activity', repo: null }
|
|
50
|
+
if (/^\/insights\/?$/.test(pathname)) return { page: 'insights', repo: null }
|
|
49
51
|
if (/^\/archived\/?$/.test(pathname)) return { page: 'archived', repo: null }
|
|
50
52
|
return { page: 'overview', repo: null }
|
|
51
53
|
}
|
|
@@ -69,6 +71,7 @@ export class HomeSt {
|
|
|
69
71
|
planned: PlannedSt
|
|
70
72
|
/** what every repo committed and holds, polled while it is the page */
|
|
71
73
|
activity: ActivitySt
|
|
74
|
+
insights: InsightsSt
|
|
72
75
|
/** the one picker, cmd+j */
|
|
73
76
|
palette: PaletteSt
|
|
74
77
|
/** the widths of the project list and a repo's sidebar */
|
|
@@ -91,12 +94,13 @@ export class HomeSt {
|
|
|
91
94
|
this.projects = new ProjectsSvc()
|
|
92
95
|
this.planned = new PlannedSt(this)
|
|
93
96
|
this.activity = new ActivitySt(this)
|
|
97
|
+
this.insights = new InsightsSt(this)
|
|
94
98
|
this.palette = new PaletteSt(this)
|
|
95
99
|
this.layout = new LayoutSt()
|
|
96
100
|
this.dock = new DockSt(this)
|
|
97
101
|
this.machine = o.home ? new MachineSt(this, o) : null
|
|
98
102
|
this.page = o.home ? 'overview' : 'repo'
|
|
99
|
-
makeAutoObservable<this, 'disposers' | 'onUrl' | 'prefetching'>(this, { machine: false, projects: false, planned: false, activity: false, palette: false, layout: false, dock: false, repos: observableShallow, disposers: false, onUrl: false, prefetching: false })
|
|
103
|
+
makeAutoObservable<this, 'disposers' | 'onUrl' | 'prefetching'>(this, { machine: false, projects: false, planned: false, activity: false, insights: false, palette: false, layout: false, dock: false, repos: observableShallow, disposers: false, onUrl: false, prefetching: false })
|
|
100
104
|
const m = this.machine
|
|
101
105
|
if (m != null) {
|
|
102
106
|
this.projects.onSelect = (name) => this.select(name)
|
|
@@ -129,6 +133,7 @@ export class HomeSt {
|
|
|
129
133
|
if (this.page === 'overview') return 'home'
|
|
130
134
|
if (this.page === 'planned') return 'planned'
|
|
131
135
|
if (this.page === 'activity') return 'activity'
|
|
136
|
+
if (this.page === 'insights') return 'insights'
|
|
132
137
|
if (this.page === 'archived') return 'archived'
|
|
133
138
|
const r = this.shown
|
|
134
139
|
return r == null || r.name === '' ? 'hub' : `${r.name} · hub`
|
|
@@ -181,7 +186,7 @@ export class HomeSt {
|
|
|
181
186
|
const want = from == null || from === to ? null : sectionOfRepo(from)
|
|
182
187
|
if (want != null && sectionOfRepo(to) !== want) {
|
|
183
188
|
const known = to.cols.loaded || to.cols.cached
|
|
184
|
-
openSection(to, known && !repoSections(to).some((s) => s.id === want) ? 'readme' : want)
|
|
189
|
+
openSection(to, known && !repoSections(to).some((s) => s.id === want && s.opens) ? 'readme' : want)
|
|
185
190
|
}
|
|
186
191
|
this.show('repo', name, 'push')
|
|
187
192
|
}
|
|
@@ -222,6 +227,7 @@ export class HomeSt {
|
|
|
222
227
|
* everything; the one shown resumes once its hub answers */
|
|
223
228
|
private show(page: Page, repo: string | null, how: 'push' | 'replace' | 'none'): void {
|
|
224
229
|
const prev = this.shown
|
|
230
|
+
const prevPage = this.page
|
|
225
231
|
const next = page === 'repo' && repo != null ? this.repo(repo) : null
|
|
226
232
|
// the page already shown is never a second history entry
|
|
227
233
|
if (how === 'push' && page === this.page && next === prev && page !== 'planned') how = 'none'
|
|
@@ -234,12 +240,13 @@ export class HomeSt {
|
|
|
234
240
|
this.projects.away = next == null
|
|
235
241
|
this.projects.place = page === 'repo' || page === 'archived' ? null : page
|
|
236
242
|
if (how !== 'none') {
|
|
237
|
-
const to = next != null ? `${repoPage(next.key)}${next.search}${next.hash}` : page === 'planned' ? this.planned.address : page === 'activity' || page === 'archived' ? PLACE_PATH[page] : '/'
|
|
243
|
+
const to = next != null ? `${repoPage(next.key)}${next.search}${next.hash}` : page === 'planned' ? this.planned.address : page === 'activity' || page === 'insights' || page === 'archived' ? PLACE_PATH[page] : '/'
|
|
238
244
|
if (how === 'push') history.pushState(null, '', to)
|
|
239
245
|
else history.replaceState(null, '', to)
|
|
240
246
|
}
|
|
241
247
|
if (page === 'planned') this.planned.start()
|
|
242
248
|
else this.planned.stop()
|
|
249
|
+
if (page === 'insights' && prevPage !== 'insights') this.insights.start()
|
|
243
250
|
if (page === 'activity') this.activity.start()
|
|
244
251
|
else this.activity.stop()
|
|
245
252
|
if (next != null && next !== prev && this.machine?.data != null) void this.machine.hub(next.key, 'ensure')
|
|
@@ -6,10 +6,6 @@
|
|
|
6
6
|
.hrepo{display:inline-flex;align-items:center;gap:var(--s2);min-width:0;flex:0 1 auto}
|
|
7
7
|
.hrepo img,.hrico{flex:none;width:18px;height:18px;border-radius:4px;object-fit:contain}
|
|
8
8
|
.hrname{font-size:13px;font-weight:650;letter-spacing:.04em;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
9
|
-
.hswatch{display:inline-flex;gap:2px}
|
|
10
|
-
.hswatch input{width:16px;height:16px;padding:0;border:1px solid var(--line);border-radius:3px;background:none;cursor:pointer}
|
|
11
|
-
.hswatch input::-webkit-color-swatch-wrapper{padding:0}
|
|
12
|
-
.hswatch input::-webkit-color-swatch{border:none;border-radius:2px}
|
|
13
9
|
/* trouble, as cards (float-cards.tsx), never taking room, so one coming or
|
|
14
10
|
going moves nothing: at the top right, in a zero height box right after
|
|
15
11
|
the bar, so just under it whatever its height. The words wrap, never cut */
|
|
@@ -61,6 +57,8 @@
|
|
|
61
57
|
.hwidget{flex:0 1 auto;max-width:100%;padding:var(--s0) var(--s2);border:1px solid var(--line);border-radius:3px;font-size:12px;line-height:1.4;overflow:auto}
|
|
62
58
|
.hwidget h3{margin:0;font-size:12px;font-weight:650;white-space:nowrap}
|
|
63
59
|
.hwidget h3 .hdim{font-weight:400}
|
|
60
|
+
.hwidget h3 .hwre{width:16px;height:16px;margin-left:var(--s1)}
|
|
61
|
+
.hwidget h3 .hwre .hico{width:12px;height:12px}
|
|
64
62
|
/* meters in two columns: <table class="wmeters">, label · bar · value, twice a row */
|
|
65
63
|
.wmeters td{padding-right:var(--s1)}
|
|
66
64
|
.wmeters td:nth-child(3n){padding-right:var(--s3)}
|
|
@@ -156,8 +154,8 @@
|
|
|
156
154
|
.wbar.hi > i{background:var(--red)}
|
|
157
155
|
|
|
158
156
|
/* a process card on the processes page: the menu's two panels at page width */
|
|
159
|
-
.
|
|
160
|
-
.
|
|
161
|
-
.
|
|
162
|
-
.
|
|
163
|
-
.
|
|
157
|
+
.pdetail .hmacts{max-width:none}
|
|
158
|
+
.pdetail .hmabout{max-width:none;max-height:none}
|
|
159
|
+
.pdetail>.cstitle{margin:var(--s3) 0 var(--s1)}
|
|
160
|
+
.pdetail>.cstitle:first-child{margin-top:0}
|
|
161
|
+
.pdetail .hlog{margin-top:0}
|
|
@@ -157,9 +157,10 @@ const ProcessRepo = observer(({ app, name, icon }: { app: HomeSt; name: string;
|
|
|
157
157
|
/** the processes as one table in the activity page's style: shipkit's own
|
|
158
158
|
* first, then a row group per declaring repo in the project list's order,
|
|
159
159
|
* its name in the first column, at the top of its group, then one line per
|
|
160
|
-
* process. The url's header carries the new window button
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
* process. The url's header carries the new window button. `repo` keeps
|
|
161
|
+
* that one repo's group, the table of its processes page */
|
|
162
|
+
export const ProcessTable = observer(({ app, m, repo }: { app: HomeSt; m: MachineSt; repo?: string }) => {
|
|
163
|
+
const groups = repo == null ? m.processGroups : m.processGroups.filter((g) => g.core !== true && g.repo === repo)
|
|
163
164
|
const rows = groups.flatMap((g) => g.rows)
|
|
164
165
|
// wide enough for the longest set of declared actions, in the button font
|
|
165
166
|
const act = Math.max(0, ...rows.map((r) => r.actions.reduce((n, a) => n + a.label.length + 3, 0)))
|
|
@@ -210,6 +211,10 @@ const Monitor = observer(({ m }: { m: MachineSt }) => {
|
|
|
210
211
|
{x.title}
|
|
211
212
|
{head == null ? null : <span className="hdim"> · <span dangerouslySetInnerHTML={{ __html: head }} /></span>}
|
|
212
213
|
{x.error != null && x.html != null ? <i className="gpwarn" title={x.error} /> : null}
|
|
214
|
+
<button className="gpi hwre" aria-label="refresh" disabled={m.refreshing.has(`${x.repo}/${x.id}`)} onClick={() => void m.refreshWidget(x.repo, x.id)}
|
|
215
|
+
title="render it again now, skipping any wait it keeps between samples">
|
|
216
|
+
{m.refreshing.has(`${x.repo}/${x.id}`) ? <span className="spin" /> : <Icon d={ICON.restart} />}
|
|
217
|
+
</button>
|
|
213
218
|
</h3>
|
|
214
219
|
{x.html != null ? <div dangerouslySetInnerHTML={{ __html: body }} />
|
|
215
220
|
: x.error != null ? <div className="hwerr">{x.error}</div>
|
|
@@ -225,19 +230,6 @@ const Monitor = observer(({ m }: { m: MachineSt }) => {
|
|
|
225
230
|
)
|
|
226
231
|
})
|
|
227
232
|
|
|
228
|
-
/** the two colours of a repo, a picker each: the page draws the dark one */
|
|
229
|
-
export const IdentitySwatches = observer(({ app, name }: { app: HomeSt; name: string }) => {
|
|
230
|
-
const id = app.projects.identityOf(name)
|
|
231
|
-
return (
|
|
232
|
-
<span className="hswatch">
|
|
233
|
-
<input type="color" aria-label="colour on a dark background" title={`${name} on a dark background (this page): ${id.colorDark}, written to repo.config.ts identity.colorDark`}
|
|
234
|
-
value={id.colorDark} onChange={(e) => app.projects.setIdentity(name, { colorDark: e.currentTarget.value })} />
|
|
235
|
-
<input type="color" aria-label="colour on a light background" title={`${name} on a light background: ${id.colorLight}, written to repo.config.ts identity.colorLight`}
|
|
236
|
-
value={id.colorLight} onChange={(e) => app.projects.setIdentity(name, { colorLight: e.currentTarget.value })} />
|
|
237
|
-
</span>
|
|
238
|
-
)
|
|
239
|
-
})
|
|
240
|
-
|
|
241
233
|
/** the shown repo in the strip: its icon, its name in its colour */
|
|
242
234
|
const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
|
|
243
235
|
const p = app.projects.list.find((x) => x.name === name)
|
|
@@ -247,7 +239,6 @@ const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
|
|
|
247
239
|
{/* the icon's box is there before the project list is, so the name never moves */}
|
|
248
240
|
{p?.icon != null ? <img src={p.icon} alt="" width={18} height={18} /> : <i className="hrico" />}
|
|
249
241
|
<b className="hrname" style={{ color: id.colorDark }}>{name}</b>
|
|
250
|
-
<IdentitySwatches app={app} name={name} />
|
|
251
242
|
</span>
|
|
252
243
|
)
|
|
253
244
|
})
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
<link rel="stylesheet" href="./planned.css" />
|
|
12
12
|
<link rel="stylesheet" href="./when.css" />
|
|
13
13
|
<link rel="stylesheet" href="./activity.css" />
|
|
14
|
+
<link rel="stylesheet" href="./insights.css" />
|
|
14
15
|
<link rel="stylesheet" href="./readme.css" />
|
|
15
16
|
<link rel="stylesheet" href="./dock.css" />
|
|
16
17
|
<link rel="stylesheet" href="./controls.css" />
|
|
@@ -11,8 +11,8 @@ import { runInAction } from 'mobx'
|
|
|
11
11
|
import { observer } from 'mobx-react-lite'
|
|
12
12
|
import { StrictMode, useEffect, useLayoutEffect, useRef } from 'react'
|
|
13
13
|
import { createRoot } from 'react-dom/client'
|
|
14
|
-
import { ColCard,
|
|
15
|
-
import { AskTable, CheckTable } from './col-tables-view.tsx'
|
|
14
|
+
import { ColCard, StatusCard } from './col-card.tsx'
|
|
15
|
+
import { AskTable, CheckTable, ItemTable, SecretTable } from './col-tables-view.tsx'
|
|
16
16
|
import { WIKIS, WikisView } from './wikis-view.tsx'
|
|
17
17
|
import { ComfyOverlay, ComfyView } from './comfy-view.tsx'
|
|
18
18
|
import type { Filter, RepoSt } from './repo-st.ts'
|
|
@@ -37,6 +37,7 @@ import { LIST, SIDEBAR } from './layout-st.ts'
|
|
|
37
37
|
import { ArchivedView, HomeStrip, NotUp, Overview } from './home-strip.tsx'
|
|
38
38
|
import { PlannedView } from './planned-view.tsx'
|
|
39
39
|
import { ActivityView } from './activity-view.tsx'
|
|
40
|
+
import { InsightsView } from './insights-view.tsx'
|
|
40
41
|
import { Palette } from './palette-view.tsx'
|
|
41
42
|
import { SCREEN_WAIT_MS } from './screen-cache.ts'
|
|
42
43
|
import { isSearchKey, pageKey, sectionKey } from './keys.ts'
|
|
@@ -93,7 +94,7 @@ const Main = observer(({ app }: { app: RepoSt }) => {
|
|
|
93
94
|
if (app.sel?.t === 'col') return (
|
|
94
95
|
<main>
|
|
95
96
|
{errs}
|
|
96
|
-
{app.sel.id === 'checks' ? <CheckTable app={app} id={app.sel.id} /> : app.sel.id === 'asks' ? <AskTable app={app} id={app.sel.id} /> : <
|
|
97
|
+
{app.sel.id === 'checks' ? <CheckTable app={app} id={app.sel.id} /> : app.sel.id === 'asks' ? <AskTable app={app} id={app.sel.id} /> : app.sel.id === 'creds' ? <SecretTable app={app} id={app.sel.id} /> : <ItemTable app={app} id={app.sel.id} />}
|
|
97
98
|
</main>
|
|
98
99
|
)
|
|
99
100
|
if (app.sel?.t === 'tests') return <main>{errs}<div className="stagewrap"><TestsPage app={app} check={app.sel.check} /></div></main>
|
|
@@ -195,13 +196,18 @@ const RepoView = observer(({ app }: { app: RepoSt }) => {
|
|
|
195
196
|
function onKey(e: KeyboardEvent): void {
|
|
196
197
|
// the command keys beat the focus check: they work from anywhere, any page
|
|
197
198
|
const pk = pageKey(e)
|
|
198
|
-
|
|
199
|
+
// in a text field ⌘↑ and ⌘↓ keep moving the caret to its ends
|
|
200
|
+
const typing = (e.target as HTMLElement).tagName === 'INPUT' || (e.target as HTMLElement).tagName === 'TEXTAREA'
|
|
201
|
+
if (pk != null && !(pk.t === 'step' && typing)) {
|
|
199
202
|
e.preventDefault()
|
|
200
203
|
if (pk.t === 'picker') home.palette.toggle()
|
|
201
204
|
else if (pk.t === 'dock') home.dock.cycle()
|
|
202
205
|
else if (pk.t === 'home' || pk.t === 'planned' || pk.t === 'activity') {
|
|
203
206
|
home.palette.close()
|
|
204
207
|
if (home.machine != null) home.navigate(PLACE_PATH[pk.t === 'home' ? 'overview' : pk.t])
|
|
208
|
+
} else if (pk.t === 'step') {
|
|
209
|
+
home.palette.close()
|
|
210
|
+
home.projects.step(pk.by)
|
|
205
211
|
} else {
|
|
206
212
|
const p = home.projects.list[pk.n - 1]
|
|
207
213
|
home.palette.close()
|
|
@@ -235,6 +241,15 @@ function onKey(e: KeyboardEvent): void {
|
|
|
235
241
|
return
|
|
236
242
|
}
|
|
237
243
|
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
244
|
+
// the tree's keys need no focus: the arrows move it from anywhere on the
|
|
245
|
+
// page, Enter and space only where no button or link claims them
|
|
246
|
+
// the atelier's ←/→ roll, so at its first or last roll they stop there
|
|
247
|
+
const rolls = app.sel?.t === 'asset' && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')
|
|
248
|
+
const own = !rolls && (e.key.startsWith('Arrow') || t.closest('button, a, select, [role="button"]') == null)
|
|
249
|
+
if (own && app.tree.key(e.key)) {
|
|
250
|
+
e.preventDefault()
|
|
251
|
+
return
|
|
252
|
+
}
|
|
238
253
|
if (e.key === FOLLOW_KEY && (!app.live.chart.follow || !home.dock.chart.follow)) {
|
|
239
254
|
e.preventDefault()
|
|
240
255
|
app.live.chart.attach()
|
|
@@ -306,6 +321,7 @@ const App = observer(() => {
|
|
|
306
321
|
{home.page === 'overview' ? <Overview app={home} /> : null}
|
|
307
322
|
{home.page === 'planned' ? <PlannedView app={home} /> : null}
|
|
308
323
|
{home.page === 'activity' ? <ActivityView app={home} /> : null}
|
|
324
|
+
{home.page === 'insights' ? <InsightsView app={home} /> : null}
|
|
309
325
|
{home.page === 'archived' ? <ArchivedView app={home} /> : null}
|
|
310
326
|
{home.visited.map((r) => <RepoView key={r.key} app={r} />)}
|
|
311
327
|
</Panel>
|