rvlib-shipkit 1.5.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 +46 -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_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 +4 -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 +120 -0
- package/src/home/gui-app.ts +24 -27
- package/src/home/home-start.ts +11 -5
- package/src/home/open-app.ts +2 -2
- package/src/home/via-hub.ts +14 -0
- 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 +1 -1
- package/src/serve/hub-app/home-strip.css +5 -9
- package/src/serve/hub-app/home-strip.tsx +4 -17
- package/src/serve/hub-app/hub-app.tsx +18 -4
- package/src/serve/hub-app/hub.css +24 -20
- package/src/serve/hub-app/keys.ts +10 -2
- 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 +6 -3
- package/src/serve/hub-app/projects-svc.ts +10 -0
- package/src/serve/hub-app/repo-sections.ts +31 -17
- package/src/serve/hub-app/repo-st.ts +24 -5
- 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/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
|
+
}
|
|
@@ -181,7 +181,7 @@ export class HomeSt {
|
|
|
181
181
|
const want = from == null || from === to ? null : sectionOfRepo(from)
|
|
182
182
|
if (want != null && sectionOfRepo(to) !== want) {
|
|
183
183
|
const known = to.cols.loaded || to.cols.cached
|
|
184
|
-
openSection(to, known && !repoSections(to).some((s) => s.id === want) ? 'readme' : want)
|
|
184
|
+
openSection(to, known && !repoSections(to).some((s) => s.id === want && s.opens) ? 'readme' : want)
|
|
185
185
|
}
|
|
186
186
|
this.show('repo', name, 'push')
|
|
187
187
|
}
|
|
@@ -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 */
|
|
@@ -156,8 +152,8 @@
|
|
|
156
152
|
.wbar.hi > i{background:var(--red)}
|
|
157
153
|
|
|
158
154
|
/* a process card on the processes page: the menu's two panels at page width */
|
|
159
|
-
.
|
|
160
|
-
.
|
|
161
|
-
.
|
|
162
|
-
.
|
|
163
|
-
.
|
|
155
|
+
.pdetail .hmacts{max-width:none}
|
|
156
|
+
.pdetail .hmabout{max-width:none;max-height:none}
|
|
157
|
+
.pdetail>.cstitle{margin:var(--s3) 0 var(--s1)}
|
|
158
|
+
.pdetail>.cstitle:first-child{margin-top:0}
|
|
159
|
+
.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)))
|
|
@@ -225,19 +226,6 @@ const Monitor = observer(({ m }: { m: MachineSt }) => {
|
|
|
225
226
|
)
|
|
226
227
|
})
|
|
227
228
|
|
|
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
229
|
/** the shown repo in the strip: its icon, its name in its colour */
|
|
242
230
|
const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
|
|
243
231
|
const p = app.projects.list.find((x) => x.name === name)
|
|
@@ -247,7 +235,6 @@ const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
|
|
|
247
235
|
{/* the icon's box is there before the project list is, so the name never moves */}
|
|
248
236
|
{p?.icon != null ? <img src={p.icon} alt="" width={18} height={18} /> : <i className="hrico" />}
|
|
249
237
|
<b className="hrname" style={{ color: id.colorDark }}>{name}</b>
|
|
250
|
-
<IdentitySwatches app={app} name={name} />
|
|
251
238
|
</span>
|
|
252
239
|
)
|
|
253
240
|
})
|
|
@@ -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'
|
|
@@ -93,7 +93,7 @@ const Main = observer(({ app }: { app: RepoSt }) => {
|
|
|
93
93
|
if (app.sel?.t === 'col') return (
|
|
94
94
|
<main>
|
|
95
95
|
{errs}
|
|
96
|
-
{app.sel.id === 'checks' ? <CheckTable app={app} id={app.sel.id} /> : app.sel.id === 'asks' ? <AskTable app={app} id={app.sel.id} /> : <
|
|
96
|
+
{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
97
|
</main>
|
|
98
98
|
)
|
|
99
99
|
if (app.sel?.t === 'tests') return <main>{errs}<div className="stagewrap"><TestsPage app={app} check={app.sel.check} /></div></main>
|
|
@@ -195,13 +195,18 @@ const RepoView = observer(({ app }: { app: RepoSt }) => {
|
|
|
195
195
|
function onKey(e: KeyboardEvent): void {
|
|
196
196
|
// the command keys beat the focus check: they work from anywhere, any page
|
|
197
197
|
const pk = pageKey(e)
|
|
198
|
-
|
|
198
|
+
// in a text field ⌘↑ and ⌘↓ keep moving the caret to its ends
|
|
199
|
+
const typing = (e.target as HTMLElement).tagName === 'INPUT' || (e.target as HTMLElement).tagName === 'TEXTAREA'
|
|
200
|
+
if (pk != null && !(pk.t === 'step' && typing)) {
|
|
199
201
|
e.preventDefault()
|
|
200
202
|
if (pk.t === 'picker') home.palette.toggle()
|
|
201
203
|
else if (pk.t === 'dock') home.dock.cycle()
|
|
202
204
|
else if (pk.t === 'home' || pk.t === 'planned' || pk.t === 'activity') {
|
|
203
205
|
home.palette.close()
|
|
204
206
|
if (home.machine != null) home.navigate(PLACE_PATH[pk.t === 'home' ? 'overview' : pk.t])
|
|
207
|
+
} else if (pk.t === 'step') {
|
|
208
|
+
home.palette.close()
|
|
209
|
+
home.projects.step(pk.by)
|
|
205
210
|
} else {
|
|
206
211
|
const p = home.projects.list[pk.n - 1]
|
|
207
212
|
home.palette.close()
|
|
@@ -235,6 +240,15 @@ function onKey(e: KeyboardEvent): void {
|
|
|
235
240
|
return
|
|
236
241
|
}
|
|
237
242
|
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
243
|
+
// the tree's keys need no focus: the arrows move it from anywhere on the
|
|
244
|
+
// page, Enter and space only where no button or link claims them
|
|
245
|
+
// the atelier's ←/→ roll, so at its first or last roll they stop there
|
|
246
|
+
const rolls = app.sel?.t === 'asset' && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')
|
|
247
|
+
const own = !rolls && (e.key.startsWith('Arrow') || t.closest('button, a, select, [role="button"]') == null)
|
|
248
|
+
if (own && app.tree.key(e.key)) {
|
|
249
|
+
e.preventDefault()
|
|
250
|
+
return
|
|
251
|
+
}
|
|
238
252
|
if (e.key === FOLLOW_KEY && (!app.live.chart.follow || !home.dock.chart.follow)) {
|
|
239
253
|
e.preventDefault()
|
|
240
254
|
app.live.chart.attach()
|
|
@@ -89,6 +89,9 @@ aside > .tscroll::-webkit-scrollbar-thumb{background:var(--line);border-radius:3
|
|
|
89
89
|
The chevron column is the tree's own, so a section's rows line up under it */
|
|
90
90
|
.tsec{display:grid;grid-template-columns:14px 16px minmax(0,1fr) auto auto;align-items:center;gap:var(--s2);height:26px;padding:0 var(--s1) 0 var(--s2);color:var(--dim);font-size:13px;cursor:pointer;white-space:nowrap}
|
|
91
91
|
.tsec:hover{background:var(--panel2);color:var(--ink)}
|
|
92
|
+
.tsec.empty{opacity:.4}
|
|
93
|
+
.tsec.noop{cursor:default}
|
|
94
|
+
.tsec.noop:hover{background:none;color:var(--dim)}
|
|
92
95
|
.tsec.on{box-shadow:inset 2px 0 0 var(--accent)}
|
|
93
96
|
.tsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
|
|
94
97
|
.tsec .hico{width:16px;height:16px}
|
|
@@ -631,19 +634,21 @@ a.tok{color:var(--accent)}
|
|
|
631
634
|
padding:var(--s1) var(--s2);font-size:12.5px;outline:none}
|
|
632
635
|
.wform input:focus{border-color:var(--accent)}
|
|
633
636
|
|
|
634
|
-
/* ── collection
|
|
635
|
-
.
|
|
636
|
-
.cghead{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
|
|
637
|
+
/* ── a collection's page: its title and count, then its table ── */
|
|
638
|
+
.cghead{font-size:15px;font-weight:600;color:var(--ink);
|
|
637
639
|
display:flex;gap:var(--s2);align-items:baseline;margin:0 0 var(--s3)}
|
|
638
640
|
.ctlead{font-size:11px;color:var(--dim);margin:calc(-1 * var(--s1)) 0 var(--s2)}
|
|
639
|
-
.cghead .cnt{font-size:
|
|
640
|
-
/* the checks and
|
|
641
|
+
.cghead .cnt{font-size:12px;font-weight:400;color:var(--dim)}
|
|
642
|
+
/* the checks, asks and secrets pages: one dense table each (col-tables-view.tsx), on
|
|
641
643
|
the activity table (.actable), a group header per state that folds */
|
|
642
644
|
.ctpage{flex:1;min-height:0;overflow-y:auto;padding:var(--s3) var(--s4);font-size:12.5px;container-type:inline-size}
|
|
645
|
+
/* the table sits on the panel surface, the way every card section does, so
|
|
646
|
+
its lines read against something lighter than the page */
|
|
647
|
+
.ctbox{background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:var(--s1) 0}
|
|
643
648
|
.cttable{font-variant-numeric:tabular-nums}
|
|
644
649
|
.cttable td{padding:var(--s0) var(--s2);vertical-align:baseline}
|
|
645
|
-
.ctgrp td{padding:var(--s2) var(--s2) var(--s1);color:var(--
|
|
646
|
-
.ctgrp:hover td{color:var(--
|
|
650
|
+
.ctgrp td{padding:var(--s2) var(--s2) var(--s1);color:var(--ink);font-size:12.5px;font-weight:600;border-bottom:1px solid var(--line);cursor:pointer;user-select:none}
|
|
651
|
+
.ctgrp:hover td{color:var(--accent)}
|
|
647
652
|
.ctgrp.g-fail td{color:var(--red)}
|
|
648
653
|
.ctgrp.g-warn td{color:var(--warn)}
|
|
649
654
|
.ctcaret{display:inline-block;width:14px}
|
|
@@ -656,17 +661,8 @@ a.tok{color:var(--accent)}
|
|
|
656
661
|
.ctbudget,.ctplan,.ctblocks{color:var(--dim)}
|
|
657
662
|
.ctprobe{font-size:11.5px}
|
|
658
663
|
.cttable .c-dot{width:22px}.cttable .c-id{width:140px}.cttable .c-title{width:16%}.cttable .c-age{width:72px}.cttable .c-dur{width:152px}.cttable .c-plan{width:88px}
|
|
659
|
-
.cttable .c-kind{width:92px}.cttable .c-blocks{width:32%}.cttable .c-probe{width:84px}
|
|
664
|
+
.cttable .c-name{width:30%}.cttable .c-file{width:34%}.cttable .c-kind{width:92px}.cttable .c-blocks{width:32%}.cttable .c-probe{width:84px}
|
|
660
665
|
@container (max-width:760px){.cttable .c-title{width:0}.cttable .cttitle{display:none}.cttable .c-plan{width:0}.cttable .ctplan{display:none}}
|
|
661
|
-
.cgcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:var(--s3)}
|
|
662
|
-
.cgcard{background:var(--panel);border:1px solid var(--line);border-radius:5px;padding:var(--s2) var(--s3);cursor:pointer;min-width:0}
|
|
663
|
-
.cgcard:hover{border-color:var(--accent)}
|
|
664
|
-
.cgcard .cgtop{display:flex;gap:var(--s2);align-items:center;min-width:0}
|
|
665
|
-
.cgcard .cgtop b{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
666
|
-
.cgcard .truth{display:block;font:11px ui-monospace,Menlo,monospace;color:var(--dim);margin-top:var(--s1);
|
|
667
|
-
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
668
|
-
.cgcard .cgrem{color:var(--ok);border-left:2px solid var(--ok);padding-left:var(--s2);font-size:12px;margin-top:var(--s1);
|
|
669
|
-
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
670
666
|
|
|
671
667
|
/* ── graph item view (the graph itself fills the stage) ── */
|
|
672
668
|
.graphview{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3) var(--s4) var(--s4)}
|
|
@@ -709,13 +705,10 @@ a.tok{color:var(--accent)}
|
|
|
709
705
|
overflow-x:auto;font:11px/1.5 ui-monospace,Menlo,monospace;white-space:pre}
|
|
710
706
|
.md pre code{background:none;border:0;padding:0}
|
|
711
707
|
.md a{color:var(--accent)}
|
|
712
|
-
.md.clamp{max-height:180px;overflow:hidden;margin-top:var(--s1);
|
|
713
|
-
-webkit-mask-image:linear-gradient(var(--solid) 65%,transparent);mask-image:linear-gradient(var(--solid) 65%,transparent)}
|
|
714
708
|
|
|
715
709
|
/* ── secret (cred) cards ── */
|
|
716
710
|
.credname{font:600 13px ui-monospace,Menlo,monospace}
|
|
717
711
|
.creddesc{font-size:12px;color:var(--dim);margin-top:var(--s1)}
|
|
718
|
-
.credrow{display:flex;gap:var(--s2);align-items:center;margin-top:var(--s2)}
|
|
719
712
|
.stchip{font-size:10px;border-radius:5px;padding:var(--s0) var(--s2);letter-spacing:.08em;text-transform:uppercase;
|
|
720
713
|
border:1px solid;flex:none;font-weight:600}
|
|
721
714
|
.stchip.ok{color:var(--ok);border-color:var(--ok)}
|
|
@@ -759,6 +752,17 @@ pre{background:var(--panel);border:1px solid var(--line);border-radius:3px;paddi
|
|
|
759
752
|
.adctl{flex:none;display:flex;align-items:center}
|
|
760
753
|
.adflag{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px;color:var(--dim)}
|
|
761
754
|
.adflag input{width:auto;margin:0}
|
|
755
|
+
.adid{display:flex;align-items:flex-start;gap:var(--s4)}
|
|
756
|
+
.adicon{flex:none;width:128px;display:flex;flex-direction:column;gap:var(--s1);font-size:12px}
|
|
757
|
+
.adicon img{display:block;width:128px;height:128px}
|
|
758
|
+
.adicon img[hidden]{display:none}
|
|
759
|
+
.adpick{display:flex;align-items:center;gap:var(--s2);margin-top:var(--s1);font-size:12px;cursor:pointer}
|
|
760
|
+
.colcard .adpick input{flex:none;min-width:0;width:28px;height:28px;padding:0;margin:0;border:1px solid var(--line);border-radius:4px;background:none;cursor:pointer}
|
|
761
|
+
.adpick input::-webkit-color-swatch-wrapper{padding:0}
|
|
762
|
+
.adpick input::-webkit-color-swatch{border:none;border-radius:3px}
|
|
763
|
+
.adsample{padding:var(--s0) var(--s2);border-radius:4px;font-weight:600;font-size:13px}
|
|
764
|
+
.adsample.dark{background:var(--bg)}
|
|
765
|
+
.adsample.light{background:var(--tag)}
|
|
762
766
|
.adconfirm{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px}
|
|
763
767
|
/* a repo's plan page: one line per planned check */
|
|
764
768
|
.plntable{width:100%;border-collapse:collapse;font-size:12.5px}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// the page's command keys, read the same on every layout: ⌘1 to ⌘9 select the
|
|
2
|
-
// nth repo of the project list, then the home row under the right hand: ⌘J
|
|
2
|
+
// nth repo of the project list, ⌘↑ and ⌘↓ the repo above or below, then the home row under the right hand: ⌘J
|
|
3
3
|
// the picker, ⌘K the overview, ⌘L the activity, ⌘' the plan. ⌘B cycles the
|
|
4
4
|
// dock at the bottom of the page (dock-st.ts): the line, slim, open.
|
|
5
5
|
// A digit and the quote are read from the physical key (`code`), so an
|
|
6
6
|
// AZERTY keyboard, where the key under 1 types `&`, selects the same repo. Ctrl stands in for
|
|
7
7
|
// ⌘ off a Mac. The macOS app binds none of these to a menu item, so they
|
|
8
8
|
// reach the page (tests/home-strip.test.tsx reads ShipkitGui.swift).
|
|
9
|
-
export type PageKey = { t: 'repo'; n: number } | { t: 'home' } | { t: 'planned' } | { t: 'activity' } | { t: 'picker' } | { t: 'dock' }
|
|
9
|
+
export type PageKey = { t: 'repo'; n: number } | { t: 'step'; by: -1 | 1 } | { t: 'home' } | { t: 'planned' } | { t: 'activity' } | { t: 'picker' } | { t: 'dock' }
|
|
10
10
|
|
|
11
11
|
type KeyLike = { key: string; code: string; metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; altKey: boolean }
|
|
12
12
|
|
|
@@ -15,6 +15,8 @@ export function pageKey(e: KeyLike): PageKey | null {
|
|
|
15
15
|
const digit = /^Digit([1-9])$/.exec(e.code)
|
|
16
16
|
if (digit != null) return { t: 'repo', n: Number(digit[1]) }
|
|
17
17
|
if (e.shiftKey) return null
|
|
18
|
+
if (e.key === 'ArrowUp') return { t: 'step', by: -1 }
|
|
19
|
+
if (e.key === 'ArrowDown') return { t: 'step', by: 1 }
|
|
18
20
|
if (e.code === 'Quote' || e.key === "'") return { t: 'planned' }
|
|
19
21
|
const k = e.key.toLowerCase()
|
|
20
22
|
if (k === 'j') return { t: 'picker' }
|
|
@@ -28,6 +30,8 @@ export function pageKey(e: KeyLike): PageKey | null {
|
|
|
28
30
|
export const PLACE_KEY = { overview: '⌘K', planned: "⌘'", activity: '⌘L' } as const
|
|
29
31
|
/** the picker's shortcut, on the first button, before home */
|
|
30
32
|
export const PICKER_KEY = '⌘J'
|
|
33
|
+
/** the repo above and below, hinted at the project list's foot */
|
|
34
|
+
export const STEP_KEYS = ['⌘↑', '⌘↓'] as const
|
|
31
35
|
/** the dock's shortcut, on the dock's own header */
|
|
32
36
|
export const DOCK_KEY = '⌘B'
|
|
33
37
|
|
|
@@ -35,6 +39,10 @@ export const DOCK_KEY = '⌘B'
|
|
|
35
39
|
* badge at the input's right prints this */
|
|
36
40
|
export const SEARCH_KEY = '/'
|
|
37
41
|
|
|
42
|
+
/** in the filter, up and down walk the tree rows the filter keeps; left
|
|
43
|
+
* and right stay the text caret's */
|
|
44
|
+
export const isTreeWalk = (key: string): boolean => key === 'ArrowUp' || key === 'ArrowDown'
|
|
45
|
+
|
|
38
46
|
export function isSearchKey(e: Pick<KeyLike, 'key' | 'metaKey' | 'ctrlKey' | 'altKey'>): boolean {
|
|
39
47
|
return e.key === SEARCH_KEY && !e.metaKey && !e.ctrlKey && !e.altKey
|
|
40
48
|
}
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
// The processes page of a repo:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// machine, never the hub.
|
|
1
|
+
// The processes page of a repo: the overview's process table, this repo's
|
|
2
|
+
// lines alone, never a card per process. The process a tree row opened gets
|
|
3
|
+
// its description and its log under the table. pm runs them, so the page
|
|
4
|
+
// reads the machine, never the hub.
|
|
5
5
|
import { observer } from 'mobx-react-lite'
|
|
6
6
|
import { useEffect, useRef } from 'react'
|
|
7
|
-
import { Log, ProcessMenu } from './home-strip.tsx'
|
|
7
|
+
import { Log, ProcessMenu, ProcessTable } from './home-strip.tsx'
|
|
8
8
|
import type { RepoSt } from './repo-st.ts'
|
|
9
9
|
import type { ProcessRow } from '../../home/process-rows.ts'
|
|
10
10
|
|
|
11
11
|
const noop = (): void => {}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/** the opened process: its menu (actions, then its description) and its log */
|
|
14
|
+
const ProcessDetail = observer(({ app, r }: { app: RepoSt; r: ProcessRow }) => {
|
|
14
15
|
const el = useRef<HTMLElement>(null)
|
|
15
|
-
useEffect(() => {
|
|
16
|
+
useEffect(() => { el.current?.scrollIntoView({ block: 'nearest' }) }, [r.id])
|
|
16
17
|
const m = app.home.machine
|
|
17
18
|
if (m == null) return null
|
|
18
19
|
return (
|
|
19
|
-
<section ref={el} className=
|
|
20
|
+
<section ref={el} className="csec pdetail" id={`process-${r.id}`}>
|
|
21
|
+
<div className="cstitle">{r.label}</div>
|
|
20
22
|
<ProcessMenu m={m} r={r} close={noop} lastLog={r.tail.length === 0} />
|
|
21
23
|
{r.tail.length === 0 ? null : <><div className="cstitle">log</div><Log lines={r.tail} /></>}
|
|
22
24
|
</section>
|
|
@@ -25,6 +27,8 @@ const ProcessCard = observer(({ app, r, lit }: { app: RepoSt; r: ProcessRow; lit
|
|
|
25
27
|
|
|
26
28
|
export const ProcessesPage = observer(({ app, id }: { app: RepoSt; id: string | null }) => {
|
|
27
29
|
const rows = app.globalProcesses
|
|
30
|
+
const m = app.home.machine
|
|
31
|
+
const lit = rows.find((r) => r.id === id) ?? null
|
|
28
32
|
return (
|
|
29
33
|
<div className="colcard checkcard">
|
|
30
34
|
<div className="chead">
|
|
@@ -32,7 +36,10 @@ export const ProcessesPage = observer(({ app, id }: { app: RepoSt; id: string |
|
|
|
32
36
|
<h3 className="ctitle">{app.name}</h3>
|
|
33
37
|
</div>
|
|
34
38
|
<div className="usays">the processes this repo declares in its <code>.shipkit.ts</code> files. pm runs them, so they run whether or not the hub answers</div>
|
|
35
|
-
{rows.length === 0
|
|
39
|
+
{rows.length === 0 || m == null
|
|
40
|
+
? <div className="dim">this repo declares no global process</div>
|
|
41
|
+
: <section className="csec"><ProcessTable app={app.home} m={m} repo={app.key} /></section>}
|
|
42
|
+
{lit == null ? null : <ProcessDetail app={app} r={lit} />}
|
|
36
43
|
</div>
|
|
37
44
|
)
|
|
38
45
|
})
|
|
@@ -21,11 +21,14 @@
|
|
|
21
21
|
.pjarow span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
22
22
|
.pjarow img{flex:none;border-radius:3px;object-fit:contain}
|
|
23
23
|
.pjaini{flex:none;width:14px;height:14px;border-radius:3px;background:var(--panel2)}
|
|
24
|
-
/* the foot
|
|
24
|
+
/* the foot: sort (its title names the order), the step keys hint, archived
|
|
25
25
|
carries its count and stays lit while its rows show */
|
|
26
26
|
.pgsort,.pjarbtn{flex:none;display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s1);background:transparent;border:1px solid var(--line);border-radius:3px;
|
|
27
27
|
color:var(--dim);font-size:11px;cursor:pointer}
|
|
28
28
|
.pgsort{flex:0 1 auto;min-width:0}
|
|
29
|
+
/* the step keys, a hint: the repo above or below */
|
|
30
|
+
.pjstep{flex:none;display:inline-flex;gap:var(--s0);margin-left:auto;color:var(--dim);font-size:11px}
|
|
31
|
+
.pjstep kbd{padding:0 var(--s1);border:1px solid var(--line);border-radius:3px;font:inherit}
|
|
29
32
|
.pgsortl{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
30
33
|
.pgsort .hico,.pjarbtn .hico{flex:none;width:14px;height:14px}
|
|
31
34
|
.pgsort:hover,.pjarbtn:hover{color:var(--ink);border-color:var(--accent)}
|