@monster-orange/den 0.2.8 → 0.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 +1547 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +507 -4
- package/make/den.mk +19 -4
- package/make/journeys.mk +2 -1
- package/make/local_dev.mk +6 -13
- package/make/secrets-1password.mk +18 -0
- package/make/test.mk +1 -1
- package/package.json +9 -2
- package/schemas/den.plugin.v1.json +156 -0
- package/schemas/den.theme.v1.json +269 -0
- package/scripts/auth-configure.ts +16 -8
- package/scripts/heal-config.ts +0 -5
- package/scripts/setup-1password.ts +41 -19
- package/scripts/setup-secrets.ts +67 -101
- package/src/pillars/api/global-setup.ts +1 -1
- package/src/pillars/journeys/sandbox.ts +23 -18
- package/src/pillars/journeys/tools/browser-session.ts +2 -2
- package/src/scripts/setup-default-workspace.ts +2 -2
- package/src/shared/auth/headless-login.ts +1 -1
- package/src/shared/auth/sudo-vault.ts +93 -31
- package/src/shared/auth/token-providers/index.ts +7 -3
- package/src/shared/brand.ts +7 -0
- package/src/shared/config-error.ts +33 -3
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +141 -5
- package/src/shared/edge/config.ts +147 -21
- package/src/shared/edge/exec.ts +31 -0
- package/src/shared/edge/included.ts +169 -0
- package/src/shared/edge/render.ts +69 -14
- package/src/shared/edge/runtime.ts +128 -121
- package/src/shared/edge/status.ts +68 -8
- package/src/shared/edge/tls.ts +328 -0
- package/src/shared/edge/types.ts +25 -9
- package/src/shared/environment-manager/default-workspace.ts +16 -1
- package/src/shared/environment-manager/health.ts +73 -38
- package/src/shared/environment-manager/scope.ts +168 -0
- package/src/shared/environment-manager/service-runner.ts +789 -43
- package/src/shared/environment-manager/up-targets.ts +132 -0
- package/src/shared/health/rollup.ts +48 -0
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +78 -59
- package/src/shared/org.ts +53 -5
- package/src/shared/plugins/manifest.ts +406 -0
- package/src/shared/plugins/store.ts +358 -0
- package/src/shared/process/legacy-adopt.ts +285 -0
- package/src/shared/process/process-group.ts +202 -0
- package/src/shared/process/spawn-streaming.ts +7 -0
- package/src/shared/secrets/doppler.ts +1 -1
- package/src/shared/secrets/index.ts +279 -50
- package/src/shared/secrets/none.ts +56 -0
- package/src/shared/secrets/strategies.ts +202 -0
- package/src/shared/secrets/types.ts +1 -1
- package/src/shared/smoke/git-remote.ts +40 -0
- package/src/shared/stacks/manifest-commands.ts +1 -1
- package/src/shared/stacks/namespace.ts +178 -0
- package/src/shared/stacks/owners.ts +303 -0
- package/src/shared/stacks/render.ts +8 -3
- package/src/shared/stacks/runtime.ts +100 -16
- package/src/shared/stacks/status.ts +63 -12
- package/src/shared/strategies/registry.ts +89 -0
- package/src/shared/theme/contract.ts +538 -0
- package/src/shared/types/index.ts +12 -3
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +131 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/flag-parser.ts +7 -2
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +548 -60
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/stack-command.ts +63 -2
- package/src/ui/cli/test-runner.ts +11 -9
- package/src/ui/cli/theme-command.ts +136 -0
- package/src/ui/cli/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +22 -1
- package/src/ui/mcp/tools.ts +50 -7
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/daemon-reaper.ts +29 -1
- package/src/ui/server/engine-stamp.ts +83 -0
- package/src/ui/server/github-service.ts +23 -44
- package/src/ui/server/graph-page.ts +20 -0
- package/src/ui/server/graph-prefs.ts +44 -1
- package/src/ui/server/http-errors.ts +293 -0
- package/src/ui/server/http-helpers.ts +22 -3
- package/src/ui/server/index.ts +532 -51
- package/src/ui/server/public/appearance.js +531 -0
- package/src/ui/server/public/edge-page.js +248 -0
- package/src/ui/server/public/edge.html +251 -0
- package/src/ui/server/public/environments.html +65 -83
- package/src/ui/server/public/error-page.js +247 -0
- package/src/ui/server/public/error.html +260 -0
- package/src/ui/server/public/graph-model.js +332 -14
- package/src/ui/server/public/graph.css +141 -47
- package/src/ui/server/public/graph.js +223 -44
- package/src/ui/server/public/header.js +396 -600
- package/src/ui/server/public/health-fit.js +248 -0
- package/src/ui/server/public/health-rollup.js +128 -0
- package/src/ui/server/public/i18n/es.js +287 -0
- package/src/ui/server/public/i18n/ja.js +276 -0
- package/src/ui/server/public/index.html +139 -438
- package/src/ui/server/public/local-dev.html +3468 -4044
- package/src/ui/server/public/logs.html +71 -208
- package/src/ui/server/public/panel.js +14 -2
- package/src/ui/server/public/release.html +175 -389
- package/src/ui/server/public/services.html +596 -770
- package/src/ui/server/public/settings-page.js +759 -0
- package/src/ui/server/public/settings-registry.js +521 -0
- package/src/ui/server/public/settings.html +590 -0
- package/src/ui/server/public/shell.js +3869 -0
- package/src/ui/server/public/topbar.js +1413 -0
- package/src/ui/server/public/version-label.js +12 -0
- package/src/ui/server/public/workspace-picker.js +74 -34
- package/src/ui/server/readonly-routes.ts +12 -1
- package/src/ui/server/services-list.ts +94 -17
- package/src/ui/server/settings-distribution.ts +74 -0
- package/src/ui/server/shell-prefs.ts +192 -0
- package/src/ui/server/theme-routes.ts +292 -0
- package/src/ui/server/theme-runtime.ts +407 -0
- package/src/ui/server/ui-assets.ts +16 -1
- package/src/ui/server/ui-language.ts +50 -0
- package/src/ui/server/workspace-routes.ts +45 -3
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- package/themes/den/mark.svg +6 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<script>
|
|
7
|
+
// Mode before first paint: yours (the server fills it in below),
|
|
8
|
+
// else the system's, else the theme's default, else dark.
|
|
9
|
+
(function () {
|
|
10
|
+
var m = "@@DEN_MODE@@";
|
|
11
|
+
if (m !== "dark" && m !== "light") {
|
|
12
|
+
var q = function (s) {
|
|
13
|
+
return (
|
|
14
|
+
window.matchMedia &&
|
|
15
|
+
matchMedia("(prefers-color-scheme: " + s + ")").matches
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
m = q("light")
|
|
19
|
+
? "light"
|
|
20
|
+
: q("dark")
|
|
21
|
+
? "dark"
|
|
22
|
+
: "@@DEN_DEFAULT_MODE@@";
|
|
23
|
+
}
|
|
24
|
+
document.documentElement.dataset.mode =
|
|
25
|
+
m === "light" ? "light" : "dark";
|
|
26
|
+
})();
|
|
27
|
+
</script>
|
|
28
|
+
<link rel="stylesheet" href="/theme.css" id="theme-css" />
|
|
29
|
+
<title>__DEN_TITLE__ · Error</title>
|
|
30
|
+
<style>
|
|
31
|
+
/* The error page (R26a, 0.3.0-dev.38): the prototype's (den-shell.html)
|
|
32
|
+
page header (.ph), empty-state box (.empty) and buttons (.btn),
|
|
33
|
+
ported as they are; --r is --radius, --r-btn is --radius-button. */
|
|
34
|
+
*,
|
|
35
|
+
*::before,
|
|
36
|
+
*::after {
|
|
37
|
+
box-sizing: border-box;
|
|
38
|
+
}
|
|
39
|
+
html,
|
|
40
|
+
body {
|
|
41
|
+
height: 100%;
|
|
42
|
+
margin: 0;
|
|
43
|
+
background: var(--bg);
|
|
44
|
+
color: var(--text);
|
|
45
|
+
}
|
|
46
|
+
body {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
}
|
|
51
|
+
/* .main */
|
|
52
|
+
html body.den-has-shell > #den-content {
|
|
53
|
+
overflow: auto;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
padding: 20px 24px 32px 24px;
|
|
56
|
+
font: 14px/1.45 var(--sans);
|
|
57
|
+
color: var(--text);
|
|
58
|
+
display: block;
|
|
59
|
+
}
|
|
60
|
+
#den-content :focus-visible {
|
|
61
|
+
outline: 2px solid var(--accent);
|
|
62
|
+
outline-offset: 2px;
|
|
63
|
+
}
|
|
64
|
+
a {
|
|
65
|
+
color: var(--accent-text);
|
|
66
|
+
}
|
|
67
|
+
/* .ph */
|
|
68
|
+
.ph {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: flex-end;
|
|
71
|
+
gap: 16px;
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
margin-bottom: 16px;
|
|
74
|
+
}
|
|
75
|
+
.ph h1 {
|
|
76
|
+
font: 600 22px/1.2 var(--display);
|
|
77
|
+
margin: 0;
|
|
78
|
+
text-wrap: balance;
|
|
79
|
+
}
|
|
80
|
+
.ph .sub {
|
|
81
|
+
color: var(--muted);
|
|
82
|
+
font-size: 13px;
|
|
83
|
+
margin-top: 4px;
|
|
84
|
+
overflow-wrap: anywhere;
|
|
85
|
+
}
|
|
86
|
+
.ph .k {
|
|
87
|
+
font: 11px var(--mono);
|
|
88
|
+
text-transform: uppercase;
|
|
89
|
+
letter-spacing: 0.07em;
|
|
90
|
+
color: var(--faint);
|
|
91
|
+
}
|
|
92
|
+
/* .empty */
|
|
93
|
+
.empty {
|
|
94
|
+
border: 1px dashed var(--border-strong);
|
|
95
|
+
border-radius: var(--radius);
|
|
96
|
+
padding: 28px;
|
|
97
|
+
color: var(--muted);
|
|
98
|
+
max-width: 640px;
|
|
99
|
+
}
|
|
100
|
+
.empty b {
|
|
101
|
+
color: var(--text);
|
|
102
|
+
}
|
|
103
|
+
.empty p {
|
|
104
|
+
margin: 0 0 12px;
|
|
105
|
+
}
|
|
106
|
+
.empty .lead {
|
|
107
|
+
color: var(--text);
|
|
108
|
+
}
|
|
109
|
+
/* .btn */
|
|
110
|
+
.btn {
|
|
111
|
+
border: 1px solid var(--border);
|
|
112
|
+
background: var(--surface);
|
|
113
|
+
color: var(--text);
|
|
114
|
+
border-radius: var(--radius-button);
|
|
115
|
+
padding: 6px 12px;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font: 500 14px/1.45 var(--sans);
|
|
118
|
+
white-space: nowrap;
|
|
119
|
+
text-decoration: none;
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 6px;
|
|
123
|
+
}
|
|
124
|
+
.btn:hover {
|
|
125
|
+
border-color: var(--border-strong);
|
|
126
|
+
}
|
|
127
|
+
.btn.primary {
|
|
128
|
+
background: var(--accent-fill);
|
|
129
|
+
border-color: var(--accent-fill);
|
|
130
|
+
color: var(--on-accent);
|
|
131
|
+
}
|
|
132
|
+
.btn kbd {
|
|
133
|
+
font: 11px var(--mono);
|
|
134
|
+
color: var(--faint);
|
|
135
|
+
}
|
|
136
|
+
.acts {
|
|
137
|
+
display: flex;
|
|
138
|
+
gap: 8px;
|
|
139
|
+
flex-wrap: wrap;
|
|
140
|
+
margin-top: 16px;
|
|
141
|
+
}
|
|
142
|
+
.mono {
|
|
143
|
+
font-family: var(--mono);
|
|
144
|
+
font-size: 12.5px;
|
|
145
|
+
color: var(--text);
|
|
146
|
+
overflow-wrap: anywhere;
|
|
147
|
+
}
|
|
148
|
+
dl.facts {
|
|
149
|
+
display: grid;
|
|
150
|
+
grid-template-columns: max-content 1fr;
|
|
151
|
+
gap: 6px 16px;
|
|
152
|
+
margin: 0;
|
|
153
|
+
}
|
|
154
|
+
dl.facts dt {
|
|
155
|
+
font: 11px var(--mono);
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
letter-spacing: 0.07em;
|
|
158
|
+
color: var(--faint);
|
|
159
|
+
padding-top: 2px;
|
|
160
|
+
}
|
|
161
|
+
dl.facts dd {
|
|
162
|
+
margin: 0;
|
|
163
|
+
}
|
|
164
|
+
h2.sec {
|
|
165
|
+
font: 600 13px var(--mono);
|
|
166
|
+
text-transform: uppercase;
|
|
167
|
+
letter-spacing: 0.07em;
|
|
168
|
+
color: var(--muted);
|
|
169
|
+
margin: 22px 0 10px;
|
|
170
|
+
}
|
|
171
|
+
/* The 422 page's problems (0.3.0-dev.40): the prototype's .tablewrap
|
|
172
|
+
and table.svc, and its pre.code for the command. */
|
|
173
|
+
.tablewrap {
|
|
174
|
+
overflow-x: auto;
|
|
175
|
+
border: 1px solid var(--border);
|
|
176
|
+
border-radius: var(--radius);
|
|
177
|
+
background: var(--surface);
|
|
178
|
+
margin: 0 0 16px;
|
|
179
|
+
}
|
|
180
|
+
table.svc {
|
|
181
|
+
width: 100%;
|
|
182
|
+
border-collapse: collapse;
|
|
183
|
+
font-size: 13.5px;
|
|
184
|
+
}
|
|
185
|
+
table.svc th {
|
|
186
|
+
text-align: left;
|
|
187
|
+
font: 11px var(--mono);
|
|
188
|
+
text-transform: uppercase;
|
|
189
|
+
letter-spacing: 0.07em;
|
|
190
|
+
color: var(--faint);
|
|
191
|
+
padding: 8px 12px;
|
|
192
|
+
border-bottom: 1px solid var(--border);
|
|
193
|
+
font-weight: 400;
|
|
194
|
+
}
|
|
195
|
+
table.svc td {
|
|
196
|
+
padding: 7px 12px;
|
|
197
|
+
border-bottom: 1px solid var(--border);
|
|
198
|
+
color: var(--text);
|
|
199
|
+
vertical-align: top;
|
|
200
|
+
}
|
|
201
|
+
table.svc td.mono {
|
|
202
|
+
white-space: nowrap;
|
|
203
|
+
}
|
|
204
|
+
table.svc tr:last-child td {
|
|
205
|
+
border-bottom: 0;
|
|
206
|
+
}
|
|
207
|
+
pre.code {
|
|
208
|
+
background: var(--sunken);
|
|
209
|
+
border: 1px solid var(--border);
|
|
210
|
+
border-radius: var(--radius-button);
|
|
211
|
+
padding: 12px;
|
|
212
|
+
overflow-x: auto;
|
|
213
|
+
font: 12px/1.5 var(--mono);
|
|
214
|
+
margin: 0 0 12px;
|
|
215
|
+
color: var(--text);
|
|
216
|
+
}
|
|
217
|
+
.empty.wide {
|
|
218
|
+
max-width: 880px;
|
|
219
|
+
}
|
|
220
|
+
ul.pages {
|
|
221
|
+
list-style: none;
|
|
222
|
+
margin: 0;
|
|
223
|
+
padding: 0;
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-wrap: wrap;
|
|
226
|
+
gap: 6px 16px;
|
|
227
|
+
}
|
|
228
|
+
</style>
|
|
229
|
+
</head>
|
|
230
|
+
<body>
|
|
231
|
+
<!-- The shell: top bar and rail, the same on every page (shell.js). -->
|
|
232
|
+
<div id="den-shell" data-page="error"></div>
|
|
233
|
+
<script src="/shell.js"></script>
|
|
234
|
+
|
|
235
|
+
<!-- The page's one main landmark: everything after the shell. -->
|
|
236
|
+
<main id="den-content">
|
|
237
|
+
<div id="den-error-page" aria-live="polite"></div>
|
|
238
|
+
<script type="application/json" id="den-error">
|
|
239
|
+
@@DEN_ERROR@@
|
|
240
|
+
</script>
|
|
241
|
+
<script type="module">
|
|
242
|
+
import {
|
|
243
|
+
applyDenConfig,
|
|
244
|
+
initChangelog,
|
|
245
|
+
initHealth,
|
|
246
|
+
initLogBadge,
|
|
247
|
+
initWorkspacePill,
|
|
248
|
+
} from "/header.js";
|
|
249
|
+
import { mountErrorPage } from "/error-page.js";
|
|
250
|
+
mountErrorPage(document.getElementById("den-error-page"));
|
|
251
|
+
void Promise.all([
|
|
252
|
+
initHealth(),
|
|
253
|
+
initWorkspacePill(),
|
|
254
|
+
applyDenConfig(),
|
|
255
|
+
initChangelog(),
|
|
256
|
+
]).then(initLogBadge);
|
|
257
|
+
</script>
|
|
258
|
+
</main>
|
|
259
|
+
</body>
|
|
260
|
+
</html>
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/** Real card sizes; there is no shrink-to-fit. */
|
|
16
|
+
import { describeRollup, rollupRepo } from "./health-rollup.js";
|
|
17
|
+
|
|
16
18
|
export const SIZE = {
|
|
17
19
|
detailed: { w: 236, h: 104 },
|
|
18
20
|
compact: { w: 176, h: 52 },
|
|
@@ -31,15 +33,17 @@ export function placementOf(r) {
|
|
|
31
33
|
return { runs, target };
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
// An org's color when the org config names none. Org labels are text, so
|
|
37
|
+
// only text tokens (each 4.5:1 on every ground) and mixes of two of them.
|
|
34
38
|
const PALETTE = [
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
39
|
+
"var(--accent-text)",
|
|
40
|
+
"var(--ok)",
|
|
41
|
+
"color-mix(in oklab, var(--remote) 50%, var(--bad))",
|
|
42
|
+
"var(--warn)",
|
|
43
|
+
"var(--remote)",
|
|
44
|
+
"color-mix(in oklab, var(--warn) 50%, var(--bad))",
|
|
45
|
+
"color-mix(in oklab, var(--accent-text) 50%, var(--remote))",
|
|
46
|
+
"var(--muted)",
|
|
43
47
|
];
|
|
44
48
|
function colorFor(name) {
|
|
45
49
|
let h = 5381;
|
|
@@ -51,7 +55,7 @@ function colorFor(name) {
|
|
|
51
55
|
/** Build the model the graph and panel render from. */
|
|
52
56
|
export function buildModel({
|
|
53
57
|
services = [],
|
|
54
|
-
health =
|
|
58
|
+
health = null,
|
|
55
59
|
graph = {},
|
|
56
60
|
apps = [],
|
|
57
61
|
orgs = null,
|
|
@@ -74,7 +78,8 @@ export function buildModel({
|
|
|
74
78
|
org: orgs?.repos?.[r.repo] || "",
|
|
75
79
|
app: a ? a.title || a.name : r.repo,
|
|
76
80
|
...placementOf(r),
|
|
77
|
-
health
|
|
81
|
+
// No health round has reached it yet (0.3.0-dev.40): "checking…".
|
|
82
|
+
health: { ok: null, checked: false },
|
|
78
83
|
open: null,
|
|
79
84
|
entry: !!r.isEntrypoint,
|
|
80
85
|
infra: !!r.infra,
|
|
@@ -88,6 +93,7 @@ export function buildModel({
|
|
|
88
93
|
cloned: r.cloned !== false,
|
|
89
94
|
manifestLayers: r.manifestLayers || [],
|
|
90
95
|
owners: r.owners || [],
|
|
96
|
+
inWorkspaces: r.inWorkspaces || [],
|
|
91
97
|
missing: depsMap[r.address] || [],
|
|
92
98
|
hasPlacement: "runs" in r,
|
|
93
99
|
};
|
|
@@ -124,7 +130,8 @@ export function buildModel({
|
|
|
124
130
|
apps: apps || [],
|
|
125
131
|
mixed: false,
|
|
126
132
|
};
|
|
127
|
-
|
|
133
|
+
// null: no health round yet, so every card says "checking…".
|
|
134
|
+
if (health) applyHealth(m, health);
|
|
128
135
|
return m;
|
|
129
136
|
}
|
|
130
137
|
|
|
@@ -136,8 +143,13 @@ export function applyHealth(m, results) {
|
|
|
136
143
|
for (const s of m.services) {
|
|
137
144
|
// Health rows name repo + service; a single-service repo's address is the bare repo.
|
|
138
145
|
const h = byKey.get(`${s.repo}/${s.name}`) || byKey.get(s.id);
|
|
139
|
-
|
|
146
|
+
// A round without a row for it still counts as its first check.
|
|
147
|
+
if (!h) {
|
|
148
|
+
s.health = { ...s.health, checked: true };
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
140
151
|
s.health = {
|
|
152
|
+
checked: true,
|
|
141
153
|
ok: h.ok === true ? true : h.ok === false ? false : null,
|
|
142
154
|
label: h.label,
|
|
143
155
|
url: h.url ?? null,
|
|
@@ -162,7 +174,7 @@ export const orgOf = (m, id) =>
|
|
|
162
174
|
m.orgs.get(id) || {
|
|
163
175
|
id,
|
|
164
176
|
label: id || m.workspace || "Workspace",
|
|
165
|
-
color: "
|
|
177
|
+
color: "var(--muted)",
|
|
166
178
|
};
|
|
167
179
|
export const isLocal = (s) => s.runs !== "remote";
|
|
168
180
|
export const status = (s) =>
|
|
@@ -193,6 +205,8 @@ export const fmtMs = (ms) =>
|
|
|
193
205
|
ms == null ? "" : ms >= 1000 ? (ms / 1000).toFixed(1) + " s" : ms + " ms";
|
|
194
206
|
|
|
195
207
|
export function healthLine(s) {
|
|
208
|
+
// Before the first health round, as the Workspaces table says (dev.37).
|
|
209
|
+
if (s.health.checked === false) return "checking…";
|
|
196
210
|
const ms = s.health.ms != null ? " · " + fmtMs(s.health.ms) : "";
|
|
197
211
|
if (s.runs === "remote")
|
|
198
212
|
return (
|
|
@@ -206,12 +220,45 @@ export function healthLine(s) {
|
|
|
206
220
|
return (s.health.ok ? "healthy" : "down") + ms;
|
|
207
221
|
}
|
|
208
222
|
|
|
223
|
+
/**
|
|
224
|
+
* A repo's health from its services: health-rollup.js's rule, the one the
|
|
225
|
+
* header row and `<cli> status` use, so a repo card never disagrees with
|
|
226
|
+
* them. Unchecked services don't count.
|
|
227
|
+
*/
|
|
228
|
+
export function repoRollup(m, repo) {
|
|
229
|
+
return rollupRepo(
|
|
230
|
+
repo,
|
|
231
|
+
(m.byRepo.get(repo) || []).map((s) => ({
|
|
232
|
+
service: s.name,
|
|
233
|
+
ok: s.health.ok,
|
|
234
|
+
isEntrypoint: s.entry,
|
|
235
|
+
})),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The rollup as a .dg-dot class, and in words. */
|
|
240
|
+
export const repoStatus = (r) =>
|
|
241
|
+
({ down: "bad", degraded: "warn", ok: "ok", unknown: "unk" })[r.state];
|
|
242
|
+
export const repoStatusWord = (r) => describeRollup(r);
|
|
243
|
+
|
|
209
244
|
/** A repo's representative service: entrypoint, root-named, else first. */
|
|
210
245
|
export function entryOf(m, repo) {
|
|
211
246
|
const l = m.byRepo.get(repo) || [];
|
|
212
247
|
return l.find((s) => s.entry) || l.find((s) => s.name === repo) || l[0];
|
|
213
248
|
}
|
|
214
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Find (the Graph page's, and the Workspaces table's, R24d): does `key` (a
|
|
252
|
+
* service address, "repo:<name>", or a repo name) match the query? Case
|
|
253
|
+
* apart, a substring; an empty query matches nothing (nothing is a hit).
|
|
254
|
+
*/
|
|
255
|
+
export function findHit(key, query) {
|
|
256
|
+
const q = String(query ?? "")
|
|
257
|
+
.trim()
|
|
258
|
+
.toLowerCase();
|
|
259
|
+
return !!q && String(key).toLowerCase().includes(q);
|
|
260
|
+
}
|
|
261
|
+
|
|
215
262
|
export const isCollapsed = (m, collapsed, repo) =>
|
|
216
263
|
collapsed.has(repo) && (m.byRepo.get(repo)?.length || 0) > 1;
|
|
217
264
|
export const nodeFor = (m, collapsed, id) => {
|
|
@@ -242,6 +289,19 @@ export function edgeModel(m, collapsed) {
|
|
|
242
289
|
}
|
|
243
290
|
|
|
244
291
|
/** The ELK graph: outer group (org or application) › repo › service. */
|
|
292
|
+
/**
|
|
293
|
+
* A compound node's minimum size, as elk.nodeSize.minimum. ELK's layered
|
|
294
|
+
* layout with INCLUDE_CHILDREN reads a compound node's minimum in its own
|
|
295
|
+
* layout axes: top-down, the first value bounds the height and the second
|
|
296
|
+
* the width. Passed as (width, height) there, a repo box's minimum width
|
|
297
|
+
* (386px) became its height, and a box with one row of cards drew about
|
|
298
|
+
* twice the height it needs (a consumer's dev.32 finding). So top-down
|
|
299
|
+
* swaps the pair; left-right it is (width, height) as written.
|
|
300
|
+
*/
|
|
301
|
+
export function groupMinimum(w, h, direction = "down") {
|
|
302
|
+
return direction === "right" ? `(${w},${h})` : `(${h},${w})`;
|
|
303
|
+
}
|
|
304
|
+
|
|
245
305
|
export function buildElkGraph(
|
|
246
306
|
m,
|
|
247
307
|
{
|
|
@@ -279,7 +339,7 @@ export function buildElkGraph(
|
|
|
279
339
|
layoutOptions: {
|
|
280
340
|
"elk.padding": "[top=40,left=14,bottom=14,right=14]",
|
|
281
341
|
"elk.nodeSize.constraints": "MINIMUM_SIZE",
|
|
282
|
-
"elk.nodeSize.minimum":
|
|
342
|
+
"elk.nodeSize.minimum": groupMinimum(sz.w + 150, 40, direction),
|
|
283
343
|
},
|
|
284
344
|
children: svcs.map((s) => {
|
|
285
345
|
parent[s.id] = rid;
|
|
@@ -349,6 +409,198 @@ export function buildElkGraph(
|
|
|
349
409
|
return root;
|
|
350
410
|
}
|
|
351
411
|
|
|
412
|
+
// ── Wrapping the top-level groups (0.3.0-dev.34) ─────────────────────────
|
|
413
|
+
// A consumer's distribution with 31 services in 10 org groups: one layered
|
|
414
|
+
// layout put every top-level group in one row (top-down), so the whole
|
|
415
|
+
// graph fitted only at 15–24%, where cards are hard to read. ELK's own
|
|
416
|
+
// options did not help: with INCLUDE_CHILDREN the root is one layered
|
|
417
|
+
// graph, and elk.aspectRatio (its component packing) left the row as it
|
|
418
|
+
// was. So each top-level group is laid out on its own, by the same layered
|
|
419
|
+
// layout with the same options (a group's inside is as before), the groups
|
|
420
|
+
// are wrapped into rows (top-down) or columns (left-right) sized for the
|
|
421
|
+
// stage, and the few edges between groups are routed here, orthogonally,
|
|
422
|
+
// through the gaps between rows (or columns).
|
|
423
|
+
|
|
424
|
+
/** Gap between wrapped groups: elk.spacing.componentComponent. */
|
|
425
|
+
export const GROUP_GAP = 48;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Split buildElkGraph's root into one ELK graph per top-level group (the
|
|
429
|
+
* root's options, the group, the edges inside it) and the edges between
|
|
430
|
+
* groups (the root's own). Groups are ordered so that an edge's source
|
|
431
|
+
* comes first where the edges allow (stable otherwise): in reading order
|
|
432
|
+
* a dependency then sits after, below or to the right of, its user, as the
|
|
433
|
+
* layered layout puts it.
|
|
434
|
+
*/
|
|
435
|
+
export function splitTopLevel(root) {
|
|
436
|
+
const groups = root.children || [];
|
|
437
|
+
const cross = root.edges || [];
|
|
438
|
+
const groupOf = new Map();
|
|
439
|
+
(function index(n, top) {
|
|
440
|
+
for (const c of n.children || []) {
|
|
441
|
+
const t = top ?? c.id;
|
|
442
|
+
groupOf.set(c.id, t);
|
|
443
|
+
index(c, t);
|
|
444
|
+
}
|
|
445
|
+
})(root, null);
|
|
446
|
+
const idx = new Map(groups.map((g, i) => [g.id, i]));
|
|
447
|
+
const after = groups.map(() => new Set());
|
|
448
|
+
const indeg = groups.map(() => 0);
|
|
449
|
+
for (const e of cross) {
|
|
450
|
+
const a = idx.get(groupOf.get(e.sources[0]));
|
|
451
|
+
const b = idx.get(groupOf.get(e.targets[0]));
|
|
452
|
+
if (a == null || b == null || a === b || after[a].has(b)) continue;
|
|
453
|
+
after[a].add(b);
|
|
454
|
+
indeg[b]++;
|
|
455
|
+
}
|
|
456
|
+
const order = [];
|
|
457
|
+
const done = new Set();
|
|
458
|
+
while (order.length < groups.length) {
|
|
459
|
+
// The first group with nothing before it; on a cycle, the first left.
|
|
460
|
+
let i = groups.findIndex((_, k) => !done.has(k) && indeg[k] === 0);
|
|
461
|
+
if (i < 0) i = groups.findIndex((_, k) => !done.has(k));
|
|
462
|
+
done.add(i);
|
|
463
|
+
order.push(i);
|
|
464
|
+
for (const b of after[i]) indeg[b]--;
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
parts: order.map((i) => ({
|
|
468
|
+
id: "root",
|
|
469
|
+
layoutOptions: root.layoutOptions,
|
|
470
|
+
children: [groups[i]],
|
|
471
|
+
edges: [],
|
|
472
|
+
})),
|
|
473
|
+
cross,
|
|
474
|
+
groupOf,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Wrap groups of the given sizes, in order, into rows (direction "down")
|
|
480
|
+
* or columns ("right"), choosing the wrap that lets the whole fit the
|
|
481
|
+
* area (width × height) at the highest zoom. Returns each group's place
|
|
482
|
+
* ({ x, y, band }) and the size of the whole ({ w, h }), from 0,0.
|
|
483
|
+
* Without an area, a 16:10 one.
|
|
484
|
+
*/
|
|
485
|
+
export function packGroups(
|
|
486
|
+
sizes,
|
|
487
|
+
{ direction = "down", width = 1600, height = 1000, gap = GROUP_GAP } = {},
|
|
488
|
+
) {
|
|
489
|
+
const down = direction !== "right";
|
|
490
|
+
// Along: the axis a row runs along (x top-down); across: the other.
|
|
491
|
+
const along = sizes.map((s) => (down ? s.w : s.h));
|
|
492
|
+
const across = sizes.map((s) => (down ? s.h : s.w));
|
|
493
|
+
const lay = (limit) => {
|
|
494
|
+
const pos = [];
|
|
495
|
+
let a = 0,
|
|
496
|
+
c = 0,
|
|
497
|
+
band = 0,
|
|
498
|
+
depth = 0,
|
|
499
|
+
maxA = 0;
|
|
500
|
+
along.forEach((len, i) => {
|
|
501
|
+
if (a > 0 && a + gap + len > limit) {
|
|
502
|
+
c += depth + gap;
|
|
503
|
+
a = 0;
|
|
504
|
+
depth = 0;
|
|
505
|
+
band++;
|
|
506
|
+
}
|
|
507
|
+
const at = a > 0 ? a + gap : 0;
|
|
508
|
+
pos.push({ a: at, c, band });
|
|
509
|
+
a = at + len;
|
|
510
|
+
depth = Math.max(depth, across[i]);
|
|
511
|
+
maxA = Math.max(maxA, a);
|
|
512
|
+
});
|
|
513
|
+
return { pos, A: maxA, C: c + depth };
|
|
514
|
+
};
|
|
515
|
+
// Every row width a greedy wrap can end on: a run of consecutive groups.
|
|
516
|
+
const limits = new Set();
|
|
517
|
+
for (let i = 0; i < along.length; i++) {
|
|
518
|
+
let w = 0;
|
|
519
|
+
for (let j = i; j < along.length; j++) {
|
|
520
|
+
w += (j > i ? gap : 0) + along[j];
|
|
521
|
+
limits.add(w);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Widest first, so a tie keeps fewer rows.
|
|
525
|
+
let best = null;
|
|
526
|
+
for (const limit of [...limits].sort((x, y) => y - x)) {
|
|
527
|
+
const l = lay(limit);
|
|
528
|
+
const W = down ? l.A : l.C,
|
|
529
|
+
H = down ? l.C : l.A;
|
|
530
|
+
const z = Math.min(width / Math.max(W, 1), height / Math.max(H, 1));
|
|
531
|
+
if (!best || z > best.z + 1e-9) best = { z, l, W, H };
|
|
532
|
+
}
|
|
533
|
+
if (!best) return { pos: [], w: 0, h: 0 };
|
|
534
|
+
return {
|
|
535
|
+
pos: best.l.pos.map((p) => ({
|
|
536
|
+
x: down ? p.a : p.c,
|
|
537
|
+
y: down ? p.c : p.a,
|
|
538
|
+
band: p.band,
|
|
539
|
+
})),
|
|
540
|
+
w: best.W,
|
|
541
|
+
h: best.H,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* An edge between two wrapped groups, as orthogonal points. s and t are
|
|
547
|
+
* the endpoint cards' boxes ({ x, y, w, h }); sBand and tBand their
|
|
548
|
+
* groups' row (or column) spans across the wrap ({ from, to, band }).
|
|
549
|
+
* A target in a later row: down out of the source, along the gap above
|
|
550
|
+
* the target's row, down into the target. In an earlier row: up, along
|
|
551
|
+
* the gap above the source's row, up into the target's foot. In the same
|
|
552
|
+
* row: out of the facing side, across the gap between them. Left-right
|
|
553
|
+
* is the same with the axes swapped.
|
|
554
|
+
*/
|
|
555
|
+
export function routeCross(
|
|
556
|
+
s,
|
|
557
|
+
t,
|
|
558
|
+
sBand,
|
|
559
|
+
tBand,
|
|
560
|
+
direction = "down",
|
|
561
|
+
gap = GROUP_GAP,
|
|
562
|
+
) {
|
|
563
|
+
const down = direction !== "right";
|
|
564
|
+
// Work top-down; swap axes for left-right and swap back at the end.
|
|
565
|
+
const sw = (b) => (down ? b : { x: b.y, y: b.x, w: b.h, h: b.w });
|
|
566
|
+
const S = sw(s),
|
|
567
|
+
T = sw(t);
|
|
568
|
+
const scx = S.x + S.w / 2,
|
|
569
|
+
tcx = T.x + T.w / 2;
|
|
570
|
+
let pts;
|
|
571
|
+
if (tBand.band > sBand.band) {
|
|
572
|
+
const mid = tBand.from - gap / 2;
|
|
573
|
+
pts = [
|
|
574
|
+
[scx, S.y + S.h],
|
|
575
|
+
[scx, mid],
|
|
576
|
+
[tcx, mid],
|
|
577
|
+
[tcx, T.y],
|
|
578
|
+
];
|
|
579
|
+
} else if (tBand.band < sBand.band) {
|
|
580
|
+
const mid = sBand.from - gap / 2;
|
|
581
|
+
pts = [
|
|
582
|
+
[scx, S.y],
|
|
583
|
+
[scx, mid],
|
|
584
|
+
[tcx, mid],
|
|
585
|
+
[tcx, T.y + T.h],
|
|
586
|
+
];
|
|
587
|
+
} else {
|
|
588
|
+
const right = T.x >= S.x + S.w;
|
|
589
|
+
const sx = right ? S.x + S.w : S.x,
|
|
590
|
+
tx = right ? T.x : T.x + T.w;
|
|
591
|
+
const sy = S.y + S.h / 2,
|
|
592
|
+
ty = T.y + T.h / 2;
|
|
593
|
+
const mid = (sx + tx) / 2;
|
|
594
|
+
pts = [
|
|
595
|
+
[sx, sy],
|
|
596
|
+
[mid, sy],
|
|
597
|
+
[mid, ty],
|
|
598
|
+
[tx, ty],
|
|
599
|
+
];
|
|
600
|
+
}
|
|
601
|
+
return down ? pts : pts.map(([x, y]) => [y, x]);
|
|
602
|
+
}
|
|
603
|
+
|
|
352
604
|
/** Manifest commands that destroy state confirm inline, in Careful. */
|
|
353
605
|
export const DANGER_CMD = /reset|drop|wipe|clean|destroy|purge/;
|
|
354
606
|
export const commandTier = (cmd) =>
|
|
@@ -414,3 +666,69 @@ export function connections(m, x) {
|
|
|
414
666
|
users: [...new Set([...usedBy, ...repoIn])].map((id) => ({ id })),
|
|
415
667
|
};
|
|
416
668
|
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* The Graph page with the top bar's "All active" (0.3.0-dev.33): every
|
|
672
|
+
* workspace in view, as the Services page shows them (one /api/services
|
|
673
|
+
* list, a service several define listed once with `inWorkspaces`). The
|
|
674
|
+
* per-workspace /api/graph, /api/applications and deps-check payloads
|
|
675
|
+
* are merged here, in view order (focus first):
|
|
676
|
+
*
|
|
677
|
+
* graph nodes and `order` once each, in first-seen order; an edge
|
|
678
|
+
* once per from → to and kind (the first workspace's)
|
|
679
|
+
* apps one per name, its repos the union
|
|
680
|
+
* depsMap an address's missing deps, the union
|
|
681
|
+
*/
|
|
682
|
+
export function mergeWorkspaceViews({
|
|
683
|
+
graphs = [],
|
|
684
|
+
apps = [],
|
|
685
|
+
deps = [],
|
|
686
|
+
} = {}) {
|
|
687
|
+
const nodes = [];
|
|
688
|
+
const order = [];
|
|
689
|
+
const edges = [];
|
|
690
|
+
const seenEdge = new Set();
|
|
691
|
+
const once = (arr, v) => {
|
|
692
|
+
if (!arr.includes(v)) arr.push(v);
|
|
693
|
+
};
|
|
694
|
+
for (const g of graphs) {
|
|
695
|
+
for (const n of g?.nodes || []) once(nodes, n);
|
|
696
|
+
for (const n of g?.order || []) once(order, n);
|
|
697
|
+
for (const e of g?.edges || []) {
|
|
698
|
+
const k = `${e.from}\u0000${e.to}\u0000${e.kind}`;
|
|
699
|
+
if (seenEdge.has(k)) continue;
|
|
700
|
+
seenEdge.add(k);
|
|
701
|
+
edges.push(e);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const byName = new Map();
|
|
705
|
+
for (const list of apps)
|
|
706
|
+
for (const a of list || []) {
|
|
707
|
+
const had = byName.get(a.name);
|
|
708
|
+
if (!had) {
|
|
709
|
+
byName.set(a.name, { ...a, repos: [...(a.repos || [])] });
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
for (const r of a.repos || []) once(had.repos, r);
|
|
713
|
+
}
|
|
714
|
+
const depsMap = {};
|
|
715
|
+
for (const list of deps)
|
|
716
|
+
for (const { address, missingDeps } of list || []) {
|
|
717
|
+
const had = (depsMap[address] ||= []);
|
|
718
|
+
for (const d of missingDeps || []) once(had, d);
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
graph: { nodes, edges, ...(order.length ? { order } : {}) },
|
|
722
|
+
apps: [...byName.values()],
|
|
723
|
+
depsMap,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Each repo's owning workspace (the first in view that defines it). */
|
|
728
|
+
export function ownerOfRepos(services = []) {
|
|
729
|
+
const out = new Map();
|
|
730
|
+
for (const s of services)
|
|
731
|
+
if (!out.has(s.repo) && s.inWorkspaces?.length)
|
|
732
|
+
out.set(s.repo, s.inWorkspaces[0]);
|
|
733
|
+
return out;
|
|
734
|
+
}
|