@monster-orange/den 0.2.7 → 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.
Files changed (139) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +619 -3
  5. package/make/den.mk +19 -4
  6. package/make/journeys.mk +2 -1
  7. package/make/local_dev.mk +6 -13
  8. package/make/secrets-1password.mk +18 -0
  9. package/make/test.mk +1 -1
  10. package/package.json +9 -2
  11. package/schemas/den.plugin.v1.json +156 -0
  12. package/schemas/den.theme.v1.json +269 -0
  13. package/scripts/auth-configure.ts +16 -8
  14. package/scripts/heal-config.ts +0 -5
  15. package/scripts/setup-1password.ts +41 -19
  16. package/scripts/setup-secrets.ts +67 -101
  17. package/src/pillars/api/global-setup.ts +1 -1
  18. package/src/pillars/journeys/sandbox.ts +23 -18
  19. package/src/pillars/journeys/tools/browser-session.ts +2 -2
  20. package/src/scripts/setup-default-workspace.ts +2 -2
  21. package/src/shared/agent-actor.ts +38 -3
  22. package/src/shared/auth/headless-login.ts +1 -1
  23. package/src/shared/auth/sudo-vault.ts +93 -31
  24. package/src/shared/auth/token-providers/index.ts +7 -3
  25. package/src/shared/brand.ts +7 -0
  26. package/src/shared/config-error.ts +63 -0
  27. package/src/shared/constants.ts +3 -24
  28. package/src/shared/den-config.ts +185 -7
  29. package/src/shared/edge/config.ts +147 -21
  30. package/src/shared/edge/exec.ts +31 -0
  31. package/src/shared/edge/included.ts +169 -0
  32. package/src/shared/edge/render.ts +69 -14
  33. package/src/shared/edge/runtime.ts +128 -121
  34. package/src/shared/edge/status.ts +68 -8
  35. package/src/shared/edge/tls.ts +328 -0
  36. package/src/shared/edge/types.ts +25 -9
  37. package/src/shared/environment-manager/catalog.ts +52 -1
  38. package/src/shared/environment-manager/default-workspace.ts +26 -3
  39. package/src/shared/environment-manager/git-remote.ts +49 -0
  40. package/src/shared/environment-manager/health.ts +73 -38
  41. package/src/shared/environment-manager/scope.ts +168 -0
  42. package/src/shared/environment-manager/service-runner.ts +789 -43
  43. package/src/shared/environment-manager/up-targets.ts +132 -0
  44. package/src/shared/environment-manager/workspace-manager.ts +67 -25
  45. package/src/shared/environment-manager/workspace-org.ts +26 -11
  46. package/src/shared/health/rollup.ts +48 -0
  47. package/src/shared/manifest/service-links.ts +20 -8
  48. package/src/shared/open-url.ts +21 -2
  49. package/src/shared/org-sources.ts +328 -0
  50. package/src/shared/org.ts +53 -5
  51. package/src/shared/plugins/manifest.ts +406 -0
  52. package/src/shared/plugins/store.ts +358 -0
  53. package/src/shared/process/legacy-adopt.ts +285 -0
  54. package/src/shared/process/process-group.ts +202 -0
  55. package/src/shared/process/spawn-streaming.ts +7 -0
  56. package/src/shared/secrets/doppler.ts +1 -1
  57. package/src/shared/secrets/index.ts +279 -50
  58. package/src/shared/secrets/none.ts +56 -0
  59. package/src/shared/secrets/strategies.ts +202 -0
  60. package/src/shared/secrets/types.ts +1 -1
  61. package/src/shared/smoke/git-remote.ts +40 -0
  62. package/src/shared/stacks/manifest-commands.ts +1 -1
  63. package/src/shared/stacks/namespace.ts +178 -0
  64. package/src/shared/stacks/owners.ts +303 -0
  65. package/src/shared/stacks/render.ts +8 -3
  66. package/src/shared/stacks/runtime.ts +100 -16
  67. package/src/shared/stacks/status.ts +63 -12
  68. package/src/shared/strategies/registry.ts +89 -0
  69. package/src/shared/theme/contract.ts +538 -0
  70. package/src/shared/types/index.ts +24 -5
  71. package/src/ui/cli/clone-plan.ts +154 -23
  72. package/src/ui/cli/create.ts +24 -7
  73. package/src/ui/cli/doctor-command.ts +131 -0
  74. package/src/ui/cli/edge-command.ts +56 -10
  75. package/src/ui/cli/flag-parser.ts +62 -3
  76. package/src/ui/cli/health-display.ts +25 -0
  77. package/src/ui/cli/index.ts +649 -72
  78. package/src/ui/cli/notice.ts +20 -0
  79. package/src/ui/cli/org-command.ts +12 -2
  80. package/src/ui/cli/plugin-command.ts +849 -0
  81. package/src/ui/cli/stack-command.ts +63 -2
  82. package/src/ui/cli/test-runner.ts +11 -9
  83. package/src/ui/cli/theme-command.ts +136 -0
  84. package/src/ui/cli/up-args.ts +15 -0
  85. package/src/ui/cli/web-command.ts +47 -23
  86. package/src/ui/mcp/api.ts +28 -2
  87. package/src/ui/mcp/scope.ts +298 -0
  88. package/src/ui/mcp/server.ts +70 -8
  89. package/src/ui/mcp/tools.ts +151 -24
  90. package/src/ui/server/appearance-prefs.ts +95 -0
  91. package/src/ui/server/audit.ts +90 -3
  92. package/src/ui/server/daemon-reaper.ts +29 -1
  93. package/src/ui/server/engine-stamp.ts +83 -0
  94. package/src/ui/server/github-service.ts +29 -45
  95. package/src/ui/server/graph-page.ts +20 -0
  96. package/src/ui/server/graph-prefs.ts +44 -1
  97. package/src/ui/server/http-errors.ts +293 -0
  98. package/src/ui/server/http-helpers.ts +22 -3
  99. package/src/ui/server/index.ts +590 -45
  100. package/src/ui/server/public/appearance.js +531 -0
  101. package/src/ui/server/public/edge-page.js +248 -0
  102. package/src/ui/server/public/edge.html +251 -0
  103. package/src/ui/server/public/environments.html +65 -83
  104. package/src/ui/server/public/error-page.js +247 -0
  105. package/src/ui/server/public/error.html +260 -0
  106. package/src/ui/server/public/graph-model.js +332 -14
  107. package/src/ui/server/public/graph.css +141 -47
  108. package/src/ui/server/public/graph.js +223 -44
  109. package/src/ui/server/public/header.js +396 -600
  110. package/src/ui/server/public/health-fit.js +248 -0
  111. package/src/ui/server/public/health-rollup.js +128 -0
  112. package/src/ui/server/public/i18n/es.js +287 -0
  113. package/src/ui/server/public/i18n/ja.js +276 -0
  114. package/src/ui/server/public/index.html +139 -438
  115. package/src/ui/server/public/local-dev.html +3468 -4044
  116. package/src/ui/server/public/logs.html +71 -208
  117. package/src/ui/server/public/panel.js +15 -3
  118. package/src/ui/server/public/release.html +175 -389
  119. package/src/ui/server/public/services.html +596 -770
  120. package/src/ui/server/public/settings-page.js +759 -0
  121. package/src/ui/server/public/settings-registry.js +521 -0
  122. package/src/ui/server/public/settings.html +590 -0
  123. package/src/ui/server/public/shell.js +3869 -0
  124. package/src/ui/server/public/topbar.js +1413 -0
  125. package/src/ui/server/public/version-label.js +12 -0
  126. package/src/ui/server/public/workspace-picker.js +74 -34
  127. package/src/ui/server/readonly-routes.ts +24 -4
  128. package/src/ui/server/release-service.ts +25 -3
  129. package/src/ui/server/services-list.ts +94 -17
  130. package/src/ui/server/settings-distribution.ts +74 -0
  131. package/src/ui/server/shell-prefs.ts +192 -0
  132. package/src/ui/server/theme-routes.ts +292 -0
  133. package/src/ui/server/theme-runtime.ts +407 -0
  134. package/src/ui/server/ui-assets.ts +16 -1
  135. package/src/ui/server/ui-language.ts +50 -0
  136. package/src/ui/server/workspace-routes.ts +55 -9
  137. package/themes/den/den-theme.json +72 -0
  138. package/themes/den/favicon.ico +0 -0
  139. 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__ · Releases</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: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
44
+ font-family: var(--sans);
35
45
  font-size: 13px;
36
46
  }
37
47
  body {
@@ -41,69 +51,11 @@
41
51
  }
42
52
 
43
53
  /* ── Header ───────────────────────────────────────────────────────────── */
44
- header {
45
- display: flex;
46
- align-items: center;
47
- gap: 10px;
48
- padding: 7px 14px;
49
- background: var(--surface);
50
- border-bottom: 1px solid var(--border);
51
- flex-shrink: 0;
52
- }
53
- header h1 {
54
- font-family: var(--mono);
55
- font-size: 14px;
56
- font-weight: 600;
57
- letter-spacing: -0.02em;
58
- }
59
54
  .h-spacer {
60
55
  flex: 1;
61
56
  }
62
57
 
63
- .v-pill {
64
- font-family: var(--mono);
65
- font-size: 10px;
66
- padding: 2px 8px;
67
- border-radius: 10px;
68
- border: 1px solid var(--border);
69
- color: var(--muted);
70
- background: none;
71
- cursor: pointer;
72
- user-select: none;
73
- position: relative;
74
- }
75
- .v-pill:hover {
76
- color: var(--accent);
77
- border-color: var(--accent);
78
- }
79
- .v-pip {
80
- position: absolute;
81
- top: -3px;
82
- right: -3px;
83
- width: 7px;
84
- height: 7px;
85
- border-radius: 50%;
86
- background: var(--accent);
87
- border: 1.5px solid var(--bg);
88
- display: none;
89
- }
90
-
91
58
  /* ── Health dots ──────────────────────────────────────────────────────── */
92
- .health {
93
- display: flex;
94
- align-items: center;
95
- gap: 10px;
96
- padding: 0 4px;
97
- flex-wrap: wrap;
98
- }
99
- .health-item,
100
- .health-group {
101
- display: flex;
102
- align-items: center;
103
- gap: 5px;
104
- font-size: 11px;
105
- color: var(--muted);
106
- }
107
59
  .dot {
108
60
  width: 7px;
109
61
  height: 7px;
@@ -113,38 +65,17 @@
113
65
  display: inline-block;
114
66
  }
115
67
  .dot.ok {
116
- background: var(--green);
68
+ background: var(--ok);
117
69
  }
118
70
  .dot.err {
119
- background: var(--red);
71
+ background: var(--bad);
120
72
  }
121
73
  .dot.running {
122
- background: var(--yellow);
74
+ background: var(--warn);
123
75
  animation: pulse 1s ease-in-out infinite;
124
76
  }
125
77
 
126
78
  /* ── Nav ──────────────────────────────────────────────────────────────── */
127
- nav {
128
- display: flex;
129
- gap: 2px;
130
- }
131
- nav a {
132
- color: var(--muted);
133
- text-decoration: none;
134
- padding: 3px 10px;
135
- border-radius: 4px;
136
- font-size: 12px;
137
- font-weight: 500;
138
- transition: color 0.1s;
139
- }
140
- nav a:hover {
141
- color: var(--text);
142
- background: var(--border);
143
- }
144
- nav a.active {
145
- color: var(--text);
146
- background: var(--surface);
147
- }
148
79
 
149
80
  /* ── Controls bar ─────────────────────────────────────────────────────── */
150
81
  .controls {
@@ -179,25 +110,25 @@
179
110
  }
180
111
  .btn:hover:not(:disabled) {
181
112
  border-color: var(--accent);
182
- color: var(--accent);
113
+ color: var(--accent-text);
183
114
  }
184
115
  .btn:disabled {
185
116
  opacity: 0.45;
186
117
  cursor: not-allowed;
187
118
  }
188
119
  .btn.primary {
189
- background: var(--accent);
190
- color: #04101f;
191
- border-color: var(--accent);
120
+ background: var(--accent-fill);
121
+ color: var(--on-accent);
122
+ border-color: var(--accent-fill);
192
123
  font-weight: 600;
193
124
  }
194
125
  .btn.primary:hover:not(:disabled) {
195
126
  opacity: 0.88;
196
- color: #04101f;
127
+ color: var(--on-accent);
197
128
  }
198
129
  .btn.danger:hover:not(:disabled) {
199
- border-color: var(--red);
200
- color: var(--red);
130
+ border-color: var(--bad);
131
+ color: var(--bad);
201
132
  }
202
133
  .env-label {
203
134
  font-size: 11px;
@@ -209,7 +140,7 @@
209
140
  }
210
141
 
211
142
  /* ── Main ─────────────────────────────────────────────────────────────── */
212
- main {
143
+ .page-main {
213
144
  flex: 1;
214
145
  display: flex;
215
146
  flex-direction: column;
@@ -251,7 +182,7 @@
251
182
  color: var(--text);
252
183
  }
253
184
  th .arrow {
254
- color: var(--accent);
185
+ color: var(--accent-text);
255
186
  font-size: 9px;
256
187
  margin-left: 3px;
257
188
  }
@@ -260,51 +191,20 @@
260
191
  border-bottom: 1px solid var(--border);
261
192
  vertical-align: middle;
262
193
  }
263
- /* Row hover. The previous #ffffff06 is white at 2.4%, resolving to about
264
- #13171c against the #0d1117 background — 1.05:1, effectively invisible.
265
- An accent tint at 20% lands on #1c2f45 (1.39:1), and the 3px accent
194
+ /* Row hover. The previous white wash at 2.4% resolved to about 1.05:1
195
+ against the background, effectively invisible. An accent tint at 20%
196
+ lands near 1.39:1 (in the original dark palette), and the 3px accent
266
197
  rule carries the rest of the signal. For scale, --surface itself is
267
198
  only 1.09:1 against the background, so this is a deliberate step past
268
199
  the page's usual contrast rather than a mistake.
269
200
  Applied to the cells, not the row: the table is border-collapse:
270
201
  collapse, where box-shadow on a <tr> is unreliable across browsers. */
271
202
  tbody tr:hover td {
272
- background: rgba(88, 166, 255, 0.2);
203
+ background: color-mix(in srgb, var(--accent) 20%, transparent);
273
204
  }
274
205
  tbody tr:hover td:first-child {
275
206
  box-shadow: inset 3px 0 0 var(--accent);
276
207
  }
277
- .session-chips {
278
- display: flex;
279
- flex-wrap: wrap;
280
- gap: 6px;
281
- padding: 6px 10px;
282
- border-bottom: 1px solid var(--border);
283
- background: var(--surface);
284
- }
285
- .session-chip {
286
- font-size: 11px;
287
- font-family: var(--mono);
288
- padding: 2px 8px;
289
- border-radius: 10px;
290
- border: 1px solid var(--border);
291
- background: transparent;
292
- color: var(--muted);
293
- cursor: pointer;
294
- }
295
- .session-chip:hover {
296
- color: var(--text);
297
- }
298
- .session-chip.active {
299
- border-color: var(--accent);
300
- color: var(--accent);
301
- }
302
- .session-chip.running {
303
- border-style: dashed;
304
- }
305
- .session-chip.fail {
306
- border-color: var(--red);
307
- }
308
208
  .repo-cell {
309
209
  font-weight: 600;
310
210
  }
@@ -315,7 +215,7 @@
315
215
  text-decoration: none;
316
216
  }
317
217
  .repo-link:hover {
318
- color: var(--accent);
218
+ color: var(--accent-text);
319
219
  text-decoration: underline;
320
220
  }
321
221
  .wave-tag {
@@ -345,8 +245,7 @@
345
245
  .deployer {
346
246
  font-family: var(--mono);
347
247
  font-size: 11px;
348
- color: var(--accent);
349
- text-decoration: none;
248
+ color: var(--accent-text);
350
249
  white-space: nowrap;
351
250
  }
352
251
  a.deployer:hover {
@@ -370,13 +269,13 @@
370
269
  font-weight: 500;
371
270
  }
372
271
  .st-up {
373
- color: var(--green);
272
+ color: var(--ok);
374
273
  }
375
274
  .st-warn {
376
- color: var(--yellow);
275
+ color: var(--warn);
377
276
  }
378
277
  .st-bad {
379
- color: var(--red);
278
+ color: var(--bad);
380
279
  }
381
280
  .st-none {
382
281
  color: var(--muted);
@@ -401,7 +300,7 @@
401
300
  #read-only-tag {
402
301
  display: none;
403
302
  font-size: 12px;
404
- color: var(--muted, #888);
303
+ color: var(--muted);
405
304
  border: 1px solid currentColor;
406
305
  border-radius: 4px;
407
306
  padding: 1px 6px;
@@ -423,8 +322,8 @@
423
322
  color 0.1s;
424
323
  }
425
324
  .dep-btn:hover:not(:disabled) {
426
- border-color: var(--green);
427
- color: var(--green);
325
+ border-color: var(--ok);
326
+ color: var(--ok);
428
327
  }
429
328
  .dep-btn:disabled {
430
329
  opacity: 0.4;
@@ -452,22 +351,22 @@
452
351
  flex-shrink: 0;
453
352
  }
454
353
  .run-pill.busy .dot {
455
- background: var(--yellow);
354
+ background: var(--warn);
456
355
  animation: pulse 1s ease-in-out infinite;
457
356
  }
458
357
  .run-pill.ok {
459
- color: var(--green);
460
- border-color: #3fb95044;
358
+ color: var(--ok);
359
+ border-color: color-mix(in srgb, var(--ok) 27%, transparent);
461
360
  }
462
361
  .run-pill.ok .dot {
463
- background: var(--green);
362
+ background: var(--ok);
464
363
  }
465
364
  .run-pill.err {
466
- color: var(--red);
467
- border-color: #f8514944;
365
+ color: var(--bad);
366
+ border-color: color-mix(in srgb, var(--bad) 27%, transparent);
468
367
  }
469
368
  .run-pill.err .dot {
470
- background: var(--red);
369
+ background: var(--bad);
471
370
  }
472
371
  @keyframes pulse {
473
372
  0%,
@@ -526,9 +425,9 @@
526
425
  border-radius: 3px;
527
426
  background: linear-gradient(
528
427
  90deg,
529
- #ffffff0d 25%,
530
- #ffffff1f 50%,
531
- #ffffff0d 75%
428
+ color-mix(in srgb, var(--text) 5%, transparent) 25%,
429
+ color-mix(in srgb, var(--text) 12%, transparent) 50%,
430
+ color-mix(in srgb, var(--text) 5%, transparent) 75%
532
431
  );
533
432
  background-size: 600px 100%;
534
433
  animation: rel-shimmer 1.5s linear infinite;
@@ -569,7 +468,7 @@
569
468
  animation: none;
570
469
  }
571
470
  .skel {
572
- background: #ffffff14;
471
+ background: color-mix(in srgb, var(--text) 8%, transparent);
573
472
  }
574
473
  }
575
474
 
@@ -604,43 +503,6 @@
604
503
  color: var(--text);
605
504
  background: var(--border);
606
505
  }
607
- .output-pane {
608
- height: 34%;
609
- display: flex;
610
- flex-direction: column;
611
- min-height: 0;
612
- }
613
- .output-pane.hidden {
614
- display: none;
615
- }
616
- #output {
617
- flex: 1;
618
- overflow-y: auto;
619
- padding: 10px 14px;
620
- font-family: var(--mono);
621
- font-size: 11.5px;
622
- line-height: 1.6;
623
- white-space: pre-wrap;
624
- word-break: break-all;
625
- min-height: 0;
626
- }
627
- #output .cmd {
628
- color: var(--accent);
629
- }
630
- #output .phase {
631
- color: var(--text);
632
- font-weight: 600;
633
- }
634
- #output .warn {
635
- color: var(--yellow);
636
- }
637
- #output .err {
638
- color: var(--red);
639
- }
640
- #output .ok {
641
- color: var(--green);
642
- }
643
-
644
506
  /* ── Modal ────────────────────────────────────────────────────────────── */
645
507
  .overlay {
646
508
  position: fixed;
@@ -663,7 +525,7 @@
663
525
  display: flex;
664
526
  flex-direction: column;
665
527
  overflow: hidden;
666
- box-shadow: 0 8px 40px rgba(0, 0, 0, 0.65);
528
+ box-shadow: var(--shadow);
667
529
  }
668
530
  .modal-hdr {
669
531
  display: flex;
@@ -727,14 +589,14 @@
727
589
  margin-bottom: 6px;
728
590
  }
729
591
  .banner.hotfix {
730
- border: 1px solid var(--yellow);
731
- background: #d2992215;
732
- color: var(--yellow);
592
+ border: 1px solid var(--warn);
593
+ background: color-mix(in srgb, var(--warn) 8%, transparent);
594
+ color: var(--warn);
733
595
  }
734
596
  .banner.prod {
735
- border: 1px solid var(--red);
736
- background: #f8514915;
737
- color: #ffb3ae;
597
+ border: 1px solid var(--bad);
598
+ background: color-mix(in srgb, var(--bad) 8%, transparent);
599
+ color: var(--bad);
738
600
  }
739
601
  .plan-wave {
740
602
  margin: 8px 0;
@@ -753,7 +615,7 @@
753
615
  padding: 2px 0 2px 12px;
754
616
  }
755
617
  .plan-repo .off {
756
- color: var(--yellow);
618
+ color: var(--warn);
757
619
  }
758
620
 
759
621
  /* wave editor */
@@ -798,80 +660,21 @@
798
660
  line-height: 1.5;
799
661
  }
800
662
  .err-text {
801
- color: var(--red);
663
+ color: var(--bad);
802
664
  font-size: 11px;
803
665
  }
804
666
  </style>
805
667
  </head>
806
668
  <body>
807
- <header>
808
- <img
809
- src="/favicon.ico"
810
- width="16"
811
- height="16"
812
- alt=""
813
- style="flex-shrink: 0"
814
- />
815
- <h1>__DEN_CLI__</h1>
816
- <button
817
- class="v-pill"
818
- id="version-pill"
819
- onclick="openChangelog()"
820
- title="View changelog"
821
- >
822
- <span class="v-pip" id="v-pip"></span><span class="v-txt">v…</span>
823
- </button>
824
- <div class="health" id="health" title="Local service health"></div>
825
- <!-- header.js polls /api/health with whatever #env-sel holds. This page's
826
- VISIBLE env picker is the deploy target, so it must not use that id —
827
- the header would then ask "are my local services healthy in sandbox?"
828
- and light up entirely red.
829
-
830
- This pinned-to-local select is what header.js reads. Deployed-env
831
- health is not shown here because it is not observable from a laptop:
832
- those checks target *.internal.<env>.example.dev (private, VPN-only) and
833
- `aws rds describe-db-instances` (needs credentials), so off-VPN every
834
- one fails and the row is a wall of red that means nothing. The
835
- deployed-env signal this page actually needs is the Status column,
836
- which compares main vs deployed SHA over the GitHub API and needs no
837
- network path to the services. -->
838
- <select
839
- id="env-sel"
840
- style="display: none"
841
- aria-hidden="true"
842
- tabindex="-1"
843
- >
844
- <option value="local" selected>local</option>
845
- </select>
846
- <div class="h-spacer"></div>
847
- <nav>
848
- <a href="/">Local Dev</a>
849
- <a href="/services">Services</a>
850
- <a href="/environments">Environments</a>
851
- <a href="/release" class="active">Releases</a>
852
- <a href="/logs"
853
- >Logs
854
- <span
855
- id="log-badge"
856
- style="
857
- background: var(--yellow);
858
- color: #0d1117;
859
- font-size: 10px;
860
- font-weight: 700;
861
- padding: 1px 5px;
862
- border-radius: 8px;
863
- line-height: 1.4;
864
- display: none;
865
- "
866
- ></span
867
- ></a>
868
- <a href="/tests">Tests</a>
869
- </nav>
870
- </header>
669
+ <!-- The shell: top bar and rail, the same on every page (shell.js). -->
670
+ <div id="den-shell" data-page="release" data-health-env="local"></div>
671
+ <script src="/shell.js"></script>
871
672
 
673
+ <!-- The page's one main landmark: everything after the shell. -->
674
+ <main id="den-content">
872
675
  <div class="controls">
873
- <span class="env-label">env:</span>
874
- <select id="rel-env" title="Environment"></select>
676
+ <label class="env-label" for="rel-env">deploy to:</label>
677
+ <select id="rel-env" title="Deploy target"></select>
875
678
  <button class="btn" id="refresh-btn" title="Re-check deployed state">
876
679
  ⟳ Refresh
877
680
  </button>
@@ -888,7 +691,7 @@
888
691
  >
889
692
  </div>
890
693
 
891
- <main>
694
+ <div class="page-main">
892
695
  <div class="table-pane">
893
696
  <table id="rel-table">
894
697
  <thead>
@@ -908,25 +711,9 @@
908
711
  </table>
909
712
  </div>
910
713
 
911
- <div class="pane-bar" id="output-bar" style="display: none">
912
- <span id="output-title">Deploy output</span>
913
- <span>
914
- <button onclick="window.__rel.clearOutput()">Clear</button>
915
- <button
916
- onclick="window.__rel.closeDeployLog()"
917
- title="Close the deploy log"
918
- >
919
- ✕ Close
920
- </button>
921
- </span>
922
- </div>
923
- <!-- Only shown when more than one deploy session exists; switches which
924
- one the single log pane below is attached to. -->
925
- <div id="session-chips" class="session-chips" style="display: none"></div>
926
- <div class="output-pane hidden" id="output-pane">
927
- <pre id="output"></pre>
928
- </div>
929
- </main>
714
+ <!-- Each deploy session is a tab in the shell's runs dock (R16):
715
+ selecting one attaches its log, its × closes the session. -->
716
+ </div>
930
717
 
931
718
  <!-- Generic modal -->
932
719
  <div class="overlay" id="overlay">
@@ -947,11 +734,13 @@
947
734
  initLogBadge,
948
735
  initHealth,
949
736
  refreshHealth,
737
+ initWorkspacePill,
950
738
  } from "/header.js";
951
739
 
952
- // The header's dots report LOCAL service health on every page, this one
953
- // included; #env-sel is pinned to "local" in the markup above. Changing
954
- // the deploy target must NOT re-point them — that was the bug where
740
+ // The header's dots report LOCAL service health here: #den-shell pins
741
+ // data-health-env="local" (shell.js). The env picker is the shell's
742
+ // view; #rel-env below is the deploy target. Changing the deploy
743
+ // target must NOT re-point the dots — that was the bug where
955
744
  // selecting sandbox made the header ask /api/health?env=sandbox and show
956
745
  // every system red while docker was perfectly healthy.
957
746
  const HEADER_HEALTH_ENV = "local";
@@ -1092,7 +881,11 @@
1092
881
  });
1093
882
 
1094
883
  renderHead();
1095
- await Promise.all([applyDenConfig(), initChangelog()]);
884
+ await Promise.all([
885
+ applyDenConfig(),
886
+ initChangelog(),
887
+ initWorkspacePill(),
888
+ ]);
1096
889
  initLogBadge();
1097
890
  initHealth();
1098
891
  loadStatus();
@@ -1251,7 +1044,7 @@
1251
1044
  const mainCell = r.mainShort
1252
1045
  ? r.deployedShort && r.deployedShort !== r.mainShort
1253
1046
  ? `${srcTag}<span class="sha">${esc(r.mainShort)}</span>`
1254
- : `${srcTag}<span class="sha" style="color:var(--green)">${esc(r.mainShort)}</span>`
1047
+ : `${srcTag}<span class="sha" style="color:var(--ok)">${esc(r.mainShort)}</span>`
1255
1048
  : '<span class="sha">—</span>';
1256
1049
  // The GitHub Environment checked, when it isn't the env's own name
1257
1050
  // (mapped CD environments, one row per tenant cell).
@@ -1595,14 +1388,12 @@
1595
1388
  });
1596
1389
  });
1597
1390
  try {
1598
- const updated = await fetch("/api/release/config", {
1599
- method: "PUT",
1600
- headers: { "Content-Type": "application/json" },
1601
- body: JSON.stringify({ repos }),
1602
- }).then((r) => {
1603
- if (!r.ok) throw new Error("save failed");
1604
- return r.json();
1605
- });
1391
+ // R26c: the server's message on a failure, not "save failed".
1392
+ const updated = await window.denShell.api(
1393
+ "PUT",
1394
+ "/api/release/config",
1395
+ { repos },
1396
+ );
1606
1397
  state.config = updated;
1607
1398
  closeModal();
1608
1399
  renderTable();
@@ -1616,46 +1407,42 @@
1616
1407
  // deploy decoupled from this page), then we attach to stream its log. The
1617
1408
  // session id is remembered so the log survives navigation / reload.
1618
1409
  async function startDeploy(params) {
1619
- showOutput(true);
1620
- clearOutput();
1621
1410
  const label =
1622
1411
  params.mode === "all"
1623
1412
  ? `all → ${state.env}`
1624
1413
  : `${params.repo} → ${state.env}`;
1625
- $("output-title").textContent = `Starting ${label}…`;
1414
+ // Until the server names the session, the request has its own tab.
1415
+ curRun = runs().start(REQUEST_RUN, `deploy ${label}`, {
1416
+ clear: true,
1417
+ doneLine: false,
1418
+ });
1626
1419
  appendLine(`▶ requesting deploy ${label}`, "cmd");
1627
1420
  setStatus(`starting ${label}…`);
1628
1421
 
1629
1422
  let session;
1630
1423
  try {
1631
- const res = await fetch("/api/release/deploy", {
1632
- method: "POST",
1633
- headers: { "Content-Type": "application/json" },
1634
- body: JSON.stringify({
1635
- env: state.env,
1636
- mode: params.mode,
1637
- repo: params.repo,
1638
- ref: params.ref,
1639
- confirm: params.confirm,
1640
- }),
1424
+ // R26c: a refusal (a 409 names the repos already in flight) is the
1425
+ // server's message in the run's output; a lost server is said too.
1426
+ session = await window.denShell.api("POST", "/api/release/deploy", {
1427
+ env: state.env,
1428
+ mode: params.mode,
1429
+ repo: params.repo,
1430
+ ref: params.ref,
1431
+ confirm: params.confirm,
1641
1432
  });
1642
- if (!res.ok) {
1643
- // 409 carries a JSON body naming the repos already in flight.
1644
- let text = await res.text().catch(() => "deploy request failed");
1645
- try {
1646
- const j = JSON.parse(text);
1647
- if (j?.error) text = j.error;
1648
- } catch {
1649
- /* not JSON — show as-is */
1650
- }
1651
- appendLine(text, "err");
1433
+ } catch (e) {
1434
+ if (e.denApi && !e.network) {
1435
+ appendLine(
1436
+ e.message + (e.requestId ? ` (request ${e.requestId})` : ""),
1437
+ "err",
1438
+ );
1439
+ runs().finish(REQUEST_RUN, { ok: false });
1652
1440
  setStatus("deploy not started");
1653
1441
  await refreshSessions();
1654
1442
  return;
1655
1443
  }
1656
- session = await res.json();
1657
- } catch (e) {
1658
1444
  appendLine("Failed to start deploy: " + e.message, "err");
1445
+ runs().finish(REQUEST_RUN, { ok: false });
1659
1446
  setStatus("deploy not started");
1660
1447
  await refreshSessions();
1661
1448
  return;
@@ -1665,6 +1452,7 @@
1665
1452
  LS_DEPLOY,
1666
1453
  JSON.stringify({ sessionId: session.id }),
1667
1454
  );
1455
+ runs().remove(REQUEST_RUN);
1668
1456
  // Show the new session immediately, then reconcile against the server.
1669
1457
  state.sessions = [
1670
1458
  session,
@@ -1693,10 +1481,12 @@
1693
1481
  state.deploySession = sessionId;
1694
1482
  state.deploying = running;
1695
1483
  clearRunsForSession(sessionId);
1696
- showOutput(true);
1697
- clearOutput(); // replay rebuilds the full log from scratch
1698
- $("output-title").textContent =
1699
- (running ? "Deploying " : "Deploy log — ") + (label || sessionId);
1484
+ // Its tab, cleared: the replay rebuilds the full log from scratch.
1485
+ curRun = runs().start(
1486
+ runKey(sessionId),
1487
+ `deploy ${label || sessionId}`,
1488
+ { ...sessionTabOpts(sessionId), clear: true, running },
1489
+ );
1700
1490
  setStatus(running ? "attaching to deploy…" : "deploy finished");
1701
1491
  // Button state comes from the full session list, not from whichever log
1702
1492
  // happens to be on screen — switching logs must not free a busy repo.
@@ -1736,6 +1526,7 @@
1736
1526
  `\n${msg.ok ? "✓" : "✗"} ${msg.summary}`,
1737
1527
  msg.ok ? "ok" : "err",
1738
1528
  );
1529
+ runs().finish(runKey(sessionId), { ok: !!msg.ok });
1739
1530
  finishDeploy();
1740
1531
  }
1741
1532
  };
@@ -1764,10 +1555,6 @@
1764
1555
  // server rather than assuming everything is now free.
1765
1556
  refreshSessions();
1766
1557
  setStatus("deploy finished — re-checking…");
1767
- const t = $("output-title");
1768
- if (t && t.textContent.startsWith("Deploying "))
1769
- t.textContent =
1770
- "Deploy log — " + t.textContent.slice("Deploying ".length);
1771
1558
  renderTable();
1772
1559
  setTimeout(() => loadStatus(true), 2500);
1773
1560
  }
@@ -1808,11 +1595,21 @@
1808
1595
  });
1809
1596
  }
1810
1597
 
1811
- // User closed the log: dismiss ONLY the session on screen. Other deploys
1812
- // keep running, so if one is left, switch the pane to it instead of
1813
- // hiding the log entirely.
1814
- async function closeDeployLog() {
1598
+ // User closed the log (its tab's ×): dismiss ONLY that session. Other
1599
+ // deploys keep running, so if the one on screen closes and one is left,
1600
+ // switch to it instead of hiding the log entirely.
1601
+ async function closeDeployLog(id = state.deploySession) {
1602
+ if (id && id !== state.deploySession) {
1603
+ fetch(`/api/release/deploy?session=${encodeURIComponent(id)}`, {
1604
+ method: "DELETE",
1605
+ }).catch(() => {});
1606
+ state.sessions = state.sessions.filter((s) => s.id !== id);
1607
+ runs().remove(runKey(id));
1608
+ recomputeDeployingRepos();
1609
+ return;
1610
+ }
1815
1611
  const closing = state.deploySession;
1612
+ if (closing) runs().remove(runKey(closing));
1816
1613
  if (closing) {
1817
1614
  fetch(`/api/release/deploy?session=${encodeURIComponent(closing)}`, {
1818
1615
  method: "DELETE",
@@ -1841,8 +1638,6 @@
1841
1638
 
1842
1639
  localStorage.removeItem(LS_DEPLOY);
1843
1640
  recomputeDeployingRepos();
1844
- showOutput(false);
1845
- clearOutput();
1846
1641
  setStatus("idle");
1847
1642
  renderTable();
1848
1643
  }
@@ -1925,7 +1720,7 @@
1925
1720
  }
1926
1721
  state.deployingRepos = live;
1927
1722
  syncButtons();
1928
- renderSessionChips();
1723
+ syncSessionTabs();
1929
1724
  }
1930
1725
 
1931
1726
  // ── Live CD run poll ──────────────────────────────────────────────────────
@@ -2052,60 +1847,51 @@
2052
1847
  recomputeDeployingRepos();
2053
1848
  }
2054
1849
 
2055
- // One log pane, N sessions: chips name each one and switch the pane.
2056
- function renderSessionChips() {
2057
- const bar = $("session-chips");
2058
- if (!bar) return;
2059
- if (state.sessions.length <= 1) {
2060
- bar.style.display = "none";
2061
- bar.innerHTML = "";
2062
- return;
2063
- }
2064
- bar.style.display = "";
2065
- bar.innerHTML = state.sessions
2066
- .map((s) => {
2067
- const cls = [
2068
- "session-chip",
2069
- s.id === state.deploySession ? "active" : "",
2070
- s.status === "running" ? "running" : s.ok ? "ok" : "fail",
2071
- ]
2072
- .filter(Boolean)
2073
- .join(" ");
2074
- return `<button class="${cls}" data-session="${esc(s.id)}" title="${esc(s.label)}">${
2075
- s.status === "running" ? "● " : ""
2076
- }${esc(s.label)}</button>`;
2077
- })
2078
- .join("");
2079
- bar.querySelectorAll(".session-chip").forEach((c) =>
2080
- c.addEventListener("click", () => {
2081
- const s = state.sessions.find((x) => x.id === c.dataset.session);
2082
- if (s && s.id !== state.deploySession) {
1850
+ // ── The runs dock (R16): one tab per deploy session ──────────────────────
1851
+ const runs = () => window.denShell.runs;
1852
+ const runKey = (id) => `deploy:${id}`;
1853
+ const REQUEST_RUN = "deploy:request";
1854
+ let curRun = null; // the tab appendLine writes to
1855
+ function sessionTabOpts(id) {
1856
+ return {
1857
+ doneLine: false,
1858
+ onClose: () => closeDeployLog(id),
1859
+ // Selecting a session's tab attaches its log (the server replays it).
1860
+ onSelect: () => {
1861
+ const s = state.sessions.find((x) => x.id === id);
1862
+ if (s && s.id !== state.deploySession)
2083
1863
  attachDeploy(s.id, { status: s.status, label: s.label });
1864
+ },
1865
+ };
2084
1866
  }
2085
- }),
2086
- );
1867
+ // N sessions, N tabs: each names its session and pulses while it runs.
1868
+ function syncSessionTabs() {
1869
+ const ids = new Set(state.sessions.map((s) => runKey(s.id)));
1870
+ for (const s of state.sessions)
1871
+ runs().add(runKey(s.id), `deploy ${s.label || s.id}`, {
1872
+ ...sessionTabOpts(s.id),
1873
+ running: s.status === "running",
1874
+ ...(s.status === "running" ? {} : { error: s.ok === false }),
1875
+ });
1876
+ for (const r of runs().list())
1877
+ if (
1878
+ r.id.startsWith("deploy:") &&
1879
+ r.id !== REQUEST_RUN &&
1880
+ !ids.has(r.id) &&
1881
+ r.id !== runKey(state.deploySession)
1882
+ )
1883
+ runs().remove(r.id);
2087
1884
  }
2088
1885
 
2089
1886
  // ── Output helpers ──────────────────────────────────────────────────────────
2090
- function showOutput(show) {
2091
- $("output-bar").style.display = show ? "" : "none";
2092
- $("output-pane").classList.toggle("hidden", !show);
2093
- }
2094
1887
  function appendLine(text, cls) {
2095
- const out = $("output");
2096
- const span = document.createElement("span");
2097
- if (cls) span.className = cls;
2098
- span.textContent = text + "\n";
2099
- out.appendChild(span);
2100
- out.scrollTop = out.scrollHeight;
2101
- }
2102
- function clearOutput() {
2103
- $("output").innerHTML = "";
1888
+ if (curRun) runs().append(curRun, text, cls);
2104
1889
  }
2105
1890
 
2106
- window.__rel = { clearOutput, closeDeployLog };
1891
+ window.__rel = { closeDeployLog };
2107
1892
 
2108
1893
  init();
2109
1894
  </script>
1895
+ </main>
2110
1896
  </body>
2111
1897
  </html>