@monster-orange/den 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +1547 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +507 -4
  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/auth/headless-login.ts +1 -1
  22. package/src/shared/auth/sudo-vault.ts +93 -31
  23. package/src/shared/auth/token-providers/index.ts +7 -3
  24. package/src/shared/brand.ts +7 -0
  25. package/src/shared/config-error.ts +33 -3
  26. package/src/shared/constants.ts +3 -24
  27. package/src/shared/den-config.ts +141 -5
  28. package/src/shared/edge/config.ts +147 -21
  29. package/src/shared/edge/exec.ts +31 -0
  30. package/src/shared/edge/included.ts +169 -0
  31. package/src/shared/edge/render.ts +69 -14
  32. package/src/shared/edge/runtime.ts +128 -121
  33. package/src/shared/edge/status.ts +68 -8
  34. package/src/shared/edge/tls.ts +328 -0
  35. package/src/shared/edge/types.ts +25 -9
  36. package/src/shared/environment-manager/default-workspace.ts +16 -1
  37. package/src/shared/environment-manager/health.ts +73 -38
  38. package/src/shared/environment-manager/scope.ts +168 -0
  39. package/src/shared/environment-manager/service-runner.ts +789 -43
  40. package/src/shared/environment-manager/up-targets.ts +132 -0
  41. package/src/shared/health/rollup.ts +48 -0
  42. package/src/shared/open-url.ts +21 -2
  43. package/src/shared/org-sources.ts +78 -59
  44. package/src/shared/org.ts +53 -5
  45. package/src/shared/plugins/manifest.ts +406 -0
  46. package/src/shared/plugins/store.ts +358 -0
  47. package/src/shared/process/legacy-adopt.ts +285 -0
  48. package/src/shared/process/process-group.ts +202 -0
  49. package/src/shared/process/spawn-streaming.ts +7 -0
  50. package/src/shared/secrets/doppler.ts +1 -1
  51. package/src/shared/secrets/index.ts +279 -50
  52. package/src/shared/secrets/none.ts +56 -0
  53. package/src/shared/secrets/strategies.ts +202 -0
  54. package/src/shared/secrets/types.ts +1 -1
  55. package/src/shared/smoke/git-remote.ts +40 -0
  56. package/src/shared/stacks/manifest-commands.ts +1 -1
  57. package/src/shared/stacks/namespace.ts +178 -0
  58. package/src/shared/stacks/owners.ts +303 -0
  59. package/src/shared/stacks/render.ts +8 -3
  60. package/src/shared/stacks/runtime.ts +100 -16
  61. package/src/shared/stacks/status.ts +63 -12
  62. package/src/shared/strategies/registry.ts +89 -0
  63. package/src/shared/theme/contract.ts +538 -0
  64. package/src/shared/types/index.ts +12 -3
  65. package/src/ui/cli/create.ts +24 -7
  66. package/src/ui/cli/doctor-command.ts +131 -0
  67. package/src/ui/cli/edge-command.ts +56 -10
  68. package/src/ui/cli/flag-parser.ts +7 -2
  69. package/src/ui/cli/health-display.ts +25 -0
  70. package/src/ui/cli/index.ts +548 -60
  71. package/src/ui/cli/notice.ts +20 -0
  72. package/src/ui/cli/plugin-command.ts +849 -0
  73. package/src/ui/cli/stack-command.ts +63 -2
  74. package/src/ui/cli/test-runner.ts +11 -9
  75. package/src/ui/cli/theme-command.ts +136 -0
  76. package/src/ui/cli/web-command.ts +47 -23
  77. package/src/ui/mcp/api.ts +28 -2
  78. package/src/ui/mcp/scope.ts +22 -1
  79. package/src/ui/mcp/tools.ts +50 -7
  80. package/src/ui/server/appearance-prefs.ts +95 -0
  81. package/src/ui/server/daemon-reaper.ts +29 -1
  82. package/src/ui/server/engine-stamp.ts +83 -0
  83. package/src/ui/server/github-service.ts +23 -44
  84. package/src/ui/server/graph-page.ts +20 -0
  85. package/src/ui/server/graph-prefs.ts +44 -1
  86. package/src/ui/server/http-errors.ts +293 -0
  87. package/src/ui/server/http-helpers.ts +22 -3
  88. package/src/ui/server/index.ts +532 -51
  89. package/src/ui/server/public/appearance.js +531 -0
  90. package/src/ui/server/public/edge-page.js +248 -0
  91. package/src/ui/server/public/edge.html +251 -0
  92. package/src/ui/server/public/environments.html +65 -83
  93. package/src/ui/server/public/error-page.js +247 -0
  94. package/src/ui/server/public/error.html +260 -0
  95. package/src/ui/server/public/graph-model.js +332 -14
  96. package/src/ui/server/public/graph.css +141 -47
  97. package/src/ui/server/public/graph.js +223 -44
  98. package/src/ui/server/public/header.js +396 -600
  99. package/src/ui/server/public/health-fit.js +248 -0
  100. package/src/ui/server/public/health-rollup.js +128 -0
  101. package/src/ui/server/public/i18n/es.js +287 -0
  102. package/src/ui/server/public/i18n/ja.js +276 -0
  103. package/src/ui/server/public/index.html +139 -438
  104. package/src/ui/server/public/local-dev.html +3468 -4044
  105. package/src/ui/server/public/logs.html +71 -208
  106. package/src/ui/server/public/panel.js +14 -2
  107. package/src/ui/server/public/release.html +175 -389
  108. package/src/ui/server/public/services.html +596 -770
  109. package/src/ui/server/public/settings-page.js +759 -0
  110. package/src/ui/server/public/settings-registry.js +521 -0
  111. package/src/ui/server/public/settings.html +590 -0
  112. package/src/ui/server/public/shell.js +3869 -0
  113. package/src/ui/server/public/topbar.js +1413 -0
  114. package/src/ui/server/public/version-label.js +12 -0
  115. package/src/ui/server/public/workspace-picker.js +74 -34
  116. package/src/ui/server/readonly-routes.ts +12 -1
  117. package/src/ui/server/services-list.ts +94 -17
  118. package/src/ui/server/settings-distribution.ts +74 -0
  119. package/src/ui/server/shell-prefs.ts +192 -0
  120. package/src/ui/server/theme-routes.ts +292 -0
  121. package/src/ui/server/theme-runtime.ts +407 -0
  122. package/src/ui/server/ui-assets.ts +16 -1
  123. package/src/ui/server/ui-language.ts +50 -0
  124. package/src/ui/server/workspace-routes.ts +45 -3
  125. package/themes/den/den-theme.json +72 -0
  126. package/themes/den/favicon.ico +0 -0
  127. package/themes/den/mark.svg +6 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * The Edge page (0.3.0-dev.30, PL-SPEC §10): a built-in shell page in the
3
+ * Ship group, in the rail only when /api/edge reports the edge configured
4
+ * (shell.js asks /api/edge?probe=1). Read-only: what `<cli> edge status`
5
+ * and `<cli> edge routes` show, from /api/edge:
6
+ *
7
+ * the container and its ports · the TLS strategy (edge.tls: none,
8
+ * mkcert or files; with its CA, and whether it was set, mapped from the
9
+ * legacy keys, or implicit) · CA trust · the cert and its names · the
10
+ * domains · Den's own routes · the routers in the included dynamic files
11
+ * (`edge include`), each with its source file: Traefik serves them, Den
12
+ * doesn't generate them (a consumer's review) · the routers the stacks
13
+ * wrote (`stack-*.yml`, 0.3.0-dev.32), source "stack" with the file.
14
+ *
15
+ * CA trust reads "not needed: fronted" when edge.publish is false (no host
16
+ * ports: a proxy in front owns browser trust, the BOOT-1 rule), neutral,
17
+ * never a red "not trusted" (0.3.0-dev.32). Under tls `files` the cert
18
+ * names are the ones the included certs should cover (0.3.0-dev.32).
19
+ *
20
+ * No trust/untrust or up/down here: those stay CLI commands.
21
+ *
22
+ * The page header is the prototype's .ph (R20). edgeView(status) is pure
23
+ * (HTML from /api/edge's JSON), so tests need no server.
24
+ */
25
+
26
+ function esc(v) {
27
+ return String(v ?? "").replace(
28
+ /[&<>"']/g,
29
+ (c) =>
30
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
31
+ c
32
+ ],
33
+ );
34
+ }
35
+
36
+ /** A link for a URL the edge serves (http/https only; anything else is text). */
37
+ function link(url, text) {
38
+ return /^https?:\/\//.test(String(url))
39
+ ? `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(text ?? url)}</a>`
40
+ : esc(text ?? url);
41
+ }
42
+
43
+ /** Where the TLS strategy came from, in a line. */
44
+ export function tlsNote(tls) {
45
+ if (!tls) return "";
46
+ if (tls.from === "implicit" || tls.implicit)
47
+ return "implicit: edge.tls is not set (mkcert with a local CA, deprecated)";
48
+ if (tls.from === "legacy")
49
+ return "from the legacy keys (caroot / issue_cert), mapped";
50
+ return "set in edge.tls";
51
+ }
52
+
53
+ function tile(k, v, s, bad) {
54
+ return (
55
+ `<div class="tile${bad ? " bad" : ""}"><div class="k">${esc(k)}</div>` +
56
+ `<div class="v">${esc(v)}</div><div class="s">${s}</div></div>`
57
+ );
58
+ }
59
+
60
+ function table(cols, rows, empty) {
61
+ if (!rows.length) return `<p class="none">${esc(empty)}</p>`;
62
+ return (
63
+ `<div class="tablewrap"><table class="svc"><thead><tr>` +
64
+ cols.map((c) => `<th scope="col">${esc(c)}</th>`).join("") +
65
+ `</tr></thead><tbody>` +
66
+ rows
67
+ .map((r) => `<tr>${r.map((c) => `<td>${c}</td>`).join("")}</tr>`)
68
+ .join("") +
69
+ `</tbody></table></div>`
70
+ );
71
+ }
72
+
73
+ /**
74
+ * The cert-names card's heading. Under tls `files` Den issues nothing: the
75
+ * names are the ones the included certs should cover (a consumer's nit,
76
+ * 0.3.0-dev.32). mkcert (and none): Den's own cert, issued or planned.
77
+ */
78
+ export function certHeading(strategy, issued) {
79
+ if (strategy === "files") return "Names the included certs should cover";
80
+ return `Certificate names${issued ? "" : " (planned)"}`;
81
+ }
82
+
83
+ /** The page's content (below the header) from /api/edge's JSON. */
84
+ export function edgeView(st) {
85
+ if (!st || st.configured === false)
86
+ return (
87
+ `<div class="empty"><b>No edge configured.</b> ` +
88
+ `${esc(st?.error ?? "")}</div>`
89
+ );
90
+ const tls = st.tls ?? { strategy: "none" };
91
+ const none = tls.strategy === "none";
92
+ const ports = st.ports
93
+ ? none
94
+ ? `:${st.ports.http} (http only)`
95
+ : `:${st.ports.http} / :${st.ports.https}`
96
+ : "not published";
97
+ // Fronted: no host ports (edge.publish: false). The proxy in front
98
+ // terminates TLS for browsers, so this machine's CA trust is moot.
99
+ const fronted = !st.ports;
100
+ const trust = none
101
+ ? "—"
102
+ : fronted
103
+ ? "not needed: fronted"
104
+ : st.caTrusted === null || st.caTrusted === undefined
105
+ ? "unknown"
106
+ : st.caTrusted
107
+ ? "trusted"
108
+ : "not trusted";
109
+ const included = Array.isArray(st.included) ? st.included : [];
110
+ const stacks = Array.isArray(st.stacks) ? st.stacks : [];
111
+ const errors = [
112
+ ...(Array.isArray(st.includedErrors) ? st.includedErrors : []),
113
+ ...(Array.isArray(st.stackErrors) ? st.stackErrors : []),
114
+ ];
115
+ const certNames = Array.isArray(st.certNames) ? st.certNames : [];
116
+ const tiles = [
117
+ tile(
118
+ "Container",
119
+ st.up ? "up" : "down",
120
+ `<span class="mono">${esc(st.container)}</span>: ${esc(st.state)}`,
121
+ !st.up,
122
+ ),
123
+ tile(
124
+ "Ports",
125
+ ports,
126
+ `network <span class="mono">${esc(st.network)}</span>`,
127
+ false,
128
+ ),
129
+ tile("TLS", tls.strategy, esc(tlsNote(tls)), false),
130
+ tile(
131
+ "CA trust",
132
+ trust,
133
+ none
134
+ ? "http only: no CA"
135
+ : fronted
136
+ ? "a proxy in front owns browser trust"
137
+ : tls.ca
138
+ ? `<span class="mono">${esc(tls.ca)}</span>`
139
+ : "no CA set",
140
+ st.caTrusted === false && !fronted,
141
+ ),
142
+ tile(
143
+ "Certificate",
144
+ none ? "—" : st.certIssued ? "issued" : "not issued",
145
+ none
146
+ ? "http only: no cert"
147
+ : tls.strategy === "files"
148
+ ? "from the included config"
149
+ : `${certNames.length} name${certNames.length === 1 ? "" : "s"}`,
150
+ false,
151
+ ),
152
+ ];
153
+ const scheme = none ? "http://" : "https://";
154
+ const routes = (st.routes ?? []).map((r) => [
155
+ `<span class="mono">${esc(r.name)}</span>`,
156
+ link(r.url),
157
+ `<span class="mono">${esc(r.upstream)}</span>`,
158
+ `<span class="src">den</span>`,
159
+ ]);
160
+ // Like Den's own rows: the URL with its scheme (https:// when the router
161
+ // has TLS or is on websecure), and the upstream URL when its file
162
+ // defines the service's loadBalancer, else the service's name
163
+ // (0.3.0-dev.33, a consumer's dogfood finding).
164
+ const routerRow = (r, src) => {
165
+ const scheme = (r.secure ?? r.tls) ? "https://" : "http://";
166
+ return [
167
+ `<span class="mono">${esc(r.name)}</span>`,
168
+ r.hosts?.length
169
+ ? r.hosts.map((h) => link(scheme + h)).join("<br>")
170
+ : `<span class="mono rule">${esc(r.rule)}</span>`,
171
+ `<span class="mono">${esc(r.upstream || r.service)}</span>`,
172
+ src,
173
+ ];
174
+ };
175
+ const inc = included.map((r) =>
176
+ routerRow(
177
+ r,
178
+ `<span class="src" title="included dynamic file">${esc(r.file)}</span>`,
179
+ ),
180
+ );
181
+ // The stacks' own route files (stack up writes them): source "stack",
182
+ // marked with the file.
183
+ const stk = stacks.map((r) =>
184
+ routerRow(
185
+ r,
186
+ `<span class="src" title="written by stack up">stack</span> ` +
187
+ `<span class="mono sub">${esc(r.file)}</span>`,
188
+ ),
189
+ );
190
+ return (
191
+ `<div class="sum">${tiles.join("")}</div>` +
192
+ `<section aria-labelledby="edge-h-routes"><h2 class="sec" id="edge-h-routes">Routes</h2>` +
193
+ table(
194
+ ["Name", "URL", "Upstream", "Source"],
195
+ [...routes, ...inc, ...stk],
196
+ "No routes.",
197
+ ) +
198
+ (errors.length
199
+ ? `<ul class="errs">` +
200
+ errors
201
+ .map(
202
+ (e) =>
203
+ `<li><span class="mono">${esc(e.file)}</span>: not read (${esc(e.error)})</li>`,
204
+ )
205
+ .join("") +
206
+ `</ul>`
207
+ : "") +
208
+ `<p class="note">Den's own routes, then the routers in the included dynamic files (edge include), each with its source file, then the routers the stacks wrote (stack).</p>` +
209
+ `</section>` +
210
+ `<section aria-labelledby="edge-h-domains"><h2 class="sec" id="edge-h-domains">Domains</h2>` +
211
+ `<ul class="names">` +
212
+ (st.domains ?? [])
213
+ .map(
214
+ (d) =>
215
+ `<li>${link(scheme + d, d)} <span class="mono sub">+ *.${esc(d)}</span></li>`,
216
+ )
217
+ .join("") +
218
+ `</ul></section>` +
219
+ (none
220
+ ? ""
221
+ : `<section aria-labelledby="edge-h-cert"><h2 class="sec" id="edge-h-cert">${esc(certHeading(tls.strategy, st.certIssued))}</h2>` +
222
+ (certNames.length
223
+ ? `<ul class="names">${certNames.map((n) => `<li class="mono">${esc(n)}</li>`).join("")}</ul>`
224
+ : `<p class="none">None: the included config holds the certs.</p>`) +
225
+ `</section>`)
226
+ );
227
+ }
228
+
229
+ /** The header's sub line: org / container · state. */
230
+ export function edgeSub(st) {
231
+ if (!st || st.configured === false) return "Not configured";
232
+ return `${st.org} / ${st.container} · ${st.state}`;
233
+ }
234
+
235
+ /** Fetch /api/edge and draw the page into `root` (the header's sub line too). */
236
+ export async function mountEdge(root, sub, fetchFn = fetch) {
237
+ let st;
238
+ try {
239
+ const r = await fetchFn("/api/edge");
240
+ st = await r.json();
241
+ } catch (e) {
242
+ st = { configured: false, error: (e && e.message) || "unreachable" };
243
+ }
244
+ if (sub) sub.textContent = edgeSub(st);
245
+ root.innerHTML = edgeView(st);
246
+ root.removeAttribute("aria-busy");
247
+ return st;
248
+ }
@@ -0,0 +1,251 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <script>
7
+ // Mode before first paint: yours (the server fills it in below),
8
+ // else the system's, else the theme's default, else dark.
9
+ (function () {
10
+ var m = "@@DEN_MODE@@";
11
+ if (m !== "dark" && m !== "light") {
12
+ var q = function (s) {
13
+ return (
14
+ window.matchMedia &&
15
+ matchMedia("(prefers-color-scheme: " + s + ")").matches
16
+ );
17
+ };
18
+ m = q("light")
19
+ ? "light"
20
+ : q("dark")
21
+ ? "dark"
22
+ : "@@DEN_DEFAULT_MODE@@";
23
+ }
24
+ document.documentElement.dataset.mode =
25
+ m === "light" ? "light" : "dark";
26
+ })();
27
+ </script>
28
+ <link rel="stylesheet" href="/theme.css" id="theme-css" />
29
+ <title>__DEN_TITLE__ · Edge</title>
30
+ <style>
31
+ /* The Edge page (PL-SPEC §10): the prototype's (den-shell.html) page
32
+ header (.ph, R20), summary tiles (.sum .tile), section headings
33
+ (h2.sec) and table (.tablewrap table.svc), ported as they are; --r
34
+ is --radius, --r-btn is --radius-button. Read-only. */
35
+ *,
36
+ *::before,
37
+ *::after {
38
+ box-sizing: border-box;
39
+ }
40
+ html,
41
+ body {
42
+ height: 100%;
43
+ margin: 0;
44
+ background: var(--bg);
45
+ color: var(--text);
46
+ }
47
+ body {
48
+ display: flex;
49
+ flex-direction: column;
50
+ overflow: hidden;
51
+ }
52
+ /* .main */
53
+ html body.den-has-shell > #den-content {
54
+ overflow: auto;
55
+ min-width: 0;
56
+ padding: 20px 24px 32px 24px;
57
+ font: 14px/1.45 var(--sans);
58
+ color: var(--text);
59
+ display: block;
60
+ }
61
+ #den-content :focus-visible {
62
+ outline: 2px solid var(--accent);
63
+ outline-offset: 2px;
64
+ }
65
+ a {
66
+ color: var(--accent-text);
67
+ }
68
+ /* .ph: the page header */
69
+ .ph {
70
+ display: flex;
71
+ align-items: flex-end;
72
+ gap: 16px;
73
+ flex-wrap: wrap;
74
+ margin-bottom: 16px;
75
+ }
76
+ .ph h1 {
77
+ font: 600 22px/1.2 var(--display);
78
+ margin: 0;
79
+ text-wrap: balance;
80
+ }
81
+ .ph .sub {
82
+ color: var(--muted);
83
+ font-size: 13px;
84
+ margin-top: 4px;
85
+ }
86
+ /* .sum, .tile */
87
+ .sum {
88
+ display: grid;
89
+ grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
90
+ gap: 10px;
91
+ margin-bottom: 22px;
92
+ }
93
+ .tile {
94
+ background: var(--surface);
95
+ border: 1px solid var(--border);
96
+ border-radius: var(--radius);
97
+ padding: 12px 14px;
98
+ min-width: 0;
99
+ }
100
+ .tile .k {
101
+ font: 11px var(--mono);
102
+ text-transform: uppercase;
103
+ letter-spacing: 0.07em;
104
+ color: var(--faint);
105
+ }
106
+ .tile .v {
107
+ font: 600 22px var(--display);
108
+ font-variant-numeric: tabular-nums;
109
+ margin-top: 2px;
110
+ }
111
+ .tile .s {
112
+ color: var(--muted);
113
+ font-size: 12.5px;
114
+ overflow-wrap: anywhere;
115
+ }
116
+ .tile.bad {
117
+ border-color: color-mix(in srgb, var(--bad) 45%, var(--border));
118
+ }
119
+ .tile.bad .v {
120
+ color: var(--bad);
121
+ }
122
+ section {
123
+ margin-bottom: 22px;
124
+ }
125
+ h2.sec {
126
+ font: 600 13px var(--mono);
127
+ text-transform: uppercase;
128
+ letter-spacing: 0.07em;
129
+ color: var(--muted);
130
+ margin: 0 0 10px;
131
+ }
132
+ /* .tablewrap, table.svc */
133
+ .tablewrap {
134
+ overflow-x: auto;
135
+ border: 1px solid var(--border);
136
+ border-radius: var(--radius);
137
+ background: var(--surface);
138
+ }
139
+ table.svc {
140
+ width: 100%;
141
+ border-collapse: collapse;
142
+ font-size: 13.5px;
143
+ }
144
+ table.svc th {
145
+ text-align: left;
146
+ font: 11px var(--mono);
147
+ text-transform: uppercase;
148
+ letter-spacing: 0.07em;
149
+ color: var(--faint);
150
+ padding: 8px 12px;
151
+ border-bottom: 1px solid var(--border);
152
+ font-weight: 400;
153
+ }
154
+ table.svc td {
155
+ padding: 7px 12px;
156
+ border-bottom: 1px solid var(--border);
157
+ white-space: nowrap;
158
+ vertical-align: top;
159
+ }
160
+ table.svc tr:last-child td {
161
+ border-bottom: 0;
162
+ }
163
+ .mono {
164
+ font-family: var(--mono);
165
+ font-size: 12.5px;
166
+ }
167
+ .rule {
168
+ white-space: normal;
169
+ overflow-wrap: anywhere;
170
+ color: var(--muted);
171
+ }
172
+ /* The source tag: like the rail's distribution tag (.tagx). */
173
+ .src {
174
+ font: 11px var(--mono);
175
+ color: var(--faint);
176
+ border: 1px dashed var(--border-strong);
177
+ border-radius: 4px;
178
+ padding: 0 4px;
179
+ }
180
+ .names {
181
+ list-style: none;
182
+ margin: 0;
183
+ padding: 0;
184
+ display: grid;
185
+ gap: 4px;
186
+ }
187
+ .names .sub {
188
+ color: var(--faint);
189
+ }
190
+ .note,
191
+ .none {
192
+ color: var(--muted);
193
+ font-size: 12.5px;
194
+ margin: 8px 0 0;
195
+ }
196
+ .errs {
197
+ margin: 8px 0 0;
198
+ padding-left: 18px;
199
+ color: var(--warn);
200
+ font-size: 12.5px;
201
+ }
202
+ .empty {
203
+ border: 1px dashed var(--border-strong);
204
+ border-radius: var(--radius);
205
+ padding: 28px;
206
+ color: var(--muted);
207
+ max-width: 640px;
208
+ }
209
+ .empty b {
210
+ color: var(--text);
211
+ }
212
+ </style>
213
+ </head>
214
+ <body>
215
+ <!-- The shell: top bar and rail, the same on every page (shell.js). -->
216
+ <div id="den-shell" data-page="edge"></div>
217
+ <script src="/shell.js"></script>
218
+
219
+ <!-- The page's one main landmark: everything after the shell. -->
220
+ <main id="den-content">
221
+ <div class="ph">
222
+ <div>
223
+ <h1>Edge</h1>
224
+ <div class="sub" id="edge-sub">Reading the edge…</div>
225
+ </div>
226
+ </div>
227
+ <div id="edge-body" aria-busy="true" aria-live="polite"></div>
228
+
229
+ <script type="module">
230
+ import {
231
+ applyDenConfig,
232
+ initChangelog,
233
+ initHealth,
234
+ initLogBadge,
235
+ initWorkspacePill,
236
+ } from "/header.js";
237
+ import { mountEdge } from "/edge-page.js";
238
+ void Promise.all([
239
+ initHealth(),
240
+ initWorkspacePill(),
241
+ applyDenConfig(),
242
+ initChangelog(),
243
+ ]).then(initLogBadge);
244
+ void mountEdge(
245
+ document.getElementById("edge-body"),
246
+ document.getElementById("edge-sub"),
247
+ );
248
+ </script>
249
+ </main>
250
+ </body>
251
+ </html>