rvlib-shipkit 1.1.1 → 1.3.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 +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* the activity page (activity-view.tsx): one table, a row group per repo. The
|
|
2
|
+
table is fixed width so a long title ellipses instead of scrolling the page
|
|
3
|
+
sideways; the file list floats on hover. State colours only: a branch green
|
|
4
|
+
on main, orange elsewhere; a commit orange until pushed; dirt red. */
|
|
5
|
+
.activity{font-size:13px;container-type:inline-size}
|
|
6
|
+
.actop{display:flex;align-items:center;flex-wrap:wrap;gap:var(--s3);margin-bottom:var(--s3)}
|
|
7
|
+
.acwin{display:inline-flex;border:1px solid var(--line);border-radius:4px;overflow:hidden}
|
|
8
|
+
.acwin button{border:0;border-radius:0;padding:var(--s0) var(--s2);background:none;color:var(--dim);font-size:12px}
|
|
9
|
+
.acwin button:hover{color:var(--ink);background:var(--panel2)}
|
|
10
|
+
.acwin button.on{color:var(--accent);background:var(--sel)}
|
|
11
|
+
.accap{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px}
|
|
12
|
+
.accap input{width:44px;height:22px;box-sizing:border-box;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:var(--s0) var(--s1);font:inherit;font-variant-numeric:tabular-nums}
|
|
13
|
+
.accap input:focus{outline:none;border-color:var(--accent)}
|
|
14
|
+
.acnext{display:inline-flex;align-items:center;min-width:40px;color:var(--dim);font-size:12px;font-variant-numeric:tabular-nums}
|
|
15
|
+
/* the counts beside the controls, and the refresh button, as small rounded chips */
|
|
16
|
+
.acpill{display:inline-flex;align-items:center;gap:var(--s1);padding:0 var(--s2);border:1px solid var(--line);border-radius:999px;background:var(--hi3);color:var(--dim);font-size:11.5px;line-height:20px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
17
|
+
button.acpill{cursor:pointer;font:inherit;font-size:11.5px}
|
|
18
|
+
button.acpill:hover:not(:disabled){color:var(--ink);border-color:var(--accent)}
|
|
19
|
+
button.acpill:disabled{cursor:default}
|
|
20
|
+
.acring{display:inline-block}
|
|
21
|
+
.acring.on{color:var(--accent);animation:acspin 1s linear infinite}
|
|
22
|
+
@keyframes acspin{to{transform:rotate(360deg)}}
|
|
23
|
+
.acdim{color:var(--dim)}
|
|
24
|
+
.actable{width:100%;table-layout:fixed;border-collapse:collapse}
|
|
25
|
+
.actable.stale{opacity:.5}
|
|
26
|
+
.actable th{text-align:left;font-weight:400;color:var(--dim);font-size:11.5px;padding:var(--s1) var(--s2);border-bottom:1px solid var(--line)}
|
|
27
|
+
.actable td{padding:var(--s0) var(--s2);vertical-align:top;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
28
|
+
.acgrp{border-bottom:1px solid var(--line)}
|
|
29
|
+
.acgrp tr:first-child td{padding-top:var(--s1)}
|
|
30
|
+
.acgrp tr:last-child td{padding-bottom:var(--s1)}
|
|
31
|
+
.acgrp:hover{background:var(--panel)}
|
|
32
|
+
.actable{--acname-w:170px}.actable,.acchktip{--acmark-w:10px}.c-name{width:var(--acname-w)}.c-status{width:52px}.c-branch{width:150px}
|
|
33
|
+
.c-sha{width:72px}.c-when{width:56px}.c-who{width:90px}
|
|
34
|
+
.acrepo{display:inline-flex;align-items:center;gap:var(--s1);max-width:100%;color:var(--ink);text-decoration:none}
|
|
35
|
+
.acrepo span{overflow:hidden;text-overflow:ellipsis}
|
|
36
|
+
.acrepo img{flex:none;border-radius:3px;object-fit:contain}
|
|
37
|
+
.acrepo:hover span{text-decoration:underline}
|
|
38
|
+
.t-ok{color:var(--ok)}.t-warn{color:var(--warn)}.t-red{color:var(--red)}
|
|
39
|
+
.acstatus{white-space:nowrap;font-variant-numeric:tabular-nums;cursor:default}
|
|
40
|
+
/* the status dot is a button: hover floats its checks, a click lists them under the row */
|
|
41
|
+
.acdot{border:0;background:none;padding:0 var(--s1);margin:0 calc(-1 * var(--s1));font:inherit;font-variant-numeric:tabular-nums;color:inherit;cursor:pointer;border-radius:3px}
|
|
42
|
+
.acdot:hover,.acdot[aria-expanded="true"]{background:var(--panel2)}
|
|
43
|
+
.acchktip{z-index:40;max-width:560px;max-height:60vh;overflow:auto;background:var(--panel2);border:1px solid var(--line);border-radius:3px;box-shadow:0 2px 10px var(--shadow);padding:var(--s1) var(--s2);color:var(--ink);font-size:12px}
|
|
44
|
+
.acchecks{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--s1);white-space:normal}
|
|
45
|
+
.acchecks li{display:flex;flex-direction:column;min-width:0}
|
|
46
|
+
.acchkhead{display:flex;align-items:baseline;gap:var(--s2);min-width:0}
|
|
47
|
+
.acchkmark{flex:none;width:var(--acmark-w)}
|
|
48
|
+
.acchecks .fail .acchkmark,.acchecks .fail .acchkid{color:var(--red)}
|
|
49
|
+
.acchecks .warn .acchkmark,.acchecks .warn .acchkid{color:var(--warn)}
|
|
50
|
+
.acchkid{flex:none;font:11.5px ui-monospace,Menlo,monospace;text-decoration:none}
|
|
51
|
+
.acchkid:hover{text-decoration:underline}
|
|
52
|
+
.acchktitle{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
53
|
+
.acchkline{margin-left:calc(var(--acmark-w) + var(--s2));color:var(--dim);font:11.5px/1.4 ui-monospace,Menlo,monospace;overflow-wrap:anywhere}
|
|
54
|
+
.acchkrow td{padding:var(--s1) var(--s2) var(--s2) calc(var(--acname-w) + var(--s2));white-space:normal}
|
|
55
|
+
.acdirty,.acdirt,.acerr{color:var(--red)}
|
|
56
|
+
.acdirt{cursor:help}
|
|
57
|
+
.acsha{font:11.5px ui-monospace,Menlo,monospace;color:var(--dim)}
|
|
58
|
+
.acwhen{color:var(--dim);font-variant-numeric:tabular-nums;text-align:right}
|
|
59
|
+
.acmark{display:inline-block;width:14px;font-size:11px}
|
|
60
|
+
.pushed .acmark{color:var(--ok)}
|
|
61
|
+
.unpushed{color:var(--warn)}
|
|
62
|
+
/* the last commit, from before the window: grey, its marker kept */
|
|
63
|
+
.actitle.old,.unpushed.old{color:var(--dim)}
|
|
64
|
+
.acside{display:inline-block;margin-right:var(--s1);padding:0 var(--s1);border:1px solid var(--line);border-radius:3px;color:var(--dim);font-size:11px}
|
|
65
|
+
.acwho{color:var(--dim)}
|
|
66
|
+
.acmore{border:0;background:none;padding:0;color:var(--accent);font-size:12px}
|
|
67
|
+
.acmore:hover{text-decoration:underline}
|
|
68
|
+
.acfiles{z-index:40;max-width:480px;max-height:60vh;overflow:hidden;background:var(--panel2);border:1px solid var(--line);border-radius:3px;box-shadow:0 2px 10px var(--shadow);padding:var(--s1) var(--s2);color:var(--ink);pointer-events:none}
|
|
69
|
+
.acfiles pre{margin:0;font:11.5px/1.4 ui-monospace,Menlo,monospace;white-space:pre-wrap;word-break:break-all}
|
|
70
|
+
/* narrow: the sha and the author go, the title keeps its room */
|
|
71
|
+
@container (max-width:680px){.c-sha,.acsha,.c-who,.acwho{display:none}.actable{--acname-w:120px}.c-branch{width:100px}.acchkrow td{padding-left:var(--s2)}}
|
|
72
|
+
/* the open in Fork button leads the commit list, quiet until its row is hovered */
|
|
73
|
+
.c-fork{width:22px}
|
|
74
|
+
.actable td.acforkcell{padding-right:0;overflow:visible}
|
|
75
|
+
.acfork{display:inline-flex;align-items:center;vertical-align:-2px;padding:0;border:0;background:none;color:var(--dim);cursor:pointer;opacity:.5}
|
|
76
|
+
.acfork .hico{width:13px;height:13px}
|
|
77
|
+
.acgrp:hover .acfork{opacity:1}
|
|
78
|
+
.acfork:hover{color:var(--accent)}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
|
3
|
+
// re-render and a test drives it headless), reload and keep active go to the
|
|
4
|
+
// machine by the repo's key. On a hub of its own there is no machine, and no
|
|
5
|
+
// admin page.
|
|
6
|
+
import { makeAutoObservable } from 'mobx'
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
|
+
|
|
9
|
+
export class AdminSvc {
|
|
10
|
+
/** the archive button was pressed: its confirmation shows in its place */
|
|
11
|
+
confirming = false
|
|
12
|
+
|
|
13
|
+
constructor(private st: RepoSt) {
|
|
14
|
+
makeAutoObservable<this, 'st'>(this, { st: false })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
askArchive(): void {
|
|
18
|
+
this.confirming = true
|
|
19
|
+
}
|
|
20
|
+
cancel(): void {
|
|
21
|
+
this.confirming = false
|
|
22
|
+
}
|
|
23
|
+
/** archived, the repo leaves the project list and its page shows the
|
|
24
|
+
* archived note with its way back */
|
|
25
|
+
archive(): void {
|
|
26
|
+
this.confirming = false
|
|
27
|
+
void this.st.home.projects.setArchived(this.st.key, true)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** its hub, stopped in the daemon and loaded again with its config read fresh */
|
|
31
|
+
reload(): Promise<boolean> {
|
|
32
|
+
return this.st.home.machine?.hub(this.st.key, 'restart') ?? Promise.resolve(false)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get active(): boolean {
|
|
36
|
+
return this.st.home.machine?.isActive(this.st.key) === true
|
|
37
|
+
}
|
|
38
|
+
setActive(on: boolean): Promise<boolean> {
|
|
39
|
+
return this.st.home.machine?.setActive(this.st.key, on) ?? Promise.resolve(false)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// THE ADMIN PAGE of a repo (`#page:admin`): what administers the repo rather
|
|
2
|
+
// than works in it, one row per control, each with the one sentence that says
|
|
3
|
+
// what it does. State in admin-svc.ts. Only on the home: a hub of its own has
|
|
4
|
+
// no machine to administer it.
|
|
5
|
+
import { observer } from 'mobx-react-lite'
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
|
+
|
|
9
|
+
const Row = ({ title, says, children }: { title: string; says: string; children: ReactNode }) => (
|
|
10
|
+
<div className="adrow">
|
|
11
|
+
<div className="adtext">
|
|
12
|
+
<b>{title}</b>
|
|
13
|
+
<span className="dim">{says}</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div className="adctl">{children}</div>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export const AdminPage = observer(({ app }: { app: RepoSt }) => {
|
|
20
|
+
const a = app.admin
|
|
21
|
+
const w = app.home.machine?.worker(app.key)
|
|
22
|
+
const name = app.name
|
|
23
|
+
return (
|
|
24
|
+
<div className="colcard checkcard">
|
|
25
|
+
<div className="chead">
|
|
26
|
+
<span className="tid">admin</span>
|
|
27
|
+
<h3 className="ctitle">{name}</h3>
|
|
28
|
+
</div>
|
|
29
|
+
<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.`}`}>
|
|
30
|
+
<button className="ghost" onClick={() => void a.reload()}>reload</button>
|
|
31
|
+
</Row>
|
|
32
|
+
<Row title="keep active" says="an active repo runs the checks the plan holds for it and gets its red pings. It is active on its own while an agent works in it or VS Code has it open: this keeps it active without either.">
|
|
33
|
+
<label className="adflag"><input type="checkbox" checked={a.active} onChange={(e) => void a.setActive(e.currentTarget.checked)} />active</label>
|
|
34
|
+
</Row>
|
|
35
|
+
<Row title="archive" says="it leaves the project list for the archived page, and nothing runs there: no hub, no watch, no plan rows, no processes, no widgets. Written to repo.config.ts as archived: true, and undone from the archived page.">
|
|
36
|
+
{a.confirming
|
|
37
|
+
? <span className="adconfirm"><span>archive {name}?</span><button className="primary" onClick={() => a.archive()}>archive it</button><button className="ghost" onClick={() => a.cancel()}>cancel</button></span>
|
|
38
|
+
: <button className="ghost" onClick={() => a.askArchive()}>archive</button>}
|
|
39
|
+
</Row>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
})
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// The view state of THE run chart: its zoom, the width it has on screen and
|
|
2
2
|
// what is under the pointer, over the geometry (timeline-geometry.ts) and the
|
|
3
|
-
// drawing (timeline-chart.tsx).
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// drawing (timeline-chart.tsx). What differs is the source: the dock's, a
|
|
4
|
+
// rolling window that ends now with one group of lanes per repo; a repo's
|
|
5
|
+
// runs page's, its whole record opened on its last minutes.
|
|
6
6
|
import { makeAutoObservable, observableRef } from 'mobx'
|
|
7
7
|
import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
|
|
8
8
|
import {
|
|
9
|
-
anchorScroll, buildAxis, fitScale, fitWindow, layoutTimeline, maxZoom, PAD_PX, windowAxis, ZOOM_MIN, zoomStep, type Axis, type Layout, type Span,
|
|
9
|
+
anchorScroll, buildAxis, fitScale, fitWindow, layoutGroups, layoutTimeline, maxZoom, PAD_PX, windowAxis, ZOOM_MIN, zoomStep, type Axis, type ChartGroup, type Layout, type Span,
|
|
10
10
|
} from './timeline-geometry.ts'
|
|
11
11
|
|
|
12
12
|
/** what a chart draws. `windowMs` null = the whole record, with idle
|
|
@@ -23,6 +23,11 @@ export type ChartSource = {
|
|
|
23
23
|
/** the span the view opens on: at its home zoom this much time, ending
|
|
24
24
|
* now, fills the width. Absent = home is zoom 1, the whole source */
|
|
25
25
|
homeSpanMs?: number
|
|
26
|
+
/** one group of lanes per key, over a rolling window only. `invocations`
|
|
27
|
+
* then holds every group's, and `laneCap` bounds each group */
|
|
28
|
+
groups?: ChartGroup[]
|
|
29
|
+
/** the fewest lanes a group takes, so its label fits beside them */
|
|
30
|
+
minLanes?: number
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/** what the tooltip says, and the viewport point it is fixed at */
|
|
@@ -117,7 +122,9 @@ export class ChartView {
|
|
|
117
122
|
get layout(): Layout | null {
|
|
118
123
|
const s = this.src
|
|
119
124
|
if (s == null || this.viewW === 0) return null
|
|
120
|
-
|
|
125
|
+
const from = this.from
|
|
126
|
+
if (s.groups != null && from != null) return layoutGroups(s.groups, this.now, this.scale, { from, laneCap: s.laneCap, minLanes: s.minLanes, cap: s.cap })
|
|
127
|
+
return layoutTimeline(s.invocations, this.now, this.scale, { from: from ?? undefined, laneCap: s.laneCap, cap: s.cap })
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
private axisAt(scale: number): Axis {
|
|
@@ -8,7 +8,7 @@ import { useEffect, useRef } from 'react'
|
|
|
8
8
|
import { CACHED } from '../../core/outcome-words.ts'
|
|
9
9
|
import { durationTone, fmtMs } from './dur.ts'
|
|
10
10
|
import { Help, TERMS } from './help.tsx'
|
|
11
|
-
import type {
|
|
11
|
+
import type { RepoSt } from './repo-st.ts'
|
|
12
12
|
import type { SlowSuite, CheckFacts, RecentRun, WhenAt } from './hub-types.ts'
|
|
13
13
|
import { renderMd, renderMdInline } from './md.ts'
|
|
14
14
|
import { nextRun } from './next-run.ts'
|
|
@@ -89,7 +89,7 @@ export const copy = (text: string): void => {
|
|
|
89
89
|
|
|
90
90
|
/** THE COMMAND, exactly as the gate spawns it from the repo root, or the one
|
|
91
91
|
* line saying its body is a function inside the gate */
|
|
92
|
-
export const CommandLine = ({ app, u }: { app:
|
|
92
|
+
export const CommandLine = ({ app, u }: { app: RepoSt; u: CheckFacts }) => {
|
|
93
93
|
const c = commandOf(u, app.root)
|
|
94
94
|
if (c.kind === 'in-process')
|
|
95
95
|
return (
|
|
@@ -132,7 +132,7 @@ export const CommandLine = ({ app, u }: { app: HubSt; u: CheckFacts }) => {
|
|
|
132
132
|
* through the managed block, and leaves the file dirty: a change of place is
|
|
133
133
|
* a change its owner reviews in a git client, never a commit made on their
|
|
134
134
|
* behalf. Choosing never asks for the reason before it writes. */
|
|
135
|
-
const RunWhen = observer(({ app, u }: { app:
|
|
135
|
+
const RunWhen = observer(({ app, u }: { app: RepoSt; u: CheckFacts }) => {
|
|
136
136
|
const s = app.checks
|
|
137
137
|
const g = s.gate
|
|
138
138
|
const shown = s.whenShown(u)
|
|
@@ -193,7 +193,7 @@ const Bars = ({ runs, ceiling, budget }: { runs: RecentRun[]; ceiling: number; b
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/** how the last answer came: `ran 3m ago in 412ms`, or `cached 16m ago` */
|
|
196
|
-
export const LastRunValue = observer(({ app, u }: { app:
|
|
196
|
+
export const LastRunValue = observer(({ app, u }: { app: RepoSt; u: CheckFacts }) => {
|
|
197
197
|
const l = lastRun(u)
|
|
198
198
|
if (l == null) return <span className="dim">never here</span>
|
|
199
199
|
return (
|
|
@@ -245,7 +245,7 @@ const LogLines = ({ lines }: { lines: string[] }) => (
|
|
|
245
245
|
|
|
246
246
|
/** the lines of the run in flight, then the lines of the last one. It
|
|
247
247
|
* scrolls itself while a run streams, so the newest line is on screen */
|
|
248
|
-
const LastOutput = observer(({ app, lines }: { app:
|
|
248
|
+
const LastOutput = observer(({ app, lines }: { app: RepoSt; lines: string[] }) => {
|
|
249
249
|
const live = app.checks.liveLines
|
|
250
250
|
const box = useRef<HTMLPreElement>(null)
|
|
251
251
|
useEffect(() => { if (box.current != null) box.current.scrollTop = box.current.scrollHeight }, [live.length])
|
|
@@ -263,7 +263,7 @@ const LastOutput = observer(({ app, lines }: { app: HubSt; lines: string[] }) =>
|
|
|
263
263
|
/** the slow suites declared under the test step, each with the case it
|
|
264
264
|
* argued. Moving a suite between the tiers is the SAME managed block write,
|
|
265
265
|
* and it demands both answers exactly as `defineRepo` does. */
|
|
266
|
-
const SlowSuites = observer(({ app }: { app:
|
|
266
|
+
const SlowSuites = observer(({ app }: { app: RepoSt }) => {
|
|
267
267
|
const s = app.checks
|
|
268
268
|
const declared: SlowSuite[] = s.gate?.tests.slow ?? []
|
|
269
269
|
return (
|
|
@@ -294,7 +294,7 @@ const SlowSuites = observer(({ app }: { app: HubSt }) => {
|
|
|
294
294
|
|
|
295
295
|
// ── the pages ────────────────────────────────────────────────────────────
|
|
296
296
|
|
|
297
|
-
export const CheckCard = observer(({ app, id }: { app:
|
|
297
|
+
export const CheckCard = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
298
298
|
const i = app.cols.find('checks', id)
|
|
299
299
|
if (i == null) return <div className="ph">check vanished, the inventory moved on</div>
|
|
300
300
|
const u = i.check
|
|
@@ -325,8 +325,8 @@ export const CheckCard = observer(({ app, id }: { app: HubSt; id: string }) => {
|
|
|
325
325
|
<Remembered u={u} />
|
|
326
326
|
{/* the help never depends on the second, so it reads the wall
|
|
327
327
|
clock and the row does not repaint on every tick */}
|
|
328
|
-
<Row k="next run" help={nextRun(u, app.cols.
|
|
329
|
-
<TimeAhead clock={app.clock} words={(now) => nextRun(u, app.cols.
|
|
328
|
+
<Row k="next run" help={nextRun(u, app.cols.planOf(u.checkId), Date.now()).help}>
|
|
329
|
+
<TimeAhead clock={app.clock} words={(now) => nextRun(u, app.cols.planOf(u.checkId), now)} />
|
|
330
330
|
</Row>
|
|
331
331
|
<Row k="last run" help={u.status == null
|
|
332
332
|
? 'no run of it is on record in this checkout'
|
|
@@ -18,7 +18,7 @@ const list = (xs: string[]): string => (xs.length < 2 ? xs.join('') : `${xs.slic
|
|
|
18
18
|
const CADENCE: Record<WhenAt, string> = {
|
|
19
19
|
always: 'on every run',
|
|
20
20
|
release: 'on every release run',
|
|
21
|
-
health: '
|
|
21
|
+
health: 'each time the plan runs it',
|
|
22
22
|
off: 'nowhere, it is off',
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -35,6 +35,8 @@ export function rememberedFact(u: CachePop): RememberedFact {
|
|
|
35
35
|
return { v: 'until a file it reads changes', inputs, help: `keyed on ${list(inputs)}: the answer is ${CACHED} until a change under ${inputs.length > 1 ? 'them' : 'it'}, then it runs again` }
|
|
36
36
|
if (u.reads === true && inputs.length === 0 && (u.ttl == null || u.ttl === '') && u.never == null)
|
|
37
37
|
return { v: 'until a file it read changes', inputs: [], help: `keyed on what its last run read: the answer is ${CACHED} until one of those files, existence probes or listings changes, then it runs again. \`shipkit keys --why\` names them` }
|
|
38
|
+
if (u.ttl === 'forever')
|
|
39
|
+
return { v: 'until its declaration changes', inputs: [], help: 'a decided fact (`ttl: \'forever\'`): a clean answer is kept until the value it checks is declared differently, and it runs only while no answer exists for that value' }
|
|
38
40
|
if (u.ttl != null && u.ttl !== '') {
|
|
39
41
|
let every = u.ttl
|
|
40
42
|
try { every = spanWords(parseDuration(u.ttl)) } catch { /* an unparsable ttl is shown as written */ }
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// dropped when the run ends and the next poll brings the same facts back
|
|
9
9
|
// from the one place that owns them.
|
|
10
10
|
import { makeAutoObservable, runInAction } from 'mobx'
|
|
11
|
-
import type {
|
|
11
|
+
import type { RepoSt } from './repo-st.ts'
|
|
12
12
|
import { applyRunEvent, idleRun, startRun, stripText, type RunProgress } from './run-strip.ts'
|
|
13
13
|
import type { GateBlock, Resource, ResourceState, SlowSuite, CheckEvent, WhenAt } from './hub-types.ts'
|
|
14
14
|
import { WHEN_OPTION } from './check-words.ts'
|
|
@@ -42,6 +42,10 @@ export class ChecksSvc {
|
|
|
42
42
|
* stay after the run: they ARE the last output until the next one */
|
|
43
43
|
liveLines: string[] = []
|
|
44
44
|
private clearTimer: ReturnType<typeof setTimeout> | null = null
|
|
45
|
+
|
|
46
|
+
dispose(): void {
|
|
47
|
+
if (this.clearTimer != null) clearTimeout(this.clearTimer)
|
|
48
|
+
}
|
|
45
49
|
/** the managed block in repo.config.ts as the server last sent it: the
|
|
46
50
|
* hash the next write hands back, and whether the file is waiting for
|
|
47
51
|
* a commit. null until the first read */
|
|
@@ -56,7 +60,7 @@ export class ChecksSvc {
|
|
|
56
60
|
/** the slow suite form under the test check */
|
|
57
61
|
suite: SlowSuite = { file: '', catches: '', whyNotFast: '' }
|
|
58
62
|
|
|
59
|
-
constructor(private st:
|
|
63
|
+
constructor(private st: RepoSt) {
|
|
60
64
|
makeAutoObservable<this, 'st' | 'clearTimer'>(this, { st: false, clearTimer: false })
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -89,7 +93,7 @@ export class ChecksSvc {
|
|
|
89
93
|
* silent overwrite */
|
|
90
94
|
async loadGate(): Promise<void> {
|
|
91
95
|
try {
|
|
92
|
-
const r = await fetch('/api/checks/when')
|
|
96
|
+
const r = await fetch(this.st.url('/api/checks/when'))
|
|
93
97
|
if (!r.ok) throw new Error(await r.text())
|
|
94
98
|
const b = (await r.json()) as GateBlock
|
|
95
99
|
runInAction(() => { this.gate = b })
|
|
@@ -109,14 +113,14 @@ export class ChecksSvc {
|
|
|
109
113
|
// the hash of the file the page last read: without it a write
|
|
110
114
|
// could land on top of an edit this page never saw
|
|
111
115
|
if (this.gate == null) await this.loadGate()
|
|
112
|
-
const r = await fetch('/api/checks/when', { method: 'POST', body: JSON.stringify({ ...edit, expect: this.gate?.hash }) })
|
|
116
|
+
const r = await fetch(this.st.url('/api/checks/when'), { method: 'POST', body: JSON.stringify({ ...edit, expect: this.gate?.hash }) })
|
|
113
117
|
if (!r.ok) throw new Error(await r.text())
|
|
114
118
|
const b = (await r.json()) as GateBlock
|
|
115
119
|
runInAction(() => { this.gate = b; this.gateNote = said })
|
|
116
120
|
took = true
|
|
117
121
|
// the rows carry `when`, so they are right only after the server
|
|
118
122
|
// has read the config it just wrote
|
|
119
|
-
await this.st.cols.poll()
|
|
123
|
+
await this.st.cols.poll(true)
|
|
120
124
|
} catch (e) {
|
|
121
125
|
runInAction(() => { this.gateNote = `refused: ${(e as Error).message}` })
|
|
122
126
|
// whatever the file really says now, so the next try is not
|
|
@@ -227,7 +231,7 @@ export class ChecksSvc {
|
|
|
227
231
|
this.note = `running ${label}…`
|
|
228
232
|
})
|
|
229
233
|
try {
|
|
230
|
-
const r = await fetch('/api/check/run', { method: 'POST', body: JSON.stringify({ ...(only == null ? {} : { only }), ...(gate ? { gate } : {}) }) })
|
|
234
|
+
const r = await fetch(this.st.url('/api/check/run'), { method: 'POST', body: JSON.stringify({ ...(only == null ? {} : { only }), ...(gate ? { gate } : {}) }) })
|
|
231
235
|
const rd = r.body!.getReader()
|
|
232
236
|
const dec = new TextDecoder()
|
|
233
237
|
let buf = ''
|
|
@@ -270,9 +274,9 @@ export class ChecksSvc {
|
|
|
270
274
|
this.clearTimer = setTimeout(() => runInAction(() => { this.progress = idleRun() }), 5_000)
|
|
271
275
|
// the numbers a run just produced live on the server, in the log it
|
|
272
276
|
// wrote: one poll and the page is reading them rather than a memory
|
|
273
|
-
await this.st.cols.poll()
|
|
277
|
+
await this.st.cols.poll(true)
|
|
274
278
|
void this.st.health.load()
|
|
275
|
-
void this.st.timeline.load()
|
|
279
|
+
void this.st.timeline.load(true)
|
|
276
280
|
// a test page on screen reads the record the run just wrote
|
|
277
281
|
const sel = this.st.sel
|
|
278
282
|
if (sel?.t === 'tests') void this.st.tests.load(sel.check)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// the file wins).
|
|
6
6
|
import { runInAction } from 'mobx'
|
|
7
7
|
import { observer } from 'mobx-react-lite'
|
|
8
|
-
import type {
|
|
8
|
+
import type { RepoSt } from './repo-st.ts'
|
|
9
9
|
import type { Resource } from './hub-types.ts'
|
|
10
10
|
import { renderMd } from './md.ts'
|
|
11
11
|
|
|
@@ -24,7 +24,7 @@ const CredHead = ({ cred }: { cred: ReturnType<typeof credOf> }) => (
|
|
|
24
24
|
</>
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
export const ColCard = observer(({ app, col, id }: { app:
|
|
27
|
+
export const ColCard = observer(({ app, col, id }: { app: RepoSt; col: string; id: string }) => {
|
|
28
28
|
const c = app.cols
|
|
29
29
|
const i = c.find(col, id)
|
|
30
30
|
if (i == null) return <div className="ph">item vanished — the truth file moved on</div>
|
|
@@ -103,7 +103,7 @@ export const ColCard = observer(({ app, col, id }: { app: HubSt; col: string; id
|
|
|
103
103
|
|
|
104
104
|
// collection view — one card per visible instance, the stage-side twin of a
|
|
105
105
|
// sidebar section (every section gets both views)
|
|
106
|
-
export const ColGrid = observer(({ app, id }: { app:
|
|
106
|
+
export const ColGrid = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
107
107
|
const c = app.cols
|
|
108
108
|
const col = c.sidebarCols.find((x) => x.id === id)
|
|
109
109
|
if (col == null) return <div className="stagewrap"><div className="ph">collection vanished — the truth moved on</div></div>
|
|
@@ -155,7 +155,7 @@ const wireClass = (l: string): string => {
|
|
|
155
155
|
|
|
156
156
|
// the RAW view of a run: every check has its own page, and this is the one
|
|
157
157
|
// node that keeps the whole text reachable
|
|
158
|
-
export const StatusCard = observer(({ app }: { app:
|
|
158
|
+
export const StatusCard = observer(({ app }: { app: RepoSt }) => {
|
|
159
159
|
const h = app.health
|
|
160
160
|
const body = h.live.length > 0
|
|
161
161
|
? h.live.join('\n')
|
|
@@ -4,20 +4,40 @@
|
|
|
4
4
|
// file through /api/act/*; errors come back as 400 text shown verbatim on
|
|
5
5
|
// the card.
|
|
6
6
|
import { makeAutoObservable, runInAction } from 'mobx'
|
|
7
|
-
import type {
|
|
8
|
-
import type { Col, ColMeta, HubProblem, Resource
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
|
+
import type { Col, ColMeta, HubProblem, Resource } from './hub-types.ts'
|
|
9
|
+
import type { PlanRow } from '../../plan/planner.ts'
|
|
10
|
+
import { readScreen, SCREEN_WRITE_MS, writeScreen } from './screen-cache.ts'
|
|
11
|
+
|
|
12
|
+
/** what /api/resources.json answers */
|
|
13
|
+
type Answer = { collections: ColMeta[]; resources: Resource[]; planned?: PlanRow[] | null; hub?: HubProblem[] }
|
|
9
14
|
|
|
10
15
|
export class ColsSvc {
|
|
11
16
|
/** exactly what the server last sent; `resources` is this plus the live
|
|
12
17
|
* results of a run in flight */
|
|
13
18
|
fetched: Resource[] = []
|
|
14
19
|
colMeta: ColMeta[] = []
|
|
15
|
-
/**
|
|
16
|
-
|
|
20
|
+
/** this repo's rows of the plan, null when no plan covers it */
|
|
21
|
+
planned: PlanRow[] | null = null
|
|
17
22
|
/** what the server says is wrong with the hub itself */
|
|
18
23
|
hubProblems: HubProblem[] = []
|
|
19
24
|
/** the last poll failed: the page cannot reach its own server */
|
|
20
25
|
fetchErr: string | null = null
|
|
26
|
+
/** a first answer arrived: until then the tree and the page links show a
|
|
27
|
+
* spinner where they will be, never an empty repo */
|
|
28
|
+
loaded = false
|
|
29
|
+
/** what shows was kept by this browser from an earlier load, and the
|
|
30
|
+
* hub has not answered yet (screen-cache.ts) */
|
|
31
|
+
cached = false
|
|
32
|
+
/** the first media category the kept screen had, until the media
|
|
33
|
+
* heartbeat lists the assets */
|
|
34
|
+
cachedCat: string | null = null
|
|
35
|
+
/** when this repo's screen was last kept, 0 for never */
|
|
36
|
+
private keptAt = 0
|
|
37
|
+
/** the last answer, and the media category last kept with it */
|
|
38
|
+
private last: Answer | null = null
|
|
39
|
+
private keptCat: string | null | undefined = undefined
|
|
40
|
+
private restoring: Promise<void> | null = null
|
|
21
41
|
actErr = ''
|
|
22
42
|
remarkDraft = ''
|
|
23
43
|
proofDraft = ''
|
|
@@ -25,30 +45,89 @@ export class ColsSvc {
|
|
|
25
45
|
foldOverride = new Map<string, boolean>()
|
|
26
46
|
private timer: ReturnType<typeof setInterval> | null = null
|
|
27
47
|
|
|
28
|
-
constructor(private st:
|
|
29
|
-
makeAutoObservable<this, 'st' | 'timer'>(this, { st: false, timer: false })
|
|
48
|
+
constructor(private st: RepoSt) {
|
|
49
|
+
makeAutoObservable<this, 'st' | 'timer' | 'keptAt' | 'restoring' | 'readKept' | 'last' | 'keptCat'>(this, { st: false, timer: false, keptAt: false, restoring: false, readKept: false, last: false, keptCat: false })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** one check's row of the plan, null when no plan covers it */
|
|
53
|
+
planOf(check: string): PlanRow | null {
|
|
54
|
+
return this.planned?.find((r) => r.check === check) ?? null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** where this repo's screen is kept: its name, or `self` on a hub of its own */
|
|
58
|
+
private get screenKey(): string {
|
|
59
|
+
return this.st.key === '' ? 'self' : this.st.key
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** paint the screen this browser kept, unless the hub answered first.
|
|
63
|
+
* One read per repo, whoever asks */
|
|
64
|
+
restore(): Promise<void> {
|
|
65
|
+
this.restoring ??= this.readKept()
|
|
66
|
+
return this.restoring
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async readKept(): Promise<void> {
|
|
70
|
+
const s = await readScreen(this.screenKey)
|
|
71
|
+
if (s == null) return
|
|
72
|
+
runInAction(() => {
|
|
73
|
+
if (this.loaded) return
|
|
74
|
+
this.colMeta = s.collections
|
|
75
|
+
this.fetched = s.resources
|
|
76
|
+
this.planned = s.planned
|
|
77
|
+
this.hubProblems = s.hub
|
|
78
|
+
this.cachedCat = s.mediaCat
|
|
79
|
+
this.cached = true
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** keep the screen for the next load: the first answer, then at most
|
|
84
|
+
* once a minute. The answer as parsed, never the observable copy, which
|
|
85
|
+
* IndexedDB cannot clone */
|
|
86
|
+
private keep(j: Answer, cat: string | null = this.st.media.assets[0]?.cat ?? this.cachedCat, force = false): void {
|
|
87
|
+
this.last = j
|
|
88
|
+
const now = Date.now()
|
|
89
|
+
if (!force && now - this.keptAt < SCREEN_WRITE_MS) return
|
|
90
|
+
this.keptAt = now
|
|
91
|
+
this.keptCat = cat
|
|
92
|
+
void writeScreen(this.screenKey, { at: now, collections: j.collections, resources: j.resources, planned: j.planned ?? null, hub: j.hub ?? [], mediaCat: cat })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** the media heartbeat listed the assets: a first category the kept
|
|
96
|
+
* screen does not name yet is kept at once, so the header's media link
|
|
97
|
+
* is there on the next load */
|
|
98
|
+
mediaListed(): void {
|
|
99
|
+
const cat = this.st.media.assets[0]?.cat ?? null
|
|
100
|
+
if (this.last != null && cat !== this.keptCat) this.keep(this.last, cat, true)
|
|
30
101
|
}
|
|
31
102
|
|
|
32
103
|
start(): void {
|
|
104
|
+
if (this.timer != null) return
|
|
33
105
|
void this.poll()
|
|
34
106
|
this.timer = setInterval(() => void this.poll(), 10_000)
|
|
35
107
|
}
|
|
36
|
-
|
|
108
|
+
stop(): void {
|
|
37
109
|
if (this.timer != null) clearInterval(this.timer)
|
|
110
|
+
this.timer = null
|
|
38
111
|
}
|
|
39
112
|
|
|
40
|
-
|
|
113
|
+
/** `fresh`: this page just changed the truth (an act, a run that ended),
|
|
114
|
+
* so the answer waits on a walk that includes it rather than on the one
|
|
115
|
+
* the server keeps warm */
|
|
116
|
+
async poll(fresh = false): Promise<void> {
|
|
41
117
|
try {
|
|
42
|
-
const r = await fetch('/api/resources.json')
|
|
118
|
+
const r = await fetch(this.st.url(fresh ? '/api/resources.json?fresh=1' : '/api/resources.json'))
|
|
43
119
|
if (!r.ok) throw new Error(await r.text())
|
|
44
|
-
const j = (await r.json()) as
|
|
120
|
+
const j = (await r.json()) as Answer
|
|
45
121
|
runInAction(() => {
|
|
46
122
|
this.colMeta = j.collections
|
|
47
123
|
this.fetched = j.resources
|
|
48
|
-
this.
|
|
124
|
+
this.planned = j.planned ?? null
|
|
49
125
|
this.hubProblems = j.hub ?? []
|
|
50
126
|
this.fetchErr = null
|
|
127
|
+
this.loaded = true
|
|
128
|
+
this.cached = false
|
|
51
129
|
})
|
|
130
|
+
this.keep(j)
|
|
52
131
|
} catch (e) {
|
|
53
132
|
// a dead server must SAY so, in the header, never leave a tree
|
|
54
133
|
// that reads like a healthy repo. The last rows stay, dated by it
|
|
@@ -130,9 +209,9 @@ export class ColsSvc {
|
|
|
130
209
|
private async act(path: string, body: Record<string, unknown>): Promise<boolean> {
|
|
131
210
|
this.actErr = ''
|
|
132
211
|
try {
|
|
133
|
-
const r = await fetch(`/api/act/${path}
|
|
212
|
+
const r = await fetch(this.st.url(`/api/act/${path}`), { method: 'POST', body: JSON.stringify(body) })
|
|
134
213
|
if (!r.ok) throw new Error(await r.text())
|
|
135
|
-
await this.poll()
|
|
214
|
+
await this.poll(true)
|
|
136
215
|
return true
|
|
137
216
|
} catch (e) {
|
|
138
217
|
runInAction(() => { this.actErr = (e as Error).message })
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// it is something the panel SAID: the header never reports a state it guessed
|
|
5
5
|
// from how the iframe looks.
|
|
6
6
|
import { makeAutoObservable, runInAction } from 'mobx'
|
|
7
|
-
import type {
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
8
|
|
|
9
9
|
export type ComfyBox = { url: string; up: boolean; err: string | null }
|
|
10
10
|
/** what the panel says, narrowed to the messages this host acts on */
|
|
@@ -37,10 +37,16 @@ export class ComfySvc {
|
|
|
37
37
|
private mountedFor = ''
|
|
38
38
|
private frame: HTMLIFrameElement | null = null
|
|
39
39
|
private handshake: ReturnType<typeof setTimeout> | null = null
|
|
40
|
+
private readonly listener = (e: MessageEvent): void => this.onMessage(e)
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
dispose(): void {
|
|
43
|
+
window.removeEventListener('message', this.listener)
|
|
44
|
+
if (this.handshake != null) clearTimeout(this.handshake)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
constructor(private st: RepoSt) {
|
|
48
|
+
makeAutoObservable<this, 'st' | 'frame' | 'handshake' | 'listener'>(this, { st: false, frame: false, handshake: false, listener: false })
|
|
49
|
+
window.addEventListener('message', this.listener)
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
setFrame(el: HTMLIFrameElement | null): void {
|
|
@@ -78,7 +84,7 @@ export class ComfySvc {
|
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
async probeBox(): Promise<void> {
|
|
81
|
-
const box = await fetch('/api/comfy/box')
|
|
87
|
+
const box = await fetch(this.st.url('/api/comfy/box'))
|
|
82
88
|
.then((r) => (r.ok ? (r.json() as Promise<ComfyBox>) : null))
|
|
83
89
|
.catch(() => null)
|
|
84
90
|
runInAction(() => {
|
|
@@ -107,7 +113,7 @@ export class ComfySvc {
|
|
|
107
113
|
})
|
|
108
114
|
void this.probeBox()
|
|
109
115
|
try {
|
|
110
|
-
const r = await fetch('/api/comfy/panel', { method: 'POST', body: JSON.stringify({ restart }) })
|
|
116
|
+
const r = await fetch(this.st.url('/api/comfy/panel'), { method: 'POST', body: JSON.stringify({ restart }) })
|
|
111
117
|
if (!r.ok) throw new Error(await r.text())
|
|
112
118
|
const up = (await r.json()) as { url: string }
|
|
113
119
|
runInAction(() => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { observer } from 'mobx-react-lite'
|
|
5
5
|
import { useEffect } from 'react'
|
|
6
6
|
import type { ComfySvc } from './comfy-svc.ts'
|
|
7
|
-
import type {
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
8
|
|
|
9
9
|
const PANEL_CHIP: Record<ComfySvc['state'], { label: string; cls: string; title: string }> = {
|
|
10
10
|
idle: { label: 'idle', cls: '', title: 'nothing started yet' },
|
|
@@ -16,7 +16,7 @@ const PANEL_CHIP: Record<ComfySvc['state'], { label: string; cls: string; title:
|
|
|
16
16
|
|
|
17
17
|
// the same panel over an asset page. A popup, so the asset, its rolls and its
|
|
18
18
|
// prompt stay on screen behind it while a value is tried
|
|
19
|
-
export const ComfyOverlay = observer(({ app }: { app:
|
|
19
|
+
export const ComfyOverlay = observer(({ app }: { app: RepoSt }) => {
|
|
20
20
|
const c = app.comfy
|
|
21
21
|
if (!c.overlay) return null
|
|
22
22
|
const chip = PANEL_CHIP[c.state]
|
|
@@ -47,7 +47,7 @@ export const ComfyOverlay = observer(({ app }: { app: HubSt }) => {
|
|
|
47
47
|
)
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
export const ComfyView = observer(({ app, id }: { app:
|
|
50
|
+
export const ComfyView = observer(({ app, id }: { app: RepoSt; id: string }) => {
|
|
51
51
|
const c = app.comfy
|
|
52
52
|
const wf = id.replace(/^comfy:/, '')
|
|
53
53
|
useEffect(() => {
|