@monster-orange/den 0.2.8 → 0.4.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 +1731 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +730 -6
- 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 +12 -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/agents/default-pack/be-dev.md +20 -0
- package/src/shared/agents/default-pack/devops.md +14 -0
- package/src/shared/agents/default-pack/fe-dev.md +20 -0
- package/src/shared/agents/default-pack/orchestrator.md +16 -0
- package/src/shared/agents/default-pack/planner.md +17 -0
- package/src/shared/agents/default-pack/reviewer.md +14 -0
- package/src/shared/agents/default-pack/ux-designer.md +15 -0
- package/src/shared/agents/den-claude.ts +173 -0
- package/src/shared/agents/generic.ts +52 -0
- package/src/shared/agents/personas.ts +218 -0
- package/src/shared/agents/records.ts +118 -0
- package/src/shared/agents/runtime.ts +192 -0
- package/src/shared/agents/runtimes.ts +47 -0
- 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 +257 -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/engine.ts +23 -0
- 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/environment-manager/workspaces.ts +19 -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/secret-env.ts +70 -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/sessions/client.ts +277 -0
- package/src/shared/sessions/den-pty.ts +97 -0
- package/src/shared/sessions/env.ts +79 -0
- package/src/shared/sessions/host.ts +667 -0
- package/src/shared/sessions/names.ts +85 -0
- package/src/shared/sessions/paths.ts +27 -0
- package/src/shared/sessions/protocol.ts +72 -0
- package/src/shared/sessions/pty.ts +141 -0
- package/src/shared/sessions/ring-buffer.ts +160 -0
- package/src/shared/sessions/types.ts +81 -0
- 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/agents-command.ts +812 -0
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +260 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/env-probe.ts +266 -0
- package/src/ui/cli/flag-parser.ts +18 -5
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +580 -60
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/sessions-command.ts +718 -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 +145 -20
- package/src/ui/mcp/server.ts +27 -2
- package/src/ui/mcp/tools.ts +94 -12
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/audit.ts +35 -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 +539 -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 +37 -3
- 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 +48 -18
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- package/themes/den/mark.svg +6 -0
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
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" />
|
|
6
29
|
<title>__DEN_TITLE__ · Services</title>
|
|
7
30
|
<style>
|
|
8
31
|
*,
|
|
@@ -13,25 +36,12 @@
|
|
|
13
36
|
padding: 0;
|
|
14
37
|
}
|
|
15
38
|
|
|
16
|
-
:root {
|
|
17
|
-
--bg: #0d1117;
|
|
18
|
-
--surface: #161b22;
|
|
19
|
-
--border: #30363d;
|
|
20
|
-
--text: #c9d1d9;
|
|
21
|
-
--muted: #8b949e;
|
|
22
|
-
--accent: #58a6ff;
|
|
23
|
-
--green: #3fb950;
|
|
24
|
-
--red: #f85149;
|
|
25
|
-
--yellow: #d29922;
|
|
26
|
-
--mono: "SF Mono", "Cascadia Code", "Fira Code", "Menlo", monospace;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
39
|
html,
|
|
30
40
|
body {
|
|
31
41
|
height: 100%;
|
|
32
42
|
background: var(--bg);
|
|
33
43
|
color: var(--text);
|
|
34
|
-
font-family:
|
|
44
|
+
font-family: var(--sans);
|
|
35
45
|
font-size: 13px;
|
|
36
46
|
}
|
|
37
47
|
|
|
@@ -42,78 +52,13 @@
|
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
/* ── Header ───────────────────────────────────────────────────────────── */
|
|
45
|
-
header {
|
|
46
|
-
display: flex;
|
|
47
|
-
align-items: center;
|
|
48
|
-
gap: 10px;
|
|
49
|
-
padding: 7px 14px;
|
|
50
|
-
background: var(--surface);
|
|
51
|
-
border-bottom: 1px solid var(--border);
|
|
52
|
-
flex-shrink: 0;
|
|
53
|
-
}
|
|
54
|
-
header h1 {
|
|
55
|
-
font-family: var(--mono);
|
|
56
|
-
font-size: 14px;
|
|
57
|
-
font-weight: 600;
|
|
58
|
-
letter-spacing: -0.02em;
|
|
59
|
-
}
|
|
60
55
|
.h-spacer {
|
|
61
56
|
flex: 1;
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
/* ── Health indicators ────────────────────────────────────────────────── */
|
|
65
|
-
.health {
|
|
66
|
-
display: flex;
|
|
67
|
-
align-items: center;
|
|
68
|
-
gap: 10px;
|
|
69
|
-
padding: 0 4px;
|
|
70
|
-
}
|
|
71
|
-
.health-item {
|
|
72
|
-
display: flex;
|
|
73
|
-
align-items: center;
|
|
74
|
-
gap: 5px;
|
|
75
|
-
font-size: 11px;
|
|
76
|
-
color: var(--muted);
|
|
77
|
-
}
|
|
78
|
-
.health-group {
|
|
79
|
-
display: flex;
|
|
80
|
-
align-items: center;
|
|
81
|
-
gap: 4px;
|
|
82
|
-
font-size: 11px;
|
|
83
|
-
color: var(--muted);
|
|
84
|
-
}
|
|
85
|
-
.health-group-label {
|
|
86
|
-
color: var(--muted);
|
|
87
|
-
margin-right: 1px;
|
|
88
|
-
}
|
|
89
|
-
.health-group-dots {
|
|
90
|
-
display: flex;
|
|
91
|
-
align-items: center;
|
|
92
|
-
gap: 3px;
|
|
93
|
-
}
|
|
94
60
|
|
|
95
61
|
/* ── Nav ──────────────────────────────────────────────────────────────── */
|
|
96
|
-
nav {
|
|
97
|
-
display: flex;
|
|
98
|
-
gap: 2px;
|
|
99
|
-
}
|
|
100
|
-
nav a {
|
|
101
|
-
color: var(--muted);
|
|
102
|
-
text-decoration: none;
|
|
103
|
-
padding: 3px 10px;
|
|
104
|
-
border-radius: 4px;
|
|
105
|
-
font-size: 12px;
|
|
106
|
-
font-weight: 500;
|
|
107
|
-
transition: color 0.1s;
|
|
108
|
-
}
|
|
109
|
-
nav a:hover {
|
|
110
|
-
color: var(--text);
|
|
111
|
-
background: var(--border);
|
|
112
|
-
}
|
|
113
|
-
nav a.active {
|
|
114
|
-
color: var(--text);
|
|
115
|
-
background: var(--surface);
|
|
116
|
-
}
|
|
117
62
|
|
|
118
63
|
/* ── Shared: dot ──────────────────────────────────────────────────────── */
|
|
119
64
|
.dot {
|
|
@@ -125,13 +70,13 @@
|
|
|
125
70
|
display: inline-block;
|
|
126
71
|
}
|
|
127
72
|
.dot.ok {
|
|
128
|
-
background: var(--
|
|
73
|
+
background: var(--ok);
|
|
129
74
|
}
|
|
130
75
|
.dot.err {
|
|
131
|
-
background: var(--
|
|
76
|
+
background: var(--bad);
|
|
132
77
|
}
|
|
133
78
|
.dot.running {
|
|
134
|
-
background: var(--
|
|
79
|
+
background: var(--warn);
|
|
135
80
|
animation: pulse 1s ease-in-out infinite;
|
|
136
81
|
}
|
|
137
82
|
@keyframes pulse {
|
|
@@ -144,16 +89,6 @@
|
|
|
144
89
|
}
|
|
145
90
|
}
|
|
146
91
|
|
|
147
|
-
/* ── Controls bar ─────────────────────────────────────────────────────── */
|
|
148
|
-
.controls {
|
|
149
|
-
display: flex;
|
|
150
|
-
align-items: center;
|
|
151
|
-
gap: 8px;
|
|
152
|
-
padding: 6px 14px;
|
|
153
|
-
background: var(--surface);
|
|
154
|
-
border-bottom: 1px solid var(--border);
|
|
155
|
-
flex-shrink: 0;
|
|
156
|
-
}
|
|
157
92
|
select {
|
|
158
93
|
background: var(--bg);
|
|
159
94
|
border: 1px solid var(--border);
|
|
@@ -169,7 +104,7 @@
|
|
|
169
104
|
}
|
|
170
105
|
|
|
171
106
|
/* ── Main ─────────────────────────────────────────────────────────────── */
|
|
172
|
-
main {
|
|
107
|
+
.page-main {
|
|
173
108
|
flex: 1;
|
|
174
109
|
display: flex;
|
|
175
110
|
flex-direction: column;
|
|
@@ -178,9 +113,12 @@
|
|
|
178
113
|
}
|
|
179
114
|
|
|
180
115
|
/* ── Service cards ────────────────────────────────────────────────────── */
|
|
116
|
+
/* The cards take the page; output is the shell's runs dock (R16). */
|
|
181
117
|
.services-pane {
|
|
182
118
|
padding: 12px 14px;
|
|
183
|
-
flex
|
|
119
|
+
flex: 1 1 auto;
|
|
120
|
+
min-height: 0;
|
|
121
|
+
overflow-y: auto;
|
|
184
122
|
display: flex;
|
|
185
123
|
flex-direction: column;
|
|
186
124
|
gap: 14px;
|
|
@@ -231,7 +169,7 @@
|
|
|
231
169
|
}
|
|
232
170
|
.service-card.subservice {
|
|
233
171
|
border-style: dashed;
|
|
234
|
-
border-color:
|
|
172
|
+
border-color: var(--border-strong);
|
|
235
173
|
/* Left rule stays solid on both — a 3px dashed vertical rule renders as
|
|
236
174
|
scattered dots. The dashed outer border carries the distinction. */
|
|
237
175
|
border-left: 3px solid var(--muted);
|
|
@@ -245,6 +183,25 @@
|
|
|
245
183
|
gap: 8px;
|
|
246
184
|
margin-bottom: 10px;
|
|
247
185
|
}
|
|
186
|
+
.service-card-title {
|
|
187
|
+
flex: 1;
|
|
188
|
+
min-width: 0;
|
|
189
|
+
}
|
|
190
|
+
.pin-btn {
|
|
191
|
+
background: none;
|
|
192
|
+
border: 1px solid transparent;
|
|
193
|
+
border-radius: var(--radius-button);
|
|
194
|
+
color: var(--faint);
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
font-size: 14px;
|
|
197
|
+
padding: 0 4px;
|
|
198
|
+
line-height: 1.4;
|
|
199
|
+
}
|
|
200
|
+
.pin-btn:hover,
|
|
201
|
+
.pin-btn[aria-pressed="true"] {
|
|
202
|
+
color: var(--accent-text);
|
|
203
|
+
background: var(--sunken);
|
|
204
|
+
}
|
|
248
205
|
.service-card-header .dot {
|
|
249
206
|
width: 8px;
|
|
250
207
|
height: 8px;
|
|
@@ -254,6 +211,9 @@
|
|
|
254
211
|
font-weight: 600;
|
|
255
212
|
color: var(--text);
|
|
256
213
|
}
|
|
214
|
+
.service-repo .owners {
|
|
215
|
+
color: var(--accent-text);
|
|
216
|
+
}
|
|
257
217
|
.service-repo {
|
|
258
218
|
font-size: 11px;
|
|
259
219
|
color: var(--muted);
|
|
@@ -267,8 +227,7 @@
|
|
|
267
227
|
margin-top: 3px;
|
|
268
228
|
font-size: 11px;
|
|
269
229
|
font-family: var(--mono);
|
|
270
|
-
color: var(--accent);
|
|
271
|
-
text-decoration: none;
|
|
230
|
+
color: var(--accent-text);
|
|
272
231
|
word-break: break-all;
|
|
273
232
|
}
|
|
274
233
|
.site-link:hover,
|
|
@@ -292,16 +251,16 @@
|
|
|
292
251
|
color: var(--muted);
|
|
293
252
|
}
|
|
294
253
|
.badge.dev-managed {
|
|
295
|
-
border-color:
|
|
296
|
-
color:
|
|
254
|
+
border-color: color-mix(in srgb, var(--accent) 27%, transparent);
|
|
255
|
+
color: var(--accent-text);
|
|
297
256
|
}
|
|
298
257
|
.badge.managed {
|
|
299
|
-
border-color:
|
|
300
|
-
color:
|
|
258
|
+
border-color: var(--border-strong);
|
|
259
|
+
color: var(--text);
|
|
301
260
|
}
|
|
302
261
|
.badge.always-on {
|
|
303
|
-
border-color:
|
|
304
|
-
color:
|
|
262
|
+
border-color: color-mix(in srgb, var(--ok) 27%, transparent);
|
|
263
|
+
color: var(--ok);
|
|
305
264
|
}
|
|
306
265
|
.badge.branch {
|
|
307
266
|
border-color: var(--border);
|
|
@@ -327,7 +286,7 @@
|
|
|
327
286
|
}
|
|
328
287
|
.cmd-btn:hover:not(:disabled) {
|
|
329
288
|
border-color: var(--accent);
|
|
330
|
-
color: var(--accent);
|
|
289
|
+
color: var(--accent-text);
|
|
331
290
|
}
|
|
332
291
|
.cmd-btn:disabled {
|
|
333
292
|
opacity: 0.45;
|
|
@@ -337,8 +296,8 @@
|
|
|
337
296
|
border-color: var(--muted);
|
|
338
297
|
}
|
|
339
298
|
.cmd-btn.logs-btn:hover:not(:disabled) {
|
|
340
|
-
border-color: var(--
|
|
341
|
-
color: var(--
|
|
299
|
+
border-color: var(--warn);
|
|
300
|
+
color: var(--warn);
|
|
342
301
|
}
|
|
343
302
|
|
|
344
303
|
.empty-state {
|
|
@@ -347,569 +306,407 @@
|
|
|
347
306
|
font-size: 12px;
|
|
348
307
|
text-align: center;
|
|
349
308
|
}
|
|
350
|
-
|
|
351
|
-
/* ── Pane bar ─────────────────────────────────────────────────────────── */
|
|
352
|
-
.pane-bar {
|
|
353
|
-
display: flex;
|
|
354
|
-
align-items: center;
|
|
355
|
-
justify-content: space-between;
|
|
356
|
-
padding: 3px 12px;
|
|
357
|
-
background: var(--surface);
|
|
358
|
-
border-bottom: 1px solid var(--border);
|
|
359
|
-
flex-shrink: 0;
|
|
360
|
-
}
|
|
361
|
-
.pane-bar span {
|
|
362
|
-
font-size: 10px;
|
|
363
|
-
font-weight: 600;
|
|
364
|
-
text-transform: uppercase;
|
|
365
|
-
letter-spacing: 0.07em;
|
|
366
|
-
color: var(--muted);
|
|
367
|
-
}
|
|
368
|
-
.pane-bar button {
|
|
369
|
-
background: none;
|
|
370
|
-
border: none;
|
|
371
|
-
color: var(--muted);
|
|
372
|
-
font-size: 11px;
|
|
373
|
-
cursor: pointer;
|
|
374
|
-
padding: 2px 6px;
|
|
375
|
-
border-radius: 3px;
|
|
376
|
-
}
|
|
377
|
-
.pane-bar button:hover {
|
|
378
|
-
color: var(--text);
|
|
379
|
-
background: var(--border);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/* ── Output pane ──────────────────────────────────────────────────────── */
|
|
383
|
-
.output-pane {
|
|
384
|
-
flex: 1;
|
|
385
|
-
display: flex;
|
|
386
|
-
flex-direction: column;
|
|
387
|
-
min-height: 0;
|
|
388
|
-
}
|
|
389
|
-
#output {
|
|
390
|
-
flex: 1;
|
|
391
|
-
overflow-y: auto;
|
|
392
|
-
padding: 10px 14px;
|
|
393
|
-
font-family: var(--mono);
|
|
394
|
-
font-size: 11.5px;
|
|
395
|
-
line-height: 1.65;
|
|
396
|
-
white-space: pre-wrap;
|
|
397
|
-
word-break: break-all;
|
|
398
|
-
min-height: 0;
|
|
399
|
-
}
|
|
400
|
-
#output .cmd {
|
|
401
|
-
color: var(--accent);
|
|
402
|
-
}
|
|
403
|
-
#output .err {
|
|
404
|
-
color: var(--red);
|
|
405
|
-
}
|
|
406
|
-
#output .done-ok {
|
|
407
|
-
color: var(--green);
|
|
408
|
-
}
|
|
409
|
-
#output .done-fail {
|
|
410
|
-
color: var(--red);
|
|
411
|
-
}
|
|
412
309
|
</style>
|
|
413
310
|
</head>
|
|
414
311
|
<body>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
<
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
onclick="openChangelog()"
|
|
428
|
-
title="View changelog"
|
|
429
|
-
>
|
|
430
|
-
<span class="v-pip" id="v-pip"></span><span class="v-txt">v…</span>
|
|
431
|
-
</button>
|
|
432
|
-
<div class="health" id="health" title="Service health"></div>
|
|
433
|
-
<span
|
|
434
|
-
id="ws-pill"
|
|
435
|
-
style="
|
|
436
|
-
font-size: 11px;
|
|
437
|
-
font-family: var(--mono);
|
|
438
|
-
padding: 2px 8px;
|
|
439
|
-
border-radius: 10px;
|
|
440
|
-
border: 1px solid #388bfd44;
|
|
441
|
-
color: #58a6ff;
|
|
442
|
-
background: #58a6ff10;
|
|
443
|
-
display: none;
|
|
444
|
-
"
|
|
445
|
-
></span>
|
|
446
|
-
<div class="h-spacer"></div>
|
|
447
|
-
<nav>
|
|
448
|
-
<a href="/">Local Dev</a>
|
|
449
|
-
<a href="/services" class="active">Services</a>
|
|
450
|
-
<a href="/environments">Environments</a>
|
|
451
|
-
<a href="/release">Releases</a>
|
|
452
|
-
<a href="/logs"
|
|
453
|
-
>Logs
|
|
454
|
-
<span
|
|
455
|
-
id="log-badge"
|
|
456
|
-
style="
|
|
457
|
-
background: var(--yellow);
|
|
458
|
-
color: #0d1117;
|
|
459
|
-
font-size: 10px;
|
|
460
|
-
font-weight: 700;
|
|
461
|
-
padding: 1px 5px;
|
|
462
|
-
border-radius: 8px;
|
|
463
|
-
line-height: 1.4;
|
|
464
|
-
display: none;
|
|
465
|
-
"
|
|
466
|
-
></span
|
|
467
|
-
></a>
|
|
468
|
-
<a href="/tests">Tests</a>
|
|
469
|
-
</nav>
|
|
470
|
-
</header>
|
|
471
|
-
|
|
472
|
-
<div class="controls">
|
|
473
|
-
<select id="env-sel" title="Environment">
|
|
474
|
-
<option value="local">local</option>
|
|
475
|
-
</select>
|
|
476
|
-
<span style="font-size: 11px; color: var(--muted)">workspace:</span>
|
|
477
|
-
<select
|
|
478
|
-
id="workspace-sel"
|
|
479
|
-
title="Active workspace"
|
|
480
|
-
style="font-family: var(--mono)"
|
|
481
|
-
>
|
|
482
|
-
<option value="default">default</option>
|
|
483
|
-
</select>
|
|
484
|
-
<span id="status-text" style="font-size: 11px; color: var(--muted)"
|
|
485
|
-
>idle</span
|
|
486
|
-
>
|
|
487
|
-
</div>
|
|
488
|
-
|
|
489
|
-
<main>
|
|
490
|
-
<div class="services-pane" id="services-pane">
|
|
491
|
-
<div class="empty-state">Loading services…</div>
|
|
492
|
-
</div>
|
|
493
|
-
|
|
494
|
-
<div class="pane-bar">
|
|
495
|
-
<span>Output</span>
|
|
496
|
-
<button onclick="clearOutput()">Clear</button>
|
|
312
|
+
<!-- The shell: top bar and rail, the same on every page (shell.js). -->
|
|
313
|
+
<div id="den-shell" data-page="services"></div>
|
|
314
|
+
<script src="/shell.js"></script>
|
|
315
|
+
|
|
316
|
+
<!-- The page's one main landmark: everything after the shell. -->
|
|
317
|
+
<main id="den-content">
|
|
318
|
+
<!-- One workspace control: the shell's View (what this page lists) and
|
|
319
|
+
Focus (where commands go), not a select of its own. -->
|
|
320
|
+
<div class="page-main">
|
|
321
|
+
<div class="services-pane" id="services-pane">
|
|
322
|
+
<div class="empty-state">Loading services…</div>
|
|
323
|
+
</div>
|
|
497
324
|
</div>
|
|
498
325
|
|
|
499
|
-
<
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
326
|
+
<script type="module">
|
|
327
|
+
import {
|
|
328
|
+
initHealth,
|
|
329
|
+
refreshHealth,
|
|
330
|
+
initWorkspacePill,
|
|
331
|
+
applyDenConfig,
|
|
332
|
+
initLogBadge,
|
|
333
|
+
initChangelog,
|
|
334
|
+
computePrimaryByRepo,
|
|
335
|
+
envReady,
|
|
336
|
+
} from "/header.js";
|
|
337
|
+
import { ownersLabel } from "/workspace-picker.js";
|
|
338
|
+
|
|
339
|
+
let es = null;
|
|
340
|
+
// The run tab `es` feeds in the shell's runs dock (R16): one per
|
|
341
|
+
// service, its label the latest command ("restart shop/web").
|
|
342
|
+
let runId = null;
|
|
343
|
+
const runs = () => window.denShell.runs;
|
|
344
|
+
|
|
345
|
+
// Release any open SSE socket on reload / navigation / bfcache-enter so the
|
|
346
|
+
// reloaded page isn't starved of a connection by a lingering stream.
|
|
347
|
+
window.addEventListener("pagehide", () => {
|
|
348
|
+
try {
|
|
349
|
+
es?.close();
|
|
350
|
+
} catch {}
|
|
351
|
+
es = null;
|
|
352
|
+
});
|
|
525
353
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
354
|
+
// ── Init ──────────────────────────────────────────────────────────────────
|
|
355
|
+
async function init() {
|
|
356
|
+
// The shell's env picker (header.js), one for every page.
|
|
357
|
+
await envReady();
|
|
358
|
+
|
|
359
|
+
await Promise.all([
|
|
360
|
+
loadServices(),
|
|
361
|
+
initHealth(),
|
|
362
|
+
initWorkspacePill(),
|
|
363
|
+
applyDenConfig(),
|
|
364
|
+
initChangelog(),
|
|
365
|
+
]);
|
|
366
|
+
initLogBadge();
|
|
367
|
+
}
|
|
538
368
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
loadServices()
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
]);
|
|
547
|
-
initLogBadge();
|
|
548
|
-
}
|
|
369
|
+
document.getElementById("env-sel").addEventListener("change", () => {
|
|
370
|
+
const env = document.getElementById("env-sel").value;
|
|
371
|
+
loadServices();
|
|
372
|
+
// Restart the header health poll for the new env — without this the dots
|
|
373
|
+
// kept polling the page-load env forever (index.html/logs.html do this too).
|
|
374
|
+
refreshHealth(env);
|
|
375
|
+
});
|
|
549
376
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
refreshHealth(env);
|
|
557
|
-
});
|
|
377
|
+
// ── Workspace panel ───────────────────────────────────────────────────────
|
|
378
|
+
window.toggleWsPanel = function () {
|
|
379
|
+
const panel = document.getElementById("ws-panel");
|
|
380
|
+
const open = panel.classList.toggle("open");
|
|
381
|
+
if (open) refreshWsPanel();
|
|
382
|
+
};
|
|
558
383
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
method: "POST",
|
|
565
|
-
headers: { "Content-Type": "application/json" },
|
|
566
|
-
body: JSON.stringify({ name }),
|
|
567
|
-
});
|
|
568
|
-
await loadServices();
|
|
569
|
-
});
|
|
384
|
+
async function refreshWsPanel() {
|
|
385
|
+
const data = await fetch("/api/workspaces")
|
|
386
|
+
.then((r) => r.json())
|
|
387
|
+
.catch(() => null);
|
|
388
|
+
if (!data) return;
|
|
570
389
|
|
|
571
|
-
// ── Load workspaces ───────────────────────────────────────────────────────
|
|
572
|
-
async function loadWorkspaces() {
|
|
573
|
-
try {
|
|
574
|
-
const data = await fetch("/api/workspaces").then((r) => r.json());
|
|
575
|
-
const sel = document.getElementById("workspace-sel");
|
|
576
|
-
const names = Object.keys(data.workspaces ?? {});
|
|
577
390
|
const focus = Array.isArray(data.active)
|
|
578
391
|
? (data.focus ?? data.active[0])
|
|
579
392
|
: data.active;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
.catch(() => null);
|
|
602
|
-
if (!data) return;
|
|
603
|
-
|
|
604
|
-
const focus = Array.isArray(data.active)
|
|
605
|
-
? (data.focus ?? data.active[0])
|
|
606
|
-
: data.active;
|
|
607
|
-
// Update active label
|
|
608
|
-
document.getElementById("ws-active-label").textContent = Array.isArray(
|
|
609
|
-
data.active,
|
|
610
|
-
)
|
|
611
|
-
? data.active.join(", ")
|
|
612
|
-
: (data.active ?? "");
|
|
613
|
-
|
|
614
|
-
// Current services list
|
|
615
|
-
const services = Object.entries(
|
|
616
|
-
data.workspaces?.[focus]?.services ?? {},
|
|
617
|
-
);
|
|
618
|
-
const listEl = document.getElementById("ws-service-list");
|
|
619
|
-
if (services.length === 0) {
|
|
620
|
-
listEl.innerHTML = '<span class="ws-msg">No services yet.</span>';
|
|
621
|
-
} else {
|
|
622
|
-
listEl.innerHTML = services
|
|
623
|
-
.map(([addr, entry]) => {
|
|
624
|
-
const meta =
|
|
625
|
-
entry.mode === "dev-managed"
|
|
626
|
-
? `<span style="color:var(--muted);font-size:11px">dev-managed · ${entry.path ?? ""}</span>`
|
|
627
|
-
: `<span style="color:var(--muted);font-size:11px">managed · ${entry.branch ?? "main"}</span>`;
|
|
628
|
-
return `<div class="ws-service-row">
|
|
393
|
+
// Update active label
|
|
394
|
+
document.getElementById("ws-active-label").textContent =
|
|
395
|
+
Array.isArray(data.active)
|
|
396
|
+
? data.active.join(", ")
|
|
397
|
+
: (data.active ?? "");
|
|
398
|
+
|
|
399
|
+
// Current services list
|
|
400
|
+
const services = Object.entries(
|
|
401
|
+
data.workspaces?.[focus]?.services ?? {},
|
|
402
|
+
);
|
|
403
|
+
const listEl = document.getElementById("ws-service-list");
|
|
404
|
+
if (services.length === 0) {
|
|
405
|
+
listEl.innerHTML = '<span class="ws-msg">No services yet.</span>';
|
|
406
|
+
} else {
|
|
407
|
+
listEl.innerHTML = services
|
|
408
|
+
.map(([addr, entry]) => {
|
|
409
|
+
const meta =
|
|
410
|
+
entry.mode === "dev-managed"
|
|
411
|
+
? `<span style="color:var(--muted);font-size:11px">dev-managed · ${entry.path ?? ""}</span>`
|
|
412
|
+
: `<span style="color:var(--muted);font-size:11px">managed · ${entry.branch ?? "main"}</span>`;
|
|
413
|
+
return `<div class="ws-service-row">
|
|
629
414
|
<span>${addr}</span>${meta}
|
|
630
415
|
<button title="Remove" onclick="removeService('${addr}')">×</button>
|
|
631
416
|
</div>`;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
// Available repos dropdown
|
|
637
|
-
const repos = await fetch("/api/workspaces/available-repos")
|
|
638
|
-
.then((r) => r.json())
|
|
639
|
-
.catch(() => []);
|
|
640
|
-
const repoSel = document.getElementById("ws-repo-sel");
|
|
641
|
-
repoSel.innerHTML =
|
|
642
|
-
'<option value="">Select repo…</option>' +
|
|
643
|
-
repos
|
|
644
|
-
.map((r) => {
|
|
645
|
-
const label = r.hasManifest ? `${r.name} ✓` : r.name;
|
|
646
|
-
const disabled = r.alreadyAdded ? " disabled" : "";
|
|
647
|
-
const suffix = r.alreadyAdded ? " (added)" : "";
|
|
648
|
-
return `<option value="${r.name}" data-path="${r.absPath}"${disabled}>${label}${suffix}</option>`;
|
|
649
|
-
})
|
|
650
|
-
.join("");
|
|
651
|
-
}
|
|
417
|
+
})
|
|
418
|
+
.join("");
|
|
419
|
+
}
|
|
652
420
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
421
|
+
// Available repos dropdown
|
|
422
|
+
const repos = await fetch("/api/workspaces/available-repos")
|
|
423
|
+
.then((r) => r.json())
|
|
424
|
+
.catch(() => []);
|
|
425
|
+
const repoSel = document.getElementById("ws-repo-sel");
|
|
426
|
+
repoSel.innerHTML =
|
|
427
|
+
'<option value="">Select repo…</option>' +
|
|
428
|
+
repos
|
|
429
|
+
.map((r) => {
|
|
430
|
+
const label = r.hasManifest ? `${r.name} ✓` : r.name;
|
|
431
|
+
const disabled = r.alreadyAdded ? " disabled" : "";
|
|
432
|
+
const suffix = r.alreadyAdded ? " (added)" : "";
|
|
433
|
+
return `<option value="${r.name}" data-path="${r.absPath}"${disabled}>${label}${suffix}</option>`;
|
|
434
|
+
})
|
|
435
|
+
.join("");
|
|
660
436
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
437
|
+
|
|
438
|
+
window.createWorkspace = async function () {
|
|
439
|
+
const name = document.getElementById("ws-name-input").value.trim();
|
|
440
|
+
const msg = document.getElementById("ws-create-msg");
|
|
441
|
+
if (!name) {
|
|
442
|
+
msg.textContent = "Name required";
|
|
443
|
+
msg.className = "ws-msg err";
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
await window.denShell.api("POST", "/api/workspaces/create", {
|
|
448
|
+
name,
|
|
449
|
+
});
|
|
450
|
+
} catch (err) {
|
|
451
|
+
// R26c: the server's message (its JSON error), inline.
|
|
452
|
+
msg.textContent = err.message;
|
|
453
|
+
msg.className = "ws-msg err";
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
667
456
|
msg.textContent = `✓ Created "${name}"`;
|
|
668
457
|
msg.className = "ws-msg ok";
|
|
669
458
|
document.getElementById("ws-name-input").value = "";
|
|
670
|
-
await loadWorkspaces();
|
|
671
459
|
await refreshWsPanel();
|
|
672
|
-
}
|
|
673
|
-
msg.textContent = await res.text();
|
|
674
|
-
msg.className = "ws-msg err";
|
|
675
|
-
}
|
|
676
|
-
};
|
|
460
|
+
};
|
|
677
461
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
msg
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
462
|
+
window.deleteWorkspace = async function () {
|
|
463
|
+
const data = await fetch("/api/workspaces")
|
|
464
|
+
.then((r) => r.json())
|
|
465
|
+
.catch(() => ({}));
|
|
466
|
+
const name = data.focus ?? data.active?.[0] ?? "default";
|
|
467
|
+
const msg = document.getElementById("ws-delete-msg");
|
|
468
|
+
if (name === "default") {
|
|
469
|
+
msg.textContent = "Cannot delete default";
|
|
470
|
+
msg.className = "ws-msg err";
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (!confirm(`Delete workspace "${name}"?`)) return;
|
|
474
|
+
try {
|
|
475
|
+
await window.denShell.api("DELETE", "/api/workspaces", { name });
|
|
476
|
+
} catch (err) {
|
|
477
|
+
// R26c: the server's message (its JSON error), inline.
|
|
478
|
+
msg.textContent = err.message;
|
|
479
|
+
msg.className = "ws-msg err";
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
693
482
|
msg.textContent = `✓ Deleted "${name}"`;
|
|
694
483
|
msg.className = "ws-msg ok";
|
|
695
|
-
await loadWorkspaces();
|
|
696
484
|
await Promise.all([loadServices(), refreshWsPanel()]);
|
|
697
|
-
}
|
|
698
|
-
msg.textContent = await res.text();
|
|
699
|
-
msg.className = "ws-msg err";
|
|
700
|
-
}
|
|
701
|
-
};
|
|
485
|
+
};
|
|
702
486
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
487
|
+
window.addService = async function () {
|
|
488
|
+
const repoSel = document.getElementById("ws-repo-sel");
|
|
489
|
+
const mode = document.getElementById("ws-mode-sel").value;
|
|
490
|
+
const msg = document.getElementById("ws-add-msg");
|
|
491
|
+
const repoName = repoSel.value;
|
|
492
|
+
if (!repoName) {
|
|
493
|
+
msg.textContent = "Select a repo";
|
|
494
|
+
msg.className = "ws-msg err";
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const opt = repoSel.options[repoSel.selectedIndex];
|
|
498
|
+
const linkedPath = opt.dataset.path;
|
|
499
|
+
try {
|
|
500
|
+
await window.denShell.api("POST", "/api/workspaces/services", {
|
|
501
|
+
address: repoName,
|
|
502
|
+
mode,
|
|
503
|
+
linkedPath,
|
|
504
|
+
});
|
|
505
|
+
} catch (err) {
|
|
506
|
+
// R26c: the server's message (its JSON error), inline.
|
|
507
|
+
msg.textContent = err.message;
|
|
508
|
+
msg.className = "ws-msg err";
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
721
511
|
msg.textContent = `✓ Added ${repoName}`;
|
|
722
512
|
msg.className = "ws-msg ok";
|
|
723
513
|
await Promise.all([loadServices(), refreshWsPanel()]);
|
|
724
|
-
}
|
|
725
|
-
msg.textContent = await res.text();
|
|
726
|
-
msg.className = "ws-msg err";
|
|
727
|
-
}
|
|
728
|
-
};
|
|
514
|
+
};
|
|
729
515
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
516
|
+
window.removeService = async function (address) {
|
|
517
|
+
const msg = document.getElementById("ws-add-msg");
|
|
518
|
+
try {
|
|
519
|
+
await window.denShell.api("DELETE", "/api/workspaces/services", {
|
|
520
|
+
address,
|
|
521
|
+
});
|
|
522
|
+
} catch (err) {
|
|
523
|
+
// R26c: the server's message (its JSON error), inline.
|
|
524
|
+
msg.textContent = err.message;
|
|
525
|
+
msg.className = "ws-msg err";
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
738
528
|
await Promise.all([loadServices(), refreshWsPanel()]);
|
|
739
|
-
}
|
|
740
|
-
msg.textContent = await res.text();
|
|
741
|
-
msg.className = "ws-msg err";
|
|
742
|
-
}
|
|
743
|
-
};
|
|
744
|
-
|
|
745
|
-
// ── Load services ─────────────────────────────────────────────────────────
|
|
746
|
-
async function loadServices() {
|
|
747
|
-
const env = document.getElementById("env-sel").value;
|
|
748
|
-
const pane = document.getElementById("services-pane");
|
|
749
|
-
pane.innerHTML = '<div class="empty-state">Loading…</div>';
|
|
750
|
-
|
|
751
|
-
let services;
|
|
752
|
-
let apps = [];
|
|
753
|
-
try {
|
|
754
|
-
const [svcRes, appRes] = await Promise.all([
|
|
755
|
-
fetch(`/api/services?env=${env}`),
|
|
756
|
-
fetch("/api/applications").catch(() => null),
|
|
757
|
-
]);
|
|
758
|
-
services = await svcRes.json();
|
|
759
|
-
if (appRes?.ok) apps = (await appRes.json()).applications ?? [];
|
|
760
|
-
} catch {
|
|
761
|
-
pane.innerHTML =
|
|
762
|
-
'<div class="empty-state">Failed to load services</div>';
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
529
|
+
};
|
|
765
530
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
531
|
+
// ── Load services ─────────────────────────────────────────────────────────
|
|
532
|
+
async function loadServices() {
|
|
533
|
+
const env = document.getElementById("env-sel").value;
|
|
534
|
+
const pane = document.getElementById("services-pane");
|
|
535
|
+
pane.innerHTML = '<div class="empty-state">Loading…</div>';
|
|
536
|
+
|
|
537
|
+
let services;
|
|
538
|
+
let apps = [];
|
|
539
|
+
try {
|
|
540
|
+
const [svcRes, appRes] = await Promise.all([
|
|
541
|
+
fetch(`/api/services?env=${env}`),
|
|
542
|
+
fetch("/api/applications").catch(() => null),
|
|
543
|
+
]);
|
|
544
|
+
services = await svcRes.json();
|
|
545
|
+
if (appRes?.ok) apps = (await appRes.json()).applications ?? [];
|
|
546
|
+
} catch {
|
|
547
|
+
pane.innerHTML =
|
|
548
|
+
'<div class="empty-state">Failed to load services</div>';
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
770
551
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const primaryByRepo = computePrimaryByRepo(services);
|
|
552
|
+
if (!services.length) {
|
|
553
|
+
pane.innerHTML = `<div class="empty-state">No services registered.<br><code>__DEN_CLI__ register <path></code> to add a repo.</div>`;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
776
556
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
557
|
+
// A "system" is a repo's primary service (entrypoint > root-named >
|
|
558
|
+
// first seen) — the same rollup the health dots, log tabs and logs
|
|
559
|
+
// toolbar use, so "one thing per system" means the same thing here.
|
|
560
|
+
// Everything else is a sub-service.
|
|
561
|
+
const primaryByRepo = computePrimaryByRepo(services);
|
|
562
|
+
|
|
563
|
+
// The shared-infra repo's own sub-services (`graph.infra.repo`; the
|
|
564
|
+
// server marks them `infra`) are the shared infra that repo IS. They
|
|
565
|
+
// carry no usable actions of their own — their only command was a
|
|
566
|
+
// lifecycle one, already dropped for sub-services — so the cards were
|
|
567
|
+
// pure noise beside the system card that already represents them.
|
|
568
|
+
// Sub-services of OTHER repos are kept: several own real work
|
|
569
|
+
// (shop/db's migrate, notes-api/db's lint) that exists nowhere else.
|
|
570
|
+
const visible = services.filter(
|
|
571
|
+
(s) =>
|
|
572
|
+
!s.infra ||
|
|
573
|
+
primaryByRepo.get(s.repo) ===
|
|
574
|
+
(s.address ?? `${s.repo}/${s.service}`),
|
|
575
|
+
);
|
|
790
576
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
? -1
|
|
803
|
-
: 0) -
|
|
804
|
-
(primaryByRepo.get(b.repo) ===
|
|
805
|
-
(b.address ?? `${b.repo}/${b.service}`)
|
|
577
|
+
// Top-down dependency order — entry repo (shop-web) first, infra-stack
|
|
578
|
+
// last — matching the health row, the dep graph and `__DEN_CLI__ status`.
|
|
579
|
+
// graphRank comes from the server, which owns deriveGraph. Sub-services
|
|
580
|
+
// sort under their own system rather than floating to the end.
|
|
581
|
+
visible.sort(
|
|
582
|
+
(a, b) =>
|
|
583
|
+
(a.graphRank ?? Number.MAX_SAFE_INTEGER) -
|
|
584
|
+
(b.graphRank ?? Number.MAX_SAFE_INTEGER) ||
|
|
585
|
+
a.repo.localeCompare(b.repo) ||
|
|
586
|
+
(primaryByRepo.get(a.repo) ===
|
|
587
|
+
(a.address ?? `${a.repo}/${a.service}`)
|
|
806
588
|
? -1
|
|
807
|
-
: 0)
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
// Domain commands are NOT hidden: shop/db's migrate/seed/rollback and
|
|
815
|
-
// notes-api/db's migrate/lint are real work you can only do here.
|
|
816
|
-
const LIFECYCLE_CMDS = new Set(["up", "down", "restart"]);
|
|
589
|
+
: 0) -
|
|
590
|
+
(primaryByRepo.get(b.repo) ===
|
|
591
|
+
(b.address ?? `${b.repo}/${b.service}`)
|
|
592
|
+
? -1
|
|
593
|
+
: 0) ||
|
|
594
|
+
a.service.localeCompare(b.service),
|
|
595
|
+
);
|
|
817
596
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
)
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
? `<span class="badge" style="border-color:var(--yellow);color:var(--yellow)" title="Not cloned yet">⬇ not cloned</span>`
|
|
597
|
+
// Lifecycle commands on a sub-service are a lie: the resource is owned by
|
|
598
|
+
// its system or by shared infra-stack infra (files-api/db, for one,
|
|
599
|
+
// is a handle onto shared localstack — `up` there does nothing useful).
|
|
600
|
+
// Domain commands are NOT hidden: shop/db's migrate/seed/rollback and
|
|
601
|
+
// notes-api/db's migrate/lint are real work you can only do here.
|
|
602
|
+
const LIFECYCLE_CMDS = new Set(["up", "down", "restart"]);
|
|
603
|
+
|
|
604
|
+
// Pin a system (or a sub-service) to the rail (R14).
|
|
605
|
+
const pinBtn = (id) => {
|
|
606
|
+
const on = window.denShell?.isPinned(id);
|
|
607
|
+
if (!window.denShell) return "";
|
|
608
|
+
return `<button class="pin-btn" data-pin-toggle="${esc(id)}" aria-pressed="${on ? "true" : "false"}" title="${on ? "Unpin" : "Pin"} ${esc(id)}" aria-label="${on ? "Unpin" : "Pin"} ${esc(id)}">${on ? "★" : "☆"}</button>`;
|
|
609
|
+
};
|
|
610
|
+
const cardHtml = (s) => {
|
|
611
|
+
const addr = s.address ?? `${s.repo}/${s.service}`;
|
|
612
|
+
const isSub = primaryByRepo.get(s.repo) !== addr;
|
|
613
|
+
const usableCmds = isSub
|
|
614
|
+
? s.commands.filter((cmd) => !LIFECYCLE_CMDS.has(cmd))
|
|
615
|
+
: s.commands;
|
|
616
|
+
const cmdButtons = usableCmds
|
|
617
|
+
.map(
|
|
618
|
+
(cmd) =>
|
|
619
|
+
`<button class="cmd-btn" data-repo="${s.repo}" data-service="${s.service}" data-cmd="${cmd}" data-type="exec">${cmd}</button>`,
|
|
620
|
+
)
|
|
621
|
+
.join("");
|
|
622
|
+
const logsButton = s.logs
|
|
623
|
+
? `<button class="cmd-btn logs-btn" data-repo="${s.repo}" data-service="${s.service}" data-type="logs">logs</button>`
|
|
846
624
|
: "";
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
625
|
+
const hasCaps = usableCmds.length > 0 || s.logs;
|
|
626
|
+
const emptyNote =
|
|
627
|
+
s.cloned === false
|
|
628
|
+
? "not cloned yet — run Sync in Workspaces"
|
|
629
|
+
: isSub
|
|
630
|
+
? "managed by its system"
|
|
631
|
+
: "no commands for this env";
|
|
632
|
+
const modeBadge = s.mode
|
|
633
|
+
? `<span class="badge ${s.mode}">${s.mode}</span>`
|
|
854
634
|
: "";
|
|
855
|
-
|
|
856
|
-
|
|
635
|
+
const unclonedBadge =
|
|
636
|
+
s.cloned === false
|
|
637
|
+
? `<span class="badge" style="border-color:var(--warn);color:var(--warn)" title="Not cloned yet">⬇ not cloned</span>`
|
|
638
|
+
: "";
|
|
639
|
+
const branchBadge = s.branch
|
|
640
|
+
? `<span class="badge branch">${s.branch}</span>`
|
|
641
|
+
: "";
|
|
642
|
+
// Which manifest layers built this card: repo or held, + override.
|
|
643
|
+
const layersBadge =
|
|
644
|
+
Array.isArray(s.manifestLayers) && s.manifestLayers.length > 0
|
|
645
|
+
? `<span class="badge layers" title="Manifest layers applied">manifest: ${s.manifestLayers.join(" + ")}</span>`
|
|
646
|
+
: "";
|
|
647
|
+
return `
|
|
648
|
+
<div class="service-card${isSub ? " subservice" : ""}"${isSub ? "" : ` id="repo-${esc(s.repo)}"`}>
|
|
857
649
|
<div class="service-card-header">
|
|
858
650
|
<span class="dot" id="dot-${s.repo}-${s.service}"></span>
|
|
859
|
-
<div>
|
|
651
|
+
<div class="service-card-title">
|
|
860
652
|
<div class="service-name">${addr}</div>
|
|
861
|
-
<div class="service-repo">${s.repo}
|
|
653
|
+
<div class="service-repo">${s.repo}${
|
|
654
|
+
ownersLabel(s.inWorkspaces)
|
|
655
|
+
? `<span class="owners" title="Shared: listed once for every workspace in view that defines it">${esc(ownersLabel(s.inWorkspaces))}</span>`
|
|
656
|
+
: ""
|
|
657
|
+
}</div>
|
|
862
658
|
<a class="site-link" id="site-${s.repo}-${s.service}" target="_blank" rel="noopener" hidden></a>
|
|
863
659
|
</div>
|
|
660
|
+
${pinBtn(isSub ? addr : s.repo)}
|
|
864
661
|
</div>
|
|
865
662
|
<div class="service-meta">${modeBadge}${branchBadge}${layersBadge}${unclonedBadge}</div>
|
|
866
663
|
<div class="cmd-row">
|
|
867
664
|
${hasCaps ? cmdButtons + logsButton : `<span style="color:var(--muted);font-size:11px">${emptyNote}</span>`}
|
|
868
665
|
</div>
|
|
869
666
|
</div>`;
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// Section the cards by application, keeping the order above within
|
|
670
|
+
// each. Repos that are their own application share one section; any
|
|
671
|
+
// card no application claims goes last.
|
|
672
|
+
const esc = (t) =>
|
|
673
|
+
String(t ?? "").replace(
|
|
674
|
+
/[&<>"]/g,
|
|
675
|
+
(c) =>
|
|
676
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
|
677
|
+
);
|
|
678
|
+
const appOf = new Map();
|
|
679
|
+
for (const a of apps) for (const r of a.repos) appOf.set(r, a);
|
|
680
|
+
const sections = [];
|
|
681
|
+
for (const a of apps.filter((x) => x.source === "declared")) {
|
|
682
|
+
const cards = visible.filter((s) => appOf.get(s.repo) === a);
|
|
683
|
+
if (cards.length) sections.push({ app: a, cards });
|
|
684
|
+
}
|
|
685
|
+
const standalone = visible.filter(
|
|
686
|
+
(s) => appOf.get(s.repo)?.source === "repo",
|
|
880
687
|
);
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
(s) => appOf.
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
? {
|
|
904
|
-
title: "Not in an application",
|
|
905
|
-
description: "Registered, but not in this workspace.",
|
|
906
|
-
}
|
|
907
|
-
: null,
|
|
908
|
-
cards: rest,
|
|
909
|
-
});
|
|
910
|
-
pane.innerHTML = sections
|
|
911
|
-
.map(
|
|
912
|
-
({ app, cards }) => `
|
|
688
|
+
if (standalone.length)
|
|
689
|
+
sections.push({
|
|
690
|
+
app: {
|
|
691
|
+
title: "Single-repo applications",
|
|
692
|
+
description: "Each repo is its own application.",
|
|
693
|
+
},
|
|
694
|
+
cards: standalone,
|
|
695
|
+
});
|
|
696
|
+
const rest = visible.filter((s) => !appOf.has(s.repo));
|
|
697
|
+
if (rest.length)
|
|
698
|
+
sections.push({
|
|
699
|
+
app: apps.length
|
|
700
|
+
? {
|
|
701
|
+
title: "Not in an application",
|
|
702
|
+
description: "In view, but in no application.",
|
|
703
|
+
}
|
|
704
|
+
: null,
|
|
705
|
+
cards: rest,
|
|
706
|
+
});
|
|
707
|
+
pane.innerHTML = sections
|
|
708
|
+
.map(
|
|
709
|
+
({ app, cards }) => `
|
|
913
710
|
<section class="app-section"${app?.name ? ` data-app="${esc(app.name)}"` : ""}>
|
|
914
711
|
${
|
|
915
712
|
app
|
|
@@ -922,182 +719,211 @@
|
|
|
922
719
|
}
|
|
923
720
|
<div class="app-cards">${cards.map(cardHtml).join("")}</div>
|
|
924
721
|
</section>`,
|
|
925
|
-
|
|
926
|
-
|
|
722
|
+
)
|
|
723
|
+
.join("");
|
|
927
724
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
725
|
+
// Pin / unpin (a preference only). The star repaints from the
|
|
726
|
+
// shell's den:pins event, whoever changed the pin (this button,
|
|
727
|
+
// the rail's × or Delete, the graph's panel).
|
|
728
|
+
pane.querySelectorAll("[data-pin-toggle]").forEach((btn) => {
|
|
729
|
+
btn.addEventListener("click", () => {
|
|
730
|
+
const id = btn.dataset.pinToggle;
|
|
731
|
+
const sh = window.denShell;
|
|
732
|
+
if (!sh) return;
|
|
733
|
+
if (sh.isPinned(id)) sh.unpin(id);
|
|
734
|
+
else sh.pin(id);
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
// A pinned row in the rail links here (#repo-<name>).
|
|
738
|
+
if (location.hash.startsWith("#repo-"))
|
|
739
|
+
document
|
|
740
|
+
.getElementById(decodeURIComponent(location.hash.slice(1)))
|
|
741
|
+
?.scrollIntoView({ block: "center" });
|
|
742
|
+
|
|
743
|
+
// Attach click handlers
|
|
744
|
+
pane.querySelectorAll(".cmd-btn").forEach((btn) => {
|
|
745
|
+
btn.addEventListener("click", () => {
|
|
746
|
+
const { repo, service, cmd, type } = btn.dataset;
|
|
747
|
+
if (type === "exec") runExec(repo, service, cmd, btn);
|
|
748
|
+
else runLogs(repo, service, btn);
|
|
749
|
+
});
|
|
934
750
|
});
|
|
935
|
-
});
|
|
936
|
-
|
|
937
|
-
// Re-render destroys the dot elements, and the poll may have already
|
|
938
|
-
// fired before this ran — repaint from the last broadcast so the cards
|
|
939
|
-
// aren't blank until the next tick.
|
|
940
|
-
applyHealthToCards(_lastHealth);
|
|
941
|
-
}
|
|
942
751
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
es = null;
|
|
752
|
+
// Re-render destroys the dot elements, and the poll may have already
|
|
753
|
+
// fired before this ran — repaint from the last broadcast so the cards
|
|
754
|
+
// aren't blank until the next tick.
|
|
755
|
+
applyHealthToCards(_lastHealth);
|
|
948
756
|
}
|
|
949
|
-
const env = document.getElementById("env-sel").value;
|
|
950
757
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
758
|
+
// ── Run command ───────────────────────────────────────────────────────────
|
|
759
|
+
/** The previous stream's tab stops pulsing: a new action replaced it. */
|
|
760
|
+
function endStream() {
|
|
761
|
+
if (es) {
|
|
762
|
+
es.close();
|
|
763
|
+
es = null;
|
|
764
|
+
if (runId) runs().set(runId, { running: false });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
955
767
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
768
|
+
function runExec(repo, service, cmd, btn) {
|
|
769
|
+
endStream();
|
|
770
|
+
const env = document.getElementById("env-sel").value;
|
|
771
|
+
|
|
772
|
+
disableAllButtons();
|
|
773
|
+
setDot(repo, service, "running");
|
|
774
|
+
runId = runs().start(
|
|
775
|
+
`${repo}/${service}`,
|
|
776
|
+
`${cmd} ${repo}/${service}`,
|
|
777
|
+
{
|
|
778
|
+
doneLine: false,
|
|
779
|
+
},
|
|
780
|
+
);
|
|
781
|
+
appendLine(`\n▶ ${repo}/${service} · ${cmd} · ${env}\n`, "cmd");
|
|
960
782
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
es
|
|
964
|
-
es = null;
|
|
783
|
+
const params = new URLSearchParams({ repo, service, cmd, env });
|
|
784
|
+
es = new EventSource(`/api/service/exec?${params}`);
|
|
785
|
+
handleSSE(es, repo, service);
|
|
965
786
|
}
|
|
966
|
-
const env = document.getElementById("env-sel").value;
|
|
967
787
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
788
|
+
function runLogs(repo, service, btn) {
|
|
789
|
+
endStream();
|
|
790
|
+
const env = document.getElementById("env-sel").value;
|
|
791
|
+
|
|
792
|
+
// A log tail is a continuous stream, not a command. It never emits
|
|
793
|
+
// `done`, so disabling the buttons here left every button on the page
|
|
794
|
+
// dead — cursor and all — until a manual reload. Tailing also says
|
|
795
|
+
// nothing about health, so the dot is left to the health poll rather
|
|
796
|
+
// than being forced to "running". Starting any other action closes this
|
|
797
|
+
// stream (both runners close `es` first).
|
|
798
|
+
// A tail is a stream, not a run: its tab doesn't pulse.
|
|
799
|
+
runId = runs().start(
|
|
800
|
+
`${repo}/${service}`,
|
|
801
|
+
`logs ${repo}/${service}`,
|
|
802
|
+
{
|
|
803
|
+
running: false,
|
|
804
|
+
doneLine: false,
|
|
805
|
+
// Clicking Logs is for its output: the dock opens (R16a).
|
|
806
|
+
open: true,
|
|
807
|
+
},
|
|
808
|
+
);
|
|
809
|
+
appendLine(`\n▶ ${repo}/${service} logs · ${env}\n`, "cmd");
|
|
976
810
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
811
|
+
const params = new URLSearchParams({ repo, service, env });
|
|
812
|
+
es = new EventSource(`/api/service/logs?${params}`);
|
|
813
|
+
handleSSE(es, repo, service, { stream: true });
|
|
814
|
+
}
|
|
981
815
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
if (msg.
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
816
|
+
function handleSSE(evtSrc, repo, service, opts = {}) {
|
|
817
|
+
// Streams (log tails) never disabled the buttons and never own the dot,
|
|
818
|
+
// so they must not re-enable or repaint either.
|
|
819
|
+
const isStream = opts.stream === true;
|
|
820
|
+
const id = runId;
|
|
821
|
+
const appendLine = (text, cls) => runs().append(id, text, cls);
|
|
822
|
+
evtSrc.onmessage = (e) => {
|
|
823
|
+
const msg = JSON.parse(e.data);
|
|
824
|
+
if (msg.type === "cmd") {
|
|
825
|
+
appendLine(msg.text, "cmd");
|
|
826
|
+
} else if (msg.type === "line") {
|
|
827
|
+
appendLine(msg.text, "");
|
|
828
|
+
} else if (msg.type === "error") {
|
|
829
|
+
appendLine(msg.text, "err");
|
|
830
|
+
} else if (msg.type === "done") {
|
|
831
|
+
const ok = msg.exitCode === 0;
|
|
832
|
+
if (msg.duration > 0) {
|
|
833
|
+
appendLine(
|
|
834
|
+
`\n${ok ? "✓ Done" : "✗ Failed"} in ${fmtDuration(msg.duration)}\n`,
|
|
835
|
+
ok ? "done-ok" : "done-fail",
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
if (!isStream) {
|
|
839
|
+
setDot(repo, service, ok ? "ok" : "err");
|
|
840
|
+
enableAllButtons();
|
|
841
|
+
runs().finish(id, { ok });
|
|
842
|
+
} else runs().set(id, { running: false });
|
|
843
|
+
evtSrc.close();
|
|
844
|
+
es = null;
|
|
1001
845
|
}
|
|
846
|
+
};
|
|
847
|
+
evtSrc.onerror = () => {
|
|
848
|
+
appendLine("Connection closed", "err");
|
|
849
|
+
// A tail ending is not a health signal — leave the dot to the poll.
|
|
1002
850
|
if (!isStream) {
|
|
1003
|
-
setDot(repo, service,
|
|
851
|
+
setDot(repo, service, "err");
|
|
1004
852
|
enableAllButtons();
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
evtSrc
|
|
853
|
+
runs().finish(id, { ok: false });
|
|
854
|
+
} else runs().set(id, { running: false });
|
|
855
|
+
evtSrc?.close();
|
|
1008
856
|
es = null;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
evtSrc.onerror = () => {
|
|
1012
|
-
appendLine("Connection closed", "err");
|
|
1013
|
-
// A tail ending is not a health signal — leave the dot to the poll.
|
|
1014
|
-
if (!isStream) {
|
|
1015
|
-
setDot(repo, service, "err");
|
|
1016
|
-
enableAllButtons();
|
|
1017
|
-
}
|
|
1018
|
-
setStatus("idle");
|
|
1019
|
-
evtSrc?.close();
|
|
1020
|
-
es = null;
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
1023
859
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
if (cls) span.className = cls;
|
|
1029
|
-
span.textContent = text + "\n";
|
|
1030
|
-
out.appendChild(span);
|
|
1031
|
-
out.scrollTop = out.scrollHeight;
|
|
1032
|
-
}
|
|
1033
|
-
function clearOutput() {
|
|
1034
|
-
document.getElementById("output").innerHTML = "";
|
|
1035
|
-
}
|
|
860
|
+
// ── Output: the run's tab in the shell's runs dock ─────────────────────────
|
|
861
|
+
function appendLine(text, cls) {
|
|
862
|
+
if (runId) runs().append(runId, text, cls);
|
|
863
|
+
}
|
|
1036
864
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
const el = document.getElementById(`dot-${repo}-${service}`);
|
|
1043
|
-
if (el) el.className = `dot ${state}`;
|
|
1044
|
-
}
|
|
865
|
+
// ── Dots ──────────────────────────────────────────────────────────────────
|
|
866
|
+
function setDot(repo, service, state) {
|
|
867
|
+
const el = document.getElementById(`dot-${repo}-${service}`);
|
|
868
|
+
if (el) el.className = `dot ${state}`;
|
|
869
|
+
}
|
|
1045
870
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
871
|
+
// ── Card health ───────────────────────────────────────────────────────────
|
|
872
|
+
// Card dots follow the header's existing poll instead of running their own:
|
|
873
|
+
// header.js broadcasts every poll and manual refresh as
|
|
874
|
+
// `__DEN_CLI__:health-update`, the same feed local-dev's graph subscribes to.
|
|
875
|
+
// Nothing had ever subscribed here, so a card dot was only written by a
|
|
876
|
+
// command's outcome — meaning it showed a stale exit status, or stayed
|
|
877
|
+
// muted forever on a page where you'd run nothing. That looked like the
|
|
878
|
+
// header and the cards disagreeing about health.
|
|
879
|
+
let _lastHealth = [];
|
|
880
|
+
function applyHealthToCards(results) {
|
|
881
|
+
for (const h of results || []) {
|
|
882
|
+
// ok === null means "no health check declared". Leave it muted rather
|
|
883
|
+
// than painting it red, which would claim the service is down.
|
|
884
|
+
const state = h.ok === true ? "ok" : h.ok === false ? "err" : "";
|
|
885
|
+
const el = document.getElementById(`dot-${h.repo}-${h.service}`);
|
|
886
|
+
if (el) el.className = `dot${state ? " " + state : ""}`;
|
|
887
|
+
const link = document.getElementById(`site-${h.repo}-${h.service}`);
|
|
888
|
+
if (link) {
|
|
889
|
+
if (h.openUrl) {
|
|
890
|
+
link.href = h.openUrl;
|
|
891
|
+
link.textContent = `${h.openUrl.replace(/^https?:\/\//, "").replace(/\/$/, "")} ↗`;
|
|
892
|
+
link.title = `Open ${h.openUrl} in a new tab`;
|
|
893
|
+
link.hidden = false;
|
|
894
|
+
} else {
|
|
895
|
+
link.hidden = true;
|
|
896
|
+
}
|
|
1071
897
|
}
|
|
1072
898
|
}
|
|
1073
899
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
});
|
|
900
|
+
document.addEventListener("__DEN_CLI__:health-update", (e) => {
|
|
901
|
+
_lastHealth = e.detail || [];
|
|
902
|
+
applyHealthToCards(_lastHealth);
|
|
903
|
+
});
|
|
1079
904
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
905
|
+
// ── Button helpers ────────────────────────────────────────────────────────
|
|
906
|
+
function disableAllButtons() {
|
|
907
|
+
document
|
|
908
|
+
.querySelectorAll(".cmd-btn")
|
|
909
|
+
.forEach((b) => (b.disabled = true));
|
|
910
|
+
}
|
|
911
|
+
function enableAllButtons() {
|
|
912
|
+
document
|
|
913
|
+
.querySelectorAll(".cmd-btn")
|
|
914
|
+
.forEach((b) => (b.disabled = false));
|
|
915
|
+
}
|
|
1091
916
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
917
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
918
|
+
function fmtDuration(ms) {
|
|
919
|
+
if (ms < 1000) return `${ms}ms`;
|
|
920
|
+
const s = Math.floor(ms / 1000),
|
|
921
|
+
m = Math.floor(s / 60);
|
|
922
|
+
return m > 0 ? `${m}m ${s % 60}s` : `${s}s`;
|
|
923
|
+
}
|
|
1099
924
|
|
|
1100
|
-
|
|
1101
|
-
|
|
925
|
+
init();
|
|
926
|
+
</script>
|
|
927
|
+
</main>
|
|
1102
928
|
</body>
|
|
1103
929
|
</html>
|