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
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
action, and red/orange/green reserved for STATE. No rule outside this
|
|
5
5
|
block writes a colour, and tests/hub-css-invariants.test.ts fails one
|
|
6
6
|
that does. */
|
|
7
|
-
:root{--bg:#0d0e10;--panel:#141619
|
|
7
|
+
:root{--bg:#0d0e10;--panel:#141619;
|
|
8
|
+
/* the surfaces of the regions, each a small step from the page so the rail,
|
|
9
|
+
the header and the dock read apart at a glance (no light theme: the page
|
|
10
|
+
is dark only, and colorDark is what it draws) */
|
|
11
|
+
--bg-side:#131519;--bg-top:#171a1f;--bg-dock:#10151b;--panel2:#1b1e22;--line:#282c31;--ink:#e2e4e7;--dim:#8d9299;
|
|
8
12
|
--accent:#6b8fb3;--sel:#1a2430;--chk1:#16181b;--chk2:#121417;
|
|
9
13
|
/* state, and nothing else: red broken · orange taste pending · green measured passing */
|
|
10
14
|
--red:#d2605c;--warn:#cf9a4e;--ok:#6faa86;--blue:#6f9bd1;
|
|
@@ -16,58 +20,91 @@
|
|
|
16
20
|
--k1:#8ca8c7;--k2:#a892c9;--k3:#6fb0b8;--k4:#c792b4;--k5:#9198a1;--k6:#8f95cf;
|
|
17
21
|
--wash:rgba(107,143,179,.15);--wash2:rgba(107,143,179,.11);--hair:rgba(107,143,179,.5);
|
|
18
22
|
--errbg:#2a1a1a;--errline:#4a2c2c;--errink:#e3bebe;
|
|
23
|
+
/* a running thing's wash, the green twin of errbg and errline */
|
|
24
|
+
--okbg:#15251c;--okline:#2f5a40;
|
|
19
25
|
/* the ONE spacing scale: every padding, margin and gap in this sheet is a
|
|
20
26
|
value from it or 0, and tests/hub-css-invariants.test.ts fails the rest */
|
|
21
27
|
--s0:2px;--s1:4px;--s2:8px;--s3:12px;--s4:16px}
|
|
22
28
|
*{box-sizing:border-box}html,body,#root{height:100%}
|
|
23
29
|
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans-serif;overflow:hidden}
|
|
24
|
-
/*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
+
/* a link with no class of its own: the browser default is dark blue on this dark page */
|
|
31
|
+
a,a:visited{color:var(--accent)}
|
|
32
|
+
/* the page is the window's height, the dock under it (hub-app.tsx): over
|
|
33
|
+
the dock two panels, the project list, then .appcol, which stacks the
|
|
34
|
+
strip, the header and the body */
|
|
35
|
+
.appv{height:100vh}
|
|
36
|
+
.app{height:100%}
|
|
37
|
+
/* a panel's class lands on the div inside the panel (react-resizable-panels) */
|
|
38
|
+
.pjpanel,.appcol,.pside,.pmain{height:100%;min-width:0}
|
|
39
|
+
.apptop,.pdock{height:100%;min-height:0}
|
|
40
|
+
.pjpanel{display:flex}
|
|
41
|
+
.appcol{display:flex;flex-direction:column}
|
|
42
|
+
/* one region per repo visited, the shown one alone displayed */
|
|
43
|
+
.repo{flex:1;min-height:0;display:flex;flex-direction:column}
|
|
44
|
+
.repo[hidden]{display:none}
|
|
45
|
+
.appbody{flex:1;min-height:0}
|
|
46
|
+
.pside{display:flex;flex-direction:column}
|
|
47
|
+
.pside > aside{flex:1}
|
|
48
|
+
.pmain{display:flex;flex-direction:column}
|
|
49
|
+
.pmain > main{flex:1}
|
|
30
50
|
|
|
31
|
-
/* ──
|
|
32
|
-
.hdr
|
|
51
|
+
/* ── a repo's bar: on the home the strip (home-strip.css .hstrip), on a hub
|
|
52
|
+
of its own the header (.hdr). Its name, what runs now and the two runs,
|
|
53
|
+
then its processes, its trouble and its editor icons. One line at any
|
|
54
|
+
width: the container queries shrink the labels to their icons, then the
|
|
55
|
+
run state to its spinner ── */
|
|
56
|
+
/* the repo's processes (serve.processes): a dot, its id, a link once up */
|
|
57
|
+
.hprocs{display:inline-flex;gap:var(--s1);align-items:center}
|
|
58
|
+
.hproc{display:inline-flex;align-items:center;gap:var(--s0)}
|
|
59
|
+
.hproc button{padding:var(--s0) var(--s2);font-size:12px}
|
|
60
|
+
.hproc a{color:var(--accent);text-decoration:none;font-size:12px}
|
|
61
|
+
.pdot{display:inline-block;width:7px;height:7px;border-radius:4px;margin-right:var(--s1);background:var(--line)}
|
|
62
|
+
.pdot.up{background:var(--ok)}.pdot.starting,.pdot.stopping{background:var(--warn)}.pdot.crashed{background:var(--red)}
|
|
63
|
+
.hdr{display:flex;gap:var(--s2);align-items:center;height:36px;flex:none;padding:0 var(--s3);border-bottom:1px solid var(--line);background:var(--bg-top)}
|
|
33
64
|
.hdr .sp{flex:1}
|
|
34
65
|
.hname{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);font-weight:600;margin-right:var(--s2);white-space:nowrap}
|
|
35
|
-
.
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.
|
|
66
|
+
.hdr,.hstrip{container-type:inline-size;white-space:nowrap}
|
|
67
|
+
.hrun{display:inline-flex;align-items:center;gap:var(--s1);flex:none;margin-left:var(--s2)}
|
|
68
|
+
.hbtn{display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s2);font-size:11.5px;white-space:nowrap;flex:none}
|
|
69
|
+
.hbtn .hico{width:13px;height:13px}
|
|
70
|
+
/* the editor icons: bare, dim, lit on hover, as the activity page's Fork icon */
|
|
71
|
+
.hicon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:24px;height:22px;padding:0;border:0;background:none;color:var(--dim)}
|
|
72
|
+
.hicon:hover{color:var(--accent)}
|
|
73
|
+
.hicon .hico{width:15px;height:15px}
|
|
39
74
|
/* the run state holds its width whatever it says, so a run never moves the buttons beside it */
|
|
40
75
|
.hstatus{max-width:420px;flex:0 1 auto;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
41
76
|
.hstatus.red{color:var(--red)}
|
|
42
77
|
.hstatus.warn{color:var(--warn)}
|
|
43
|
-
|
|
78
|
+
/* its words sit against the run buttons, cut with an ellipsis */
|
|
79
|
+
.hstate{display:inline-flex;gap:var(--s1);align-items:center;width:200px;min-width:200px;flex:none;justify-content:flex-end;font-size:11.5px;color:var(--dim);overflow:hidden}
|
|
44
80
|
.hstate.on{color:var(--ink)}
|
|
45
|
-
.hstate .htext{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
81
|
+
.hstate .htext{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
82
|
+
.hstate .spin.idle{visibility:hidden;animation:none}
|
|
83
|
+
/* after .hstate, so a narrow bar wins over its width */
|
|
84
|
+
@container (max-width:600px){.hbtn .lbl{display:none}.hbtn{padding:0 var(--s1)}}
|
|
85
|
+
@container (max-width:520px){.hstate{width:120px;min-width:120px}}
|
|
86
|
+
@container (max-width:420px){.hstate .htext{display:none}.hstate{width:16px;min-width:16px}}
|
|
46
87
|
|
|
47
|
-
/*
|
|
48
|
-
whatever runs; unfolded, the chart at the height its lanes hold, set
|
|
49
|
-
inline from a held value that grows at once and shrinks only after a
|
|
50
|
-
while, so it never jumps every second. The strip clips nothing itself, so
|
|
51
|
-
its (?) opens below it over the page; the chart box clips the chart ── */
|
|
52
|
-
.live{display:flex;flex-direction:column;height:30px;flex:none;border-bottom:1px solid var(--line);background:var(--bg);font-size:11px;color:var(--dim)}
|
|
53
|
-
.live .hbub{top:calc(100% + 6px);bottom:auto;z-index:30}
|
|
88
|
+
/* a row of a (?) legend: one glyph or word, then its meaning */
|
|
54
89
|
.lrow{display:flex;gap:var(--s2);align-items:center;height:29px;flex:none;padding:0 var(--s3)}
|
|
55
|
-
.lrow .sp{flex:1}
|
|
56
|
-
.lfold{display:inline-flex;gap:var(--s1);align-items:center;border-color:transparent;color:var(--dim);padding:var(--s0) var(--s1)}
|
|
57
|
-
.lfold .sk{font-size:9px;opacity:.55}
|
|
58
|
-
.lwho{width:180px;flex:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink)}
|
|
59
|
-
.live.idle .lwho{color:var(--dim)}
|
|
60
|
-
.lwho i{font-style:normal;color:var(--dim);margin-left:var(--s1)}
|
|
61
|
-
.lwho b{font-weight:500;color:var(--accent);margin-left:var(--s1)}
|
|
62
|
-
.lchart{flex:1;min-width:0;height:21px;border:1px solid var(--line);border-radius:3px;overflow:hidden}
|
|
63
|
-
.lopen{flex:1;min-height:0;overflow-y:auto;padding:0 var(--s3);scrollbar-width:thin;scrollbar-color:var(--line) transparent}
|
|
64
|
-
.lnote,.lmore{flex:none;color:var(--warn);white-space:nowrap}
|
|
65
|
-
.lbusy{flex:none;white-space:nowrap}
|
|
66
90
|
|
|
67
|
-
/* ──
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
/* ── the left pane: the sections, the filter, then the tree, the one part
|
|
92
|
+
that scrolls ── */
|
|
93
|
+
aside{border-right:1px solid var(--line);background:var(--panel);min-height:0;display:flex;flex-direction:column;overflow:hidden}
|
|
94
|
+
aside > *{flex:none}
|
|
95
|
+
aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
|
|
96
|
+
/* a repo's sections: the project list's lines (project-list.css .pjpl), a
|
|
97
|
+
little shorter, the current one lit and marked in the accent */
|
|
98
|
+
.rsecs{display:flex;flex-direction:column;padding:var(--s1) 0;border-bottom:1px solid var(--line)}
|
|
99
|
+
.rsec{display:flex;align-items:center;gap:var(--s2);width:100%;height:26px;padding:0 var(--s3);background:transparent;border:0;border-radius:0;text-align:left;color:var(--dim);font-size:13px;cursor:pointer}
|
|
100
|
+
.rsec:hover{background:var(--panel2);color:var(--ink);border-color:transparent}
|
|
101
|
+
.rsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
|
|
102
|
+
.rsec .hico{flex:none;width:16px;height:16px}
|
|
103
|
+
.rsecl{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
104
|
+
.rsecn{flex:none;min-width:16px;height:16px;padding:0 var(--s1);border-radius:8px;font-size:10px;font-weight:700;line-height:16px;text-align:center;color:var(--bg)}
|
|
105
|
+
.rsecn.nred{background:var(--red)}
|
|
106
|
+
.rsecn.nwarn{background:var(--warn)}
|
|
107
|
+
.rsec:hover .pjkey,.rsec.cur .pjkey{opacity:1}
|
|
71
108
|
/* segmented severity filter — all · needs · red. A thin border and an
|
|
72
109
|
underline on the live one: no fill, no pill */
|
|
73
110
|
.seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden;flex:none}
|
|
@@ -78,10 +115,16 @@ aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto
|
|
|
78
115
|
.seg button.on{color:var(--accent);border-bottom-color:var(--accent)}
|
|
79
116
|
.seg button .sk{margin-left:var(--s1);font-size:9px;opacity:.55}
|
|
80
117
|
.swhen{padding:0 var(--s3) var(--s2);font-size:11px;color:var(--dim)}
|
|
81
|
-
|
|
118
|
+
/* a narrow pane puts the severity filter under the input, never over it */
|
|
119
|
+
.ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3)}
|
|
120
|
+
.sfield{position:relative;flex:1 1 96px;min-width:0;display:flex}
|
|
82
121
|
.ssearch input{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:4px;color:var(--ink);
|
|
83
|
-
padding:var(--
|
|
122
|
+
padding:var(--s0) var(--s4) var(--s0) var(--s2);font-size:12px;outline:none}
|
|
123
|
+
.ssearch input::placeholder{color:var(--dim);opacity:.6;font-size:11.5px}
|
|
84
124
|
.ssearch input:focus{border-color:var(--accent)}
|
|
125
|
+
/* its key, the way the project list prints its keys (.pjkey) */
|
|
126
|
+
.skey{position:absolute;right:var(--s2);top:50%;transform:translateY(-50%);pointer-events:none;font:10px/1 ui-monospace,Menlo,monospace;color:var(--dim);opacity:.6;background:none;border:0;padding:0}
|
|
127
|
+
.sfield input:focus + .skey{opacity:0}
|
|
85
128
|
/* ── the resource tree: ONE row shape, a fixed grid. The indent rides in
|
|
86
129
|
the FIRST column and the label eats the slack, so every right side slot
|
|
87
130
|
lines up down the whole column whatever the depth. Colour is on the
|
|
@@ -135,10 +178,11 @@ aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto
|
|
|
135
178
|
.tnote b{font-weight:600;color:var(--red)}
|
|
136
179
|
.stabs{display:flex;gap:var(--s2);align-items:center;padding:var(--s1) var(--s3);border-bottom:1px solid var(--line)}
|
|
137
180
|
.stabs .sp{flex:1}
|
|
138
|
-
.stabs .cnt{font-size:11px;color:var(--dim)}
|
|
181
|
+
.stabs .cnt{min-width:4ch;text-align:right;font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums}
|
|
139
182
|
.catgen{display:inline-flex;gap:var(--s1)}
|
|
140
183
|
.catgen button{padding:var(--s0) var(--s2);font-size:11px;font-weight:700}
|
|
141
184
|
.tempty{padding:var(--s2) var(--s3);font-size:12px;color:var(--dim);white-space:pre-wrap}
|
|
185
|
+
.twait{display:flex;align-items:center;gap:var(--s2)}
|
|
142
186
|
.warnline{color:var(--red)}
|
|
143
187
|
.fold{cursor:pointer;color:var(--dim);font-size:15px;display:inline-block;width:22px;text-align:center;
|
|
144
188
|
padding:var(--s0);border-radius:3px;user-select:none}
|
|
@@ -373,7 +417,6 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
|
|
|
373
417
|
.tlb.bad,.tlb.red{fill:var(--red)}
|
|
374
418
|
.tlb.rep{fill:var(--line)}
|
|
375
419
|
.tlb.run{fill:var(--accent);animation:pulse 1.6s ease-in-out infinite}
|
|
376
|
-
.tlb.repo{fill:var(--k6)}
|
|
377
420
|
.tlband.run{stroke:var(--accent)}
|
|
378
421
|
.tlconc{fill:var(--hair)}
|
|
379
422
|
.tlconc.over{fill:var(--warn)}
|
|
@@ -388,9 +431,9 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
|
|
|
388
431
|
.tlgap.live text{fill:var(--ink)}
|
|
389
432
|
.tlgap text{font-size:10px;pointer-events:none}
|
|
390
433
|
.tlnow{stroke:var(--accent)}
|
|
391
|
-
/*
|
|
392
|
-
clips its overflow, and a tooltip inside them is cut to a sliver */
|
|
393
|
-
.tlaxtip{
|
|
434
|
+
/* placed by floating-ui under the pointer, in a portal: every box of the
|
|
435
|
+
strip clips its overflow, and a tooltip inside them is cut to a sliver */
|
|
436
|
+
.tlaxtip{z-index:30;display:flex;flex-direction:column;gap:0;padding:var(--s1) var(--s2);pointer-events:none;
|
|
394
437
|
background:var(--panel);border:1px solid var(--line);border-radius:3px;box-shadow:0 2px 12px var(--shadow);font-size:11px;white-space:nowrap}
|
|
395
438
|
.tlaxtip .dim{color:var(--dim)}
|
|
396
439
|
/* the runs page: one tab bar, then one list. Every line on a grid of fixed
|
|
@@ -422,6 +465,8 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
|
|
|
422
465
|
per test check (the tests page), the failing files with their failure line */
|
|
423
466
|
.rtime{grid-template-columns:minmax(0,1fr) 88px 72px 128px 72px 40px 88px}
|
|
424
467
|
.rcheckt{grid-template-columns:152px 88px 48px 56px minmax(0,1fr) 72px}
|
|
468
|
+
/* every test file: file, check, duration, status, age */
|
|
469
|
+
.rall{grid-template-columns:minmax(0,1fr) 152px 88px 52px 72px}
|
|
425
470
|
.rhead .tleft{text-align:left}
|
|
426
471
|
.tshare{display:grid;grid-template-columns:minmax(0,1fr) 36px;gap:var(--s1);align-items:center}
|
|
427
472
|
.ttrack{display:block;height:6px;border-radius:1px;overflow:hidden;background:var(--panel2)}
|
|
@@ -542,17 +587,18 @@ a.tok{color:var(--accent)}
|
|
|
542
587
|
height, so a streaming run never jumps the card it sits in */
|
|
543
588
|
.wlog{margin:0;min-height:120px;max-height:320px;overflow-y:auto;background:var(--panel2);border:1px solid var(--line);
|
|
544
589
|
border-radius:3px;padding:var(--s2);font:11.5px/1.5 ui-monospace,Menlo,monospace;color:var(--ink);white-space:pre-wrap}
|
|
545
|
-
/* THE HELP: a `(?)` that says its one sentence
|
|
546
|
-
|
|
590
|
+
/* THE HELP: a `(?)` that says its one sentence on hover and on focus, the
|
|
591
|
+
bubble placed by floating-ui (floating.tsx) */
|
|
547
592
|
.hlp{display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;flex:none;
|
|
548
593
|
border:1px solid var(--line);border-radius:50%;color:var(--dim);font-size:9px;line-height:1;cursor:help;
|
|
549
|
-
|
|
594
|
+
outline:none;user-select:none}
|
|
550
595
|
.hlp:hover,.hlp:focus{color:var(--accent);border-color:var(--accent)}
|
|
551
|
-
|
|
596
|
+
/* out of sight, still read by a screen reader and found by a page search */
|
|
597
|
+
.vh{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
|
|
598
|
+
.hbub,.ftip{z-index:40;width:max-content;
|
|
552
599
|
max-width:260px;background:var(--panel2);border:1px solid var(--line);border-radius:3px;box-shadow:0 2px 10px var(--shadow);
|
|
553
600
|
padding:var(--s1) var(--s2);font:11.5px/1.4 system-ui,sans-serif;text-transform:none;letter-spacing:0;
|
|
554
|
-
color:var(--ink);white-space:normal;
|
|
555
|
-
.hlp:hover .hbub,.hlp:focus .hbub{display:block}
|
|
601
|
+
color:var(--ink);white-space:normal;pointer-events:none}
|
|
556
602
|
/* the type legend: every glyph of the tree with its word, in one bubble */
|
|
557
603
|
.legend{display:flex;flex-direction:column;gap:var(--s1)}
|
|
558
604
|
.legend .lrow{display:flex;gap:var(--s2);align-items:baseline;white-space:normal}
|
|
@@ -596,7 +642,9 @@ a.tok{color:var(--accent)}
|
|
|
596
642
|
.graphview{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3) var(--s4) var(--s4)}
|
|
597
643
|
.gvhead{display:flex;gap:var(--s2);align-items:center;font-size:13px;min-width:0}
|
|
598
644
|
.gvhead .gvtext{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
599
|
-
.
|
|
645
|
+
.gvframe{flex:1;min-height:0;position:relative;display:flex;border:1px solid var(--line);border-radius:3px;background:var(--bg)}
|
|
646
|
+
.gvwait{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:var(--s2);font-size:12px;color:var(--dim)}
|
|
647
|
+
.graphview iframe{position:relative;flex:1;min-height:0;width:100%;border:0;background:transparent}
|
|
600
648
|
|
|
601
649
|
/* ── comfy item view (comfy-ts's own panel fills the stage) ── */
|
|
602
650
|
.comfyview{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3) var(--s4) var(--s4)}
|
|
@@ -646,7 +694,7 @@ a.mint{color:var(--accent);font-size:12px;text-decoration:none}
|
|
|
646
694
|
a.mint:hover{text-decoration:underline}
|
|
647
695
|
.colcard h3 .stchip,.colcard h3 a.mint{margin-left:var(--s2)}
|
|
648
696
|
|
|
649
|
-
/* ── cmd+
|
|
697
|
+
/* ── the cmd+k picker ── */
|
|
650
698
|
.palov{position:fixed;inset:0;background:var(--scrim);z-index:100;display:flex;justify-content:center;align-items:flex-start;
|
|
651
699
|
padding-top:14vh}
|
|
652
700
|
.pal{width:min(560px,90vw);background:var(--panel);border:1px solid var(--line);border-radius:3px;
|
|
@@ -658,6 +706,9 @@ a.mint:hover{text-decoration:underline}
|
|
|
658
706
|
.prow.on{background:var(--panel2)}
|
|
659
707
|
.prow.none{color:var(--dim);cursor:default}
|
|
660
708
|
.prow .plabel{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
709
|
+
/* the entry's icon: a place's button icon, a repo's own (project-list.tsx Face) */
|
|
710
|
+
.prow .pface{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;color:var(--dim)}
|
|
711
|
+
.prow .pface img,.prow .pface .pgi{width:16px;height:16px;border-radius:4px;object-fit:contain}
|
|
661
712
|
.prow .pchip{font-size:10px;color:var(--dim);background:var(--bg);border:1px solid var(--line);border-radius:5px;
|
|
662
713
|
padding:var(--s0) var(--s1);text-transform:uppercase;letter-spacing:.08em;flex:none}
|
|
663
714
|
|
|
@@ -669,3 +720,12 @@ a.mint:hover{text-decoration:underline}
|
|
|
669
720
|
pre{background:var(--panel);border:1px solid var(--line);border-radius:3px;padding:var(--s4);overflow-x:auto;
|
|
670
721
|
font:12.5px/1.55 ui-monospace,Menlo,monospace;white-space:pre-wrap}
|
|
671
722
|
.ok{color:var(--ok)}.warn{color:var(--warn)}.fail{color:var(--red)}.dim{color:var(--dim)}
|
|
723
|
+
|
|
724
|
+
/* ── the admin page: one row per control, its sentence under its name ── */
|
|
725
|
+
.adrow{display:flex;align-items:center;gap:var(--s4);padding:var(--s3) 0;border-top:1px solid var(--line)}
|
|
726
|
+
.adtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--s0);font-size:13px}
|
|
727
|
+
.adtext .dim{font-size:12px}
|
|
728
|
+
.adctl{flex:none;display:flex;align-items:center}
|
|
729
|
+
.adflag{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px;color:var(--dim)}
|
|
730
|
+
.adflag input{width:auto;margin:0}
|
|
731
|
+
.adconfirm{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// the few line icons of the home (the overview's section headers, the places
|
|
2
|
+
// on top of the project list, a repo page's sections and its top bar): one 20
|
|
3
|
+
// unit path each, stroked in the text colour, so the page's tokens colour them
|
|
4
|
+
// and nothing else does.
|
|
5
|
+
export const ICON = {
|
|
6
|
+
shipkit: 'M3 7.5 10 4l7 3.5v5L10 16l-7-3.5zM3 7.5 10 11l7-3.5M10 11v5',
|
|
7
|
+
process: 'M2 10h3.5l2-5 5 10 2-5H18',
|
|
8
|
+
monitor: 'M3 4h14v9H3zM7 16.5h6M10 13v3.5',
|
|
9
|
+
archived: 'M2.5 4h15v3.5h-15zM4 7.5V16h12V7.5M8 10.5h4',
|
|
10
|
+
home: 'M3 9.5 10 3.5l7 6M5 8v8.5h10V8M8.5 16.5v-4.5h3v4.5',
|
|
11
|
+
planned: 'M5.5 3h9M5.5 17h9M6.5 3c0 4 7 4 7 7s-7 3-7 7M13.5 3c0 4-7 4-7 7s7 3 7 7',
|
|
12
|
+
browser: 'M10 2.5a7.5 7.5 0 1 0 0 15a7.5 7.5 0 1 0 0-15M2.5 10h15M10 2.5c-3 3-3 12 0 15M10 2.5c3 3 3 12 0 15',
|
|
13
|
+
window: 'M2.5 4h15v12h-15zM2.5 7.5h15M5 5.75h.01M7 5.75h.01',
|
|
14
|
+
activity: 'M6 3v14M6 6.5a2 2 0 1 0 0 .01M6 13.5a2 2 0 1 0 0 .01M10 6.5h7M10 13.5h7M10 10h5',
|
|
15
|
+
fork: 'M6 5v10M6 3a2 2 0 1 0 0 .01M6 17a2 2 0 1 0 0 .01M14 5a2 2 0 1 0 0 .01M14 7c0 4.5-8 3.5-8 7',
|
|
16
|
+
search: 'M8.5 3a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0-11M12.5 12.5 17 17',
|
|
17
|
+
// a repo page's sections, then its two runs and its editor
|
|
18
|
+
checks: 'M4 10.5 7.5 14 16 5.5',
|
|
19
|
+
runs: 'M3 5h8M3 10h12M3 15h6',
|
|
20
|
+
asks: 'M3 4h14v9H8l-4 3.5V13H3z',
|
|
21
|
+
tests: 'M8 3h4M8.5 3v5L4 16.5h12L11.5 8V3M6.2 12.5h7.6',
|
|
22
|
+
media: 'M3 4h14v12H3zM3 13l4-4 4 4 2-2 4 4M13 7.5h.01',
|
|
23
|
+
wikis: 'M10 5.5C8.5 4 6 3.5 3 4v11c3-.5 5.5 0 7 1.5c1.5-1.5 4-2 7-1.5V4c-3-.5-5.5 0-7 1.5zM10 5.5v11',
|
|
24
|
+
admin: 'M4 5h12M4 10h12M4 15h12M7 3.5v3M13 8.5v3M9 13.5v3',
|
|
25
|
+
health: 'M2 10h3.5l2-5 5 10 2-5H18',
|
|
26
|
+
gate: 'M10 2.5 16 5v4.5c0 4-2.7 6.5-6 8c-3.3-1.5-6-4-6-8V5zM7.5 10l2 2 3.5-3.5',
|
|
27
|
+
// the VS Code mark, its ribbon drawn as one outline
|
|
28
|
+
vscode: 'M14 2.5l3.5 1.7v11.6L14 17.5zM14 2.5 4.5 12.3 2.5 11V9l2-1.3L14 17.5M14 6.8 10.2 10l3.8 3.2',
|
|
29
|
+
sort: 'M6 3.5v13M3 13.5l3 3 3-3M14 16.5v-13M11 6.5l3-3 3 3',
|
|
30
|
+
} as const
|
|
31
|
+
|
|
32
|
+
export const Icon = ({ d }: { d: string }) => (
|
|
33
|
+
<svg className="hico" viewBox="0 0 20 20" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d={d} /></svg>
|
|
34
|
+
)
|
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>hub</title>
|
|
7
7
|
<link rel="stylesheet" href="./hub.css" />
|
|
8
|
-
<link rel="stylesheet" href="./project-
|
|
8
|
+
<link rel="stylesheet" href="./project-list.css" />
|
|
9
9
|
<link rel="stylesheet" href="./prose-hits.css" />
|
|
10
|
+
<link rel="stylesheet" href="./home-strip.css" />
|
|
11
|
+
<link rel="stylesheet" href="./planned.css" />
|
|
12
|
+
<link rel="stylesheet" href="./activity.css" />
|
|
13
|
+
<link rel="stylesheet" href="./readme.css" />
|
|
14
|
+
<link rel="stylesheet" href="./dock.css" />
|
|
15
|
+
<link rel="stylesheet" href="./controls.css" />
|
|
10
16
|
</head>
|
|
11
17
|
<body>
|
|
12
18
|
<div id="root"></div>
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
|
3
|
+
// the overview, ⌘K the picker, ⌘L the activity, ⌘' the plan. ⌘B cycles the
|
|
4
|
+
// dock at the bottom of the page (dock-st.ts): the line, slim, open.
|
|
5
|
+
// A digit and the quote are read from the physical key (`code`), so an
|
|
6
|
+
// AZERTY keyboard, where the key under 1 types `&`, selects the same repo. Ctrl stands in for
|
|
7
|
+
// ⌘ off a Mac. The macOS app binds none of these to a menu item, so they
|
|
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' }
|
|
10
|
+
|
|
11
|
+
type KeyLike = { key: string; code: string; metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; altKey: boolean }
|
|
12
|
+
|
|
13
|
+
export function pageKey(e: KeyLike): PageKey | null {
|
|
14
|
+
if (!(e.metaKey || e.ctrlKey) || e.altKey) return null
|
|
15
|
+
const digit = /^Digit([1-9])$/.exec(e.code)
|
|
16
|
+
if (digit != null) return { t: 'repo', n: Number(digit[1]) }
|
|
17
|
+
if (e.shiftKey) return null
|
|
18
|
+
if (e.code === 'Quote' || e.key === "'") return { t: 'planned' }
|
|
19
|
+
const k = e.key.toLowerCase()
|
|
20
|
+
if (k === 'j') return { t: 'home' }
|
|
21
|
+
if (k === 'k') return { t: 'picker' }
|
|
22
|
+
if (k === 'l') return { t: 'activity' }
|
|
23
|
+
if (k === 'b') return { t: 'dock' }
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** the shortcut each place of the home carries on its button */
|
|
28
|
+
export const PLACE_KEY = { overview: '⌘J', planned: "⌘'", activity: '⌘L' } as const
|
|
29
|
+
/** the picker's shortcut, on the second button, after home */
|
|
30
|
+
export const PICKER_KEY = '⌘K'
|
|
31
|
+
/** the dock's shortcut, on the dock's own header */
|
|
32
|
+
export const DOCK_KEY = '⌘B'
|
|
33
|
+
|
|
34
|
+
/** the tree's filter, on a repo page: a plain `/` focuses it, and its
|
|
35
|
+
* badge at the input's right prints this */
|
|
36
|
+
export const SEARCH_KEY = '/'
|
|
37
|
+
|
|
38
|
+
export function isSearchKey(e: Pick<KeyLike, 'key' | 'metaKey' | 'ctrlKey' | 'altKey'>): boolean {
|
|
39
|
+
return e.key === SEARCH_KEY && !e.metaKey && !e.ctrlKey && !e.altKey
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** a repo page's sections, each on ⌥ and its letter. ⌥ because every other
|
|
43
|
+
* layer is spoken for: the plain letters are the severity filter (a n r)
|
|
44
|
+
* and the chart's follow key, ⌘ with these letters is copy, reload, select
|
|
45
|
+
* all, new tab, minimize and close in the browser and the macOS app, and ⌥
|
|
46
|
+
* plus a letter only types a character inside a text field, where the page
|
|
47
|
+
* leaves the key alone. Read from the physical key (`code`): on a Mac ⌥C
|
|
48
|
+
* types ç, and AZERTY moves the letters */
|
|
49
|
+
export type SectionId = 'checks' | 'runs' | 'asks' | 'tests' | 'media' | 'wikis' | 'admin'
|
|
50
|
+
export const SECTION_LETTER: Record<SectionId, string> = { checks: 'C', runs: 'R', asks: 'A', tests: 'T', media: 'M', wikis: 'W', admin: 'D' }
|
|
51
|
+
/** the badge at the end of a section's line */
|
|
52
|
+
export const sectionKeyLabel = (id: SectionId): string => `⌥${SECTION_LETTER[id]}`
|
|
53
|
+
|
|
54
|
+
export function sectionKey(e: KeyLike): SectionId | null {
|
|
55
|
+
if (!e.altKey || e.metaKey || e.ctrlKey || e.shiftKey) return null
|
|
56
|
+
const m = /^Key([A-Z])$/.exec(e.code)
|
|
57
|
+
if (m == null) return null
|
|
58
|
+
const hit = Object.entries(SECTION_LETTER).find(([, l]) => l === m[1])
|
|
59
|
+
return hit == null ? null : (hit[0] as SectionId)
|
|
60
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// the page's two resizable widths, a node of the root (home-st.ts): the
|
|
2
|
+
// project list at the far left and a repo's own sidebar. Each is dragged by
|
|
3
|
+
// its separator (react-resizable-panels) and kept per browser in px, so the
|
|
4
|
+
// next load renders at that width from its first paint. A stored value that
|
|
5
|
+
// is not a number, or is out of bounds, reads as the default: no runtime
|
|
6
|
+
// value can hand the page a list wider than LIST.max.
|
|
7
|
+
import { makeAutoObservable } from 'mobx'
|
|
8
|
+
|
|
9
|
+
export type Bounds = { key: string; min: number; max: number; def: number }
|
|
10
|
+
|
|
11
|
+
/** the project list: icon and name on one line */
|
|
12
|
+
export const LIST: Bounds = { key: 'hub:list-width', min: 140, max: 360, def: 200 }
|
|
13
|
+
/** a repo's sidebar: the resource tree */
|
|
14
|
+
export const SIDEBAR: Bounds = { key: 'hub:sidebar-width', min: 220, max: 640, def: 330 }
|
|
15
|
+
|
|
16
|
+
/** a width in its bounds, or the default for anything else */
|
|
17
|
+
export const clampWidth = (v: unknown, b: Bounds): number => {
|
|
18
|
+
const n = typeof v === 'string' ? Number(v) : typeof v === 'number' ? v : NaN
|
|
19
|
+
return Number.isFinite(n) && n >= b.min && n <= b.max ? Math.round(n) : b.def
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loadWidth(b: Bounds): number {
|
|
23
|
+
try {
|
|
24
|
+
return clampWidth(globalThis.localStorage?.getItem(b.key), b)
|
|
25
|
+
} catch {
|
|
26
|
+
return b.def
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function saveWidth(b: Bounds, px: number): void {
|
|
31
|
+
try {
|
|
32
|
+
globalThis.localStorage?.setItem(b.key, String(px))
|
|
33
|
+
} catch { /* private window or blocked site data: the width lasts the tab */ }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class LayoutSt {
|
|
37
|
+
listWidth = loadWidth(LIST)
|
|
38
|
+
sidebarWidth = loadWidth(SIDEBAR)
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
makeAutoObservable(this)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** a drag of the list's separator ended at this width */
|
|
45
|
+
setListWidth(px: number): void {
|
|
46
|
+
this.listWidth = clampWidth(Math.round(px), LIST)
|
|
47
|
+
saveWidth(LIST, this.listWidth)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** a drag of the sidebar's separator ended at this width */
|
|
51
|
+
setSidebarWidth(px: number): void {
|
|
52
|
+
this.sidebarWidth = clampWidth(Math.round(px), SIDEBAR)
|
|
53
|
+
saveWidth(SIDEBAR, this.sidebarWidth)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// (chart-view.ts, timeline-chart.tsx).
|
|
1
|
+
// A repo's live runs, as the page holds them: `/api/live.json` read every
|
|
2
|
+
// second while the repo shows (the runs of the last ten minutes and the ones
|
|
3
|
+
// in flight), which the header's run state reads, and the chart of the runs
|
|
4
|
+
// page over it: the whole record (the recent runs of /api/live.json over the
|
|
5
|
+
// older ones of /api/timeline.json), opened on its last ten minutes and fit
|
|
6
|
+
// to all of it on demand (chart-view.ts, timeline-chart.tsx). What runs in
|
|
7
|
+
// every repo at once is the dock's (dock-st.ts).
|
|
9
8
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
10
9
|
import type { LiveView } from '../../ledger/live-timeline.ts'
|
|
11
10
|
import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
|
|
12
11
|
import { ChartView, type ChartSource } from './chart-view.ts'
|
|
13
|
-
import type {
|
|
12
|
+
import type { RepoSt } from './repo-st.ts'
|
|
14
13
|
import { chartHeight } from './timeline-geometry.ts'
|
|
15
14
|
|
|
16
15
|
/** how often the page asks. A strip a second behind the runner is still live */
|
|
17
16
|
export const LIVE_POLL_MS = 1_000
|
|
18
|
-
/** the
|
|
19
|
-
export const FOLDED_WINDOW_MS = 120_000
|
|
17
|
+
/** the span the chart opens on */
|
|
20
18
|
export const OPEN_WINDOW_MS = 600_000
|
|
21
|
-
/** the most the
|
|
19
|
+
/** the most the chart grows to: past it the lanes scroll inside it */
|
|
22
20
|
export const OPEN_MAX_H = 320
|
|
23
|
-
/** how long the
|
|
21
|
+
/** how long the chart keeps a height its lanes no longer fill,
|
|
24
22
|
* so it grows at once and never shrinks under the eye every second */
|
|
25
23
|
export const HEIGHT_HOLD_MS = 10_000
|
|
26
24
|
|
|
27
|
-
/** the height the
|
|
25
|
+
/** the height the chart shows: up at once, down only once the
|
|
28
26
|
* lanes have wanted less for HEIGHT_HOLD_MS */
|
|
29
27
|
export type HeldHeight = { h: number; lowSince: number | null }
|
|
30
28
|
export function settleHeight(prev: HeldHeight, wanted: number, now: number, holdMs = HEIGHT_HOLD_MS): HeldHeight {
|
|
@@ -33,32 +31,20 @@ export function settleHeight(prev: HeldHeight, wanted: number, now: number, hold
|
|
|
33
31
|
return now - prev.lowSince >= holdMs ? { h: wanted, lowSince: null } : prev
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
/** the whole record the
|
|
34
|
+
/** the whole record the chart draws: every run of the live view,
|
|
37
35
|
* then the older runs of the record it does not carry, newest first */
|
|
38
36
|
export function mergeRuns(recent: TimelineInvocation[], record: TimelineInvocation[]): TimelineInvocation[] {
|
|
39
37
|
const seen = new Set(recent.map((i) => i.runId))
|
|
40
38
|
return [...recent, ...record.filter((i) => !seen.has(i.runId))].sort((a, b) => b.started - a.started)
|
|
41
39
|
}
|
|
42
|
-
/** the lanes the folded strip has room for */
|
|
43
|
-
export const FOLDED_LANES = 5
|
|
44
|
-
|
|
45
|
-
/** where the fold is remembered. localStorage is per origin and every
|
|
46
|
-
* repo's hub has its own port, so this is per repo already */
|
|
47
|
-
export const OPEN_KEY = 'hub:liveOpen'
|
|
48
|
-
|
|
49
|
-
const readOpen = (): boolean => {
|
|
50
|
-
try { return globalThis.localStorage?.getItem(OPEN_KEY) === '1' } catch { return false }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
40
|
/** the server clock now: the view's stamp plus what the page clock has
|
|
54
41
|
* counted since it arrived, since the page may be another machine */
|
|
55
|
-
export const serverNow = (view:
|
|
42
|
+
export const serverNow = (view: { now: number }, receivedAt: number, clientNow: number): number => view.now + Math.max(0, clientNow - receivedAt)
|
|
56
43
|
|
|
57
|
-
/** what the chart draws:
|
|
58
|
-
*
|
|
59
|
-
export function liveSource(view: LiveView | null, now: number,
|
|
44
|
+
/** what the runs page's chart draws: the whole record, opening on its last
|
|
45
|
+
* ten minutes */
|
|
46
|
+
export function liveSource(view: LiveView | null, now: number, record: TimelineInvocation[] = []): ChartSource | null {
|
|
60
47
|
if (view == null) return null
|
|
61
|
-
if (!open) return { invocations: view.invocations, now, windowMs: FOLDED_WINDOW_MS, laneCap: Math.max(1, FOLDED_LANES - view.repos.length), zoomable: false, cap: view.cap }
|
|
62
48
|
return { invocations: mergeRuns(view.invocations, record), now, windowMs: null, zoomable: true, cap: view.cap, homeSpanMs: OPEN_WINDOW_MS }
|
|
63
49
|
}
|
|
64
50
|
|
|
@@ -67,18 +53,16 @@ export class LiveSvc {
|
|
|
67
53
|
/** the page clock when `view` arrived */
|
|
68
54
|
receivedAt = 0
|
|
69
55
|
error = ''
|
|
70
|
-
/**
|
|
71
|
-
note = ''
|
|
72
|
-
open = readOpen()
|
|
73
|
-
/** the unfolded chart's height, held so it does not jump every second */
|
|
56
|
+
/** the chart's height, held so it does not jump every second */
|
|
74
57
|
held: HeldHeight = { h: 0, lowSince: null }
|
|
75
58
|
chart: ChartView
|
|
76
59
|
private timer: ReturnType<typeof setInterval> | null = null
|
|
77
60
|
private inFlight = false
|
|
61
|
+
private started = false
|
|
78
62
|
|
|
79
|
-
constructor(private st:
|
|
80
|
-
this.chart = new ChartView(null, () => liveSource(this.view, this.now, this.
|
|
81
|
-
makeAutoObservable<this, 'st' | 'timer' | 'inFlight'>(this, { st: false, timer: false, inFlight: false, view: observableRef, chart: false })
|
|
63
|
+
constructor(private st: RepoSt) {
|
|
64
|
+
this.chart = new ChartView(null, () => liveSource(this.view, this.now, this.st.timeline.data?.invocations ?? []))
|
|
65
|
+
makeAutoObservable<this, 'st' | 'timer' | 'inFlight' | 'started'>(this, { st: false, timer: false, inFlight: false, started: false, view: observableRef, chart: false })
|
|
82
66
|
}
|
|
83
67
|
|
|
84
68
|
/** the server clock, as of the page clock's last tick */
|
|
@@ -86,18 +70,9 @@ export class LiveSvc {
|
|
|
86
70
|
return this.view == null ? this.st.clock.now : serverNow(this.view, this.receivedAt, this.st.clock.now)
|
|
87
71
|
}
|
|
88
72
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
try { globalThis.localStorage?.setItem(OPEN_KEY, this.open ? '1' : '0') } catch { /* private window: the fold lasts the tab */ }
|
|
92
|
-
// either fold starts live: the folded strip is a trail, nothing else
|
|
93
|
-
this.chart.attach()
|
|
94
|
-
this.opened()
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** unfolded: back to the last ten minutes, the older record fetched, and
|
|
98
|
-
* a height from the lanes as they are */
|
|
73
|
+
/** back to the last ten minutes, the older record fetched, and a height
|
|
74
|
+
* from the lanes as they are */
|
|
99
75
|
private opened(): void {
|
|
100
|
-
if (!this.open) return
|
|
101
76
|
this.chart.home()
|
|
102
77
|
this.chart.attach()
|
|
103
78
|
this.held = { h: 0, lowSince: null }
|
|
@@ -107,18 +82,22 @@ export class LiveSvc {
|
|
|
107
82
|
|
|
108
83
|
/** the height the lanes want now, as the held rule lets it show */
|
|
109
84
|
settle(now: number): void {
|
|
85
|
+
// no runs page on screen, no chart to size: the record is not laid out
|
|
86
|
+
if (this.chart.viewW === 0) return
|
|
110
87
|
const l = this.chart.layout
|
|
111
|
-
const lanes =
|
|
88
|
+
const lanes = l?.lanes ?? 0
|
|
112
89
|
this.held = settleHeight(this.held, Math.min(OPEN_MAX_H, chartHeight(lanes)), now)
|
|
113
90
|
}
|
|
114
91
|
|
|
92
|
+
/** a resume keeps the chart where it was: only the first start homes it */
|
|
115
93
|
start(): void {
|
|
116
94
|
if (this.timer != null) return
|
|
117
|
-
this.opened()
|
|
95
|
+
if (!this.started) this.opened()
|
|
96
|
+
this.started = true
|
|
118
97
|
void this.poll()
|
|
119
98
|
this.timer = setInterval(() => void this.poll(), LIVE_POLL_MS)
|
|
120
99
|
}
|
|
121
|
-
|
|
100
|
+
stop(): void {
|
|
122
101
|
if (this.timer != null) clearInterval(this.timer)
|
|
123
102
|
this.timer = null
|
|
124
103
|
}
|
|
@@ -128,7 +107,7 @@ export class LiveSvc {
|
|
|
128
107
|
if (this.inFlight) return
|
|
129
108
|
this.inFlight = true
|
|
130
109
|
try {
|
|
131
|
-
const r = await fetch('/api/live.json')
|
|
110
|
+
const r = await fetch(this.st.url('/api/live.json'))
|
|
132
111
|
if (!r.ok) throw new Error(await r.text())
|
|
133
112
|
const v = (await r.json()) as LiveView
|
|
134
113
|
const ended = this.running && v.runs.length === 0
|
|
@@ -136,14 +115,14 @@ export class LiveSvc {
|
|
|
136
115
|
this.view = v
|
|
137
116
|
this.receivedAt = Date.now()
|
|
138
117
|
this.error = ''
|
|
139
|
-
|
|
118
|
+
this.settle(Date.now())
|
|
140
119
|
})
|
|
141
120
|
// a run that ended, from a terminal as much as from a button,
|
|
142
121
|
// wrote new numbers: the rows and the timeline read them now
|
|
143
122
|
// rather than on the next ten second poll
|
|
144
123
|
if (ended) {
|
|
145
|
-
void this.st.cols.poll()
|
|
146
|
-
if (this.st.timeline.data != null) void this.st.timeline.load()
|
|
124
|
+
void this.st.cols.poll(true)
|
|
125
|
+
if (this.st.timeline.data != null) void this.st.timeline.load(true)
|
|
147
126
|
}
|
|
148
127
|
} catch (e) {
|
|
149
128
|
runInAction(() => { this.error = `the live runs could not be read: ${(e as Error).message}` })
|
|
@@ -155,20 +134,4 @@ export class LiveSvc {
|
|
|
155
134
|
get running(): boolean {
|
|
156
135
|
return (this.view?.runs.length ?? 0) > 0
|
|
157
136
|
}
|
|
158
|
-
|
|
159
|
-
/** a repo lane was clicked: its hub, when it serves */
|
|
160
|
-
async openRepo(repo: string): Promise<void> {
|
|
161
|
-
try {
|
|
162
|
-
const r = (await (await fetch(`/api/repo-hub?repo=${encodeURIComponent(repo)}`)).json()) as { url: string | null }
|
|
163
|
-
if (r.url != null) window.open(r.url, '_blank')
|
|
164
|
-
else this.say(`${repo} has no hub serving`)
|
|
165
|
-
} catch (e) {
|
|
166
|
-
this.say(`${repo}: ${(e as Error).message}`)
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private say(text: string): void {
|
|
171
|
-
runInAction(() => { this.note = text })
|
|
172
|
-
setTimeout(() => runInAction(() => { if (this.note === text) this.note = '' }), 4_000)
|
|
173
|
-
}
|
|
174
137
|
}
|