@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.
Files changed (158) hide show
  1. package/CHANGELOG.md +1731 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +730 -6
  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 +12 -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/agents/default-pack/be-dev.md +20 -0
  22. package/src/shared/agents/default-pack/devops.md +14 -0
  23. package/src/shared/agents/default-pack/fe-dev.md +20 -0
  24. package/src/shared/agents/default-pack/orchestrator.md +16 -0
  25. package/src/shared/agents/default-pack/planner.md +17 -0
  26. package/src/shared/agents/default-pack/reviewer.md +14 -0
  27. package/src/shared/agents/default-pack/ux-designer.md +15 -0
  28. package/src/shared/agents/den-claude.ts +173 -0
  29. package/src/shared/agents/generic.ts +52 -0
  30. package/src/shared/agents/personas.ts +218 -0
  31. package/src/shared/agents/records.ts +118 -0
  32. package/src/shared/agents/runtime.ts +192 -0
  33. package/src/shared/agents/runtimes.ts +47 -0
  34. package/src/shared/auth/headless-login.ts +1 -1
  35. package/src/shared/auth/sudo-vault.ts +93 -31
  36. package/src/shared/auth/token-providers/index.ts +7 -3
  37. package/src/shared/brand.ts +7 -0
  38. package/src/shared/config-error.ts +33 -3
  39. package/src/shared/constants.ts +3 -24
  40. package/src/shared/den-config.ts +257 -5
  41. package/src/shared/edge/config.ts +147 -21
  42. package/src/shared/edge/exec.ts +31 -0
  43. package/src/shared/edge/included.ts +169 -0
  44. package/src/shared/edge/render.ts +69 -14
  45. package/src/shared/edge/runtime.ts +128 -121
  46. package/src/shared/edge/status.ts +68 -8
  47. package/src/shared/edge/tls.ts +328 -0
  48. package/src/shared/edge/types.ts +25 -9
  49. package/src/shared/engine.ts +23 -0
  50. package/src/shared/environment-manager/default-workspace.ts +16 -1
  51. package/src/shared/environment-manager/health.ts +73 -38
  52. package/src/shared/environment-manager/scope.ts +168 -0
  53. package/src/shared/environment-manager/service-runner.ts +789 -43
  54. package/src/shared/environment-manager/up-targets.ts +132 -0
  55. package/src/shared/environment-manager/workspaces.ts +19 -0
  56. package/src/shared/health/rollup.ts +48 -0
  57. package/src/shared/open-url.ts +21 -2
  58. package/src/shared/org-sources.ts +78 -59
  59. package/src/shared/org.ts +53 -5
  60. package/src/shared/plugins/manifest.ts +406 -0
  61. package/src/shared/plugins/store.ts +358 -0
  62. package/src/shared/process/legacy-adopt.ts +285 -0
  63. package/src/shared/process/process-group.ts +202 -0
  64. package/src/shared/process/spawn-streaming.ts +7 -0
  65. package/src/shared/secret-env.ts +70 -0
  66. package/src/shared/secrets/doppler.ts +1 -1
  67. package/src/shared/secrets/index.ts +279 -50
  68. package/src/shared/secrets/none.ts +56 -0
  69. package/src/shared/secrets/strategies.ts +202 -0
  70. package/src/shared/secrets/types.ts +1 -1
  71. package/src/shared/sessions/client.ts +277 -0
  72. package/src/shared/sessions/den-pty.ts +97 -0
  73. package/src/shared/sessions/env.ts +79 -0
  74. package/src/shared/sessions/host.ts +667 -0
  75. package/src/shared/sessions/names.ts +85 -0
  76. package/src/shared/sessions/paths.ts +27 -0
  77. package/src/shared/sessions/protocol.ts +72 -0
  78. package/src/shared/sessions/pty.ts +141 -0
  79. package/src/shared/sessions/ring-buffer.ts +160 -0
  80. package/src/shared/sessions/types.ts +81 -0
  81. package/src/shared/smoke/git-remote.ts +40 -0
  82. package/src/shared/stacks/manifest-commands.ts +1 -1
  83. package/src/shared/stacks/namespace.ts +178 -0
  84. package/src/shared/stacks/owners.ts +303 -0
  85. package/src/shared/stacks/render.ts +8 -3
  86. package/src/shared/stacks/runtime.ts +100 -16
  87. package/src/shared/stacks/status.ts +63 -12
  88. package/src/shared/strategies/registry.ts +89 -0
  89. package/src/shared/theme/contract.ts +538 -0
  90. package/src/shared/types/index.ts +12 -3
  91. package/src/ui/cli/agents-command.ts +812 -0
  92. package/src/ui/cli/create.ts +24 -7
  93. package/src/ui/cli/doctor-command.ts +260 -0
  94. package/src/ui/cli/edge-command.ts +56 -10
  95. package/src/ui/cli/env-probe.ts +266 -0
  96. package/src/ui/cli/flag-parser.ts +18 -5
  97. package/src/ui/cli/health-display.ts +25 -0
  98. package/src/ui/cli/index.ts +580 -60
  99. package/src/ui/cli/notice.ts +20 -0
  100. package/src/ui/cli/plugin-command.ts +849 -0
  101. package/src/ui/cli/sessions-command.ts +718 -0
  102. package/src/ui/cli/stack-command.ts +63 -2
  103. package/src/ui/cli/test-runner.ts +11 -9
  104. package/src/ui/cli/theme-command.ts +136 -0
  105. package/src/ui/cli/web-command.ts +47 -23
  106. package/src/ui/mcp/api.ts +28 -2
  107. package/src/ui/mcp/scope.ts +145 -20
  108. package/src/ui/mcp/server.ts +27 -2
  109. package/src/ui/mcp/tools.ts +94 -12
  110. package/src/ui/server/appearance-prefs.ts +95 -0
  111. package/src/ui/server/audit.ts +35 -0
  112. package/src/ui/server/daemon-reaper.ts +29 -1
  113. package/src/ui/server/engine-stamp.ts +83 -0
  114. package/src/ui/server/github-service.ts +23 -44
  115. package/src/ui/server/graph-page.ts +20 -0
  116. package/src/ui/server/graph-prefs.ts +44 -1
  117. package/src/ui/server/http-errors.ts +293 -0
  118. package/src/ui/server/http-helpers.ts +22 -3
  119. package/src/ui/server/index.ts +539 -51
  120. package/src/ui/server/public/appearance.js +531 -0
  121. package/src/ui/server/public/edge-page.js +248 -0
  122. package/src/ui/server/public/edge.html +251 -0
  123. package/src/ui/server/public/environments.html +65 -83
  124. package/src/ui/server/public/error-page.js +247 -0
  125. package/src/ui/server/public/error.html +260 -0
  126. package/src/ui/server/public/graph-model.js +332 -14
  127. package/src/ui/server/public/graph.css +141 -47
  128. package/src/ui/server/public/graph.js +223 -44
  129. package/src/ui/server/public/header.js +396 -600
  130. package/src/ui/server/public/health-fit.js +248 -0
  131. package/src/ui/server/public/health-rollup.js +128 -0
  132. package/src/ui/server/public/i18n/es.js +287 -0
  133. package/src/ui/server/public/i18n/ja.js +276 -0
  134. package/src/ui/server/public/index.html +139 -438
  135. package/src/ui/server/public/local-dev.html +3468 -4044
  136. package/src/ui/server/public/logs.html +71 -208
  137. package/src/ui/server/public/panel.js +14 -2
  138. package/src/ui/server/public/release.html +175 -389
  139. package/src/ui/server/public/services.html +596 -770
  140. package/src/ui/server/public/settings-page.js +759 -0
  141. package/src/ui/server/public/settings-registry.js +521 -0
  142. package/src/ui/server/public/settings.html +590 -0
  143. package/src/ui/server/public/shell.js +3869 -0
  144. package/src/ui/server/public/topbar.js +1413 -0
  145. package/src/ui/server/public/version-label.js +12 -0
  146. package/src/ui/server/public/workspace-picker.js +74 -34
  147. package/src/ui/server/readonly-routes.ts +37 -3
  148. package/src/ui/server/services-list.ts +94 -17
  149. package/src/ui/server/settings-distribution.ts +74 -0
  150. package/src/ui/server/shell-prefs.ts +192 -0
  151. package/src/ui/server/theme-routes.ts +292 -0
  152. package/src/ui/server/theme-runtime.ts +407 -0
  153. package/src/ui/server/ui-assets.ts +16 -1
  154. package/src/ui/server/ui-language.ts +50 -0
  155. package/src/ui/server/workspace-routes.ts +48 -18
  156. package/themes/den/den-theme.json +72 -0
  157. package/themes/den/favicon.ico +0 -0
  158. 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__ · Environments</title>
7
30
  <style>
8
31
  *,
@@ -12,65 +35,17 @@
12
35
  margin: 0;
13
36
  padding: 0;
14
37
  }
15
- :root {
16
- --bg: #0d1117;
17
- --surface: #161b22;
18
- --border: #30363d;
19
- --text: #c9d1d9;
20
- --muted: #8b949e;
21
- --accent: #58a6ff;
22
- --green: #3fb950;
23
- --red: #f85149;
24
- --yellow: #d29922;
25
- --mono: "SF Mono", "Cascadia Code", "Fira Code", "Menlo", monospace;
26
- }
27
38
  html,
28
39
  body {
29
40
  min-height: 100%;
30
41
  background: var(--bg);
31
42
  color: var(--text);
32
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
43
+ font-family: var(--sans);
33
44
  font-size: 13px;
34
45
  }
35
- header {
36
- display: flex;
37
- align-items: center;
38
- gap: 10px;
39
- padding: 7px 14px;
40
- background: var(--surface);
41
- border-bottom: 1px solid var(--border);
42
- flex-wrap: wrap;
43
- }
44
- header h1 {
45
- font-family: var(--mono);
46
- font-size: 14px;
47
- font-weight: 600;
48
- letter-spacing: -0.02em;
49
- }
50
46
  .h-spacer {
51
47
  flex: 1;
52
48
  }
53
- nav {
54
- display: flex;
55
- gap: 2px;
56
- flex-wrap: wrap;
57
- }
58
- nav a {
59
- color: var(--muted);
60
- text-decoration: none;
61
- padding: 3px 10px;
62
- border-radius: 4px;
63
- font-size: 12px;
64
- font-weight: 500;
65
- }
66
- nav a:hover {
67
- color: var(--text);
68
- background: var(--border);
69
- }
70
- nav a.active {
71
- color: var(--text);
72
- background: var(--bg);
73
- }
74
49
  .controls {
75
50
  display: flex;
76
51
  align-items: center;
@@ -96,7 +71,7 @@
96
71
  button:hover {
97
72
  border-color: var(--accent);
98
73
  }
99
- main {
74
+ .page-main {
100
75
  padding: 12px 14px;
101
76
  overflow-x: auto;
102
77
  }
@@ -142,7 +117,7 @@
142
117
  text-decoration: none;
143
118
  }
144
119
  .cell a:hover {
145
- color: var(--accent);
120
+ color: var(--accent-text);
146
121
  text-decoration: underline;
147
122
  }
148
123
  .cell .ms {
@@ -162,13 +137,13 @@
162
137
  display: inline-block;
163
138
  }
164
139
  .dot.ok {
165
- background: var(--green);
140
+ background: var(--ok);
166
141
  }
167
142
  .dot.err {
168
- background: var(--red);
143
+ background: var(--bad);
169
144
  }
170
145
  .dot.slow {
171
- background: var(--yellow);
146
+ background: var(--warn);
172
147
  }
173
148
  .empty {
174
149
  color: var(--muted);
@@ -198,13 +173,13 @@
198
173
  color: var(--muted);
199
174
  }
200
175
  .rel .ok {
201
- color: var(--green);
176
+ color: var(--ok);
202
177
  }
203
178
  .rel .warn {
204
- color: var(--yellow);
179
+ color: var(--warn);
205
180
  }
206
181
  .rel .bad {
207
- color: var(--red);
182
+ color: var(--bad);
208
183
  }
209
184
  a.open {
210
185
  color: var(--muted);
@@ -215,7 +190,7 @@
215
190
  }
216
191
  a.open:hover,
217
192
  a.open:focus-visible {
218
- color: var(--accent);
193
+ color: var(--accent-text);
219
194
  background: var(--border);
220
195
  }
221
196
  select {
@@ -229,26 +204,12 @@
229
204
  </style>
230
205
  </head>
231
206
  <body>
232
- <header>
233
- <img
234
- src="/favicon.ico"
235
- width="16"
236
- height="16"
237
- alt=""
238
- style="flex-shrink: 0"
239
- />
240
- <h1>__DEN_TITLE__</h1>
241
- <div class="h-spacer"></div>
242
- <nav>
243
- <a href="/">Local Dev</a>
244
- <a href="/services">Services</a>
245
- <a href="/environments" class="active">Environments</a>
246
- <a href="/release">Releases</a>
247
- <a href="/logs">Logs</a>
248
- <a href="/tests">Tests</a>
249
- </nav>
250
- </header>
207
+ <!-- The shell: top bar and rail, the same on every page (shell.js). -->
208
+ <div id="den-shell" data-page="environments"></div>
209
+ <script src="/shell.js"></script>
251
210
 
211
+ <!-- The page's one main landmark: everything after the shell. -->
212
+ <main id="den-content">
252
213
  <div class="controls">
253
214
  <button id="refresh">Re-check</button>
254
215
  <label class="note"
@@ -265,7 +226,7 @@
265
226
  >
266
227
  </div>
267
228
 
268
- <main>
229
+ <div class="page-main">
269
230
  <table>
270
231
  <thead id="head"></thead>
271
232
  <tbody id="body">
@@ -274,9 +235,24 @@
274
235
  </tr>
275
236
  </tbody>
276
237
  </table>
277
- </main>
238
+ </div>
278
239
 
279
240
  <script type="module">
241
+ // The shell's behaviour, as on every page (header.js also mounts the
242
+ // appearance controls).
243
+ import {
244
+ applyDenConfig,
245
+ initChangelog,
246
+ initHealth,
247
+ initLogBadge,
248
+ initWorkspacePill,
249
+ } from "/header.js";
250
+ void Promise.all([
251
+ initHealth(),
252
+ initWorkspacePill(),
253
+ applyDenConfig(),
254
+ initChangelog(),
255
+ ]).then(initLogBadge);
280
256
  const SLOW_MS = 1500;
281
257
  const POLL_MS = 60_000;
282
258
  const esc = (s) =>
@@ -349,6 +325,7 @@
349
325
  status.textContent = "checking…";
350
326
  const envs = await fetch("/api/environments")
351
327
  .then((r) => r.json())
328
+ .then((v) => (Array.isArray(v) ? v : ["local"]))
352
329
  .catch(() => ["local"]);
353
330
  const [results, orgs, relCfg] = await Promise.all([
354
331
  Promise.all(
@@ -360,9 +337,12 @@
360
337
  .catch(() => []),
361
338
  ),
362
339
  ),
363
- fetch("/api/orgs")
340
+ // The org of every repo comes from the catalog (/api/orgs?repos=1):
341
+ // /api/health rows never carry one, so reading r.org put every
342
+ // service under "no org" (0.3.0-dev.14).
343
+ fetch("/api/orgs?repos=1")
364
344
  .then((r) => r.json())
365
- .catch(() => []),
345
+ .catch(() => ({ orgs: [], repos: {} })),
366
346
  fetch("/api/release/config")
367
347
  .then((r) => r.json())
368
348
  .catch(() => ({ environments: [] })),
@@ -384,8 +364,9 @@
384
364
  }),
385
365
  );
386
366
  const orgInfo = Object.fromEntries(
387
- (Array.isArray(orgs) ? orgs : []).map((o) => [o.id, o]),
367
+ (Array.isArray(orgs?.orgs) ? orgs.orgs : []).map((o) => [o.id, o]),
388
368
  );
369
+ const orgOfRepo = orgs?.repos ?? {};
389
370
  const filterSel = document.getElementById("org-filter");
390
371
  const saved = (() => {
391
372
  try {
@@ -414,7 +395,7 @@
414
395
  repo: r.repo,
415
396
  service: r.service,
416
397
  label: r.label,
417
- org: r.org || "",
398
+ org: orgOfRepo[r.repo] || "",
418
399
  isEntrypoint: r.isEntrypoint,
419
400
  idx: r.idx ?? 1e9,
420
401
  byEnv: {},
@@ -492,5 +473,6 @@
492
473
  if (!document.hidden) load();
493
474
  }, POLL_MS);
494
475
  </script>
476
+ </main>
495
477
  </body>
496
478
  </html>
@@ -0,0 +1,247 @@
1
+ /**
2
+ * The error page (R26a, 0.3.0-dev.38): what a failed page request gets
3
+ * inside the shell frame (rail, top bar, status chip). The server fills
4
+ * #den-error (a JSON script) with what failed; this draws it.
5
+ *
6
+ * 404 "Page not found" the path asked for; "Did you mean …" from the
7
+ * command palette's own matcher (topbar.js
8
+ * rankCommands) over the shell's pages; links to
9
+ * Workspaces and Search (⌘K); the list of pages;
10
+ * for /plugins/<id>/…, that the plugin isn't
11
+ * installed, by name
12
+ * 422 "Config is invalid" each validation problem (file, key path,
13
+ * message), `<cli> validate`, "Reload once fixed",
14
+ * and links to Logs and Settings (config-free)
15
+ * 500 "Something went wrong" the method and route template (never query
16
+ * values), the time, the request id; Retry and
17
+ * Copy details. No stack: the server log has it
18
+ * under the same request id.
19
+ *
20
+ * On an engine mismatch (MD-2) the shell's banner leads the page with the
21
+ * restart message, and a 404 says the page may be new in that version.
22
+ *
23
+ * suggestPage and detailsText are pure, for tests.
24
+ */
25
+ import { rankCommands } from "/topbar.js";
26
+
27
+ // Its words are the shell's catalog (shell.js MESSAGES, R19: the err.*
28
+ // keys), so the page is in the page's language. "Copy details" is a
29
+ // report: it stays in English, like the server log it points at.
30
+
31
+ function esc(v) {
32
+ return String(v ?? "").replace(
33
+ /[&<>"']/g,
34
+ (c) =>
35
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
36
+ c
37
+ ],
38
+ );
39
+ }
40
+
41
+ /**
42
+ * The closest page to `pathname` by the palette's ranking: the whole slug
43
+ * first, then each of its words, then shorter prefixes of it (two letters
44
+ * at least), so a near-miss and a stray word both find something. `items`
45
+ * are palette rows ({ label, keywords, href }). Null when nothing matches.
46
+ */
47
+ export function suggestPage(pathname, items, rank = rankCommands) {
48
+ const slug = String(pathname || "")
49
+ .split("/")
50
+ .filter(Boolean)
51
+ .pop()
52
+ ?.replace(/\.html?$/i, "")
53
+ .toLowerCase();
54
+ if (!slug) return null;
55
+ const tries = [slug, ...slug.split(/[-_.\s]+/).filter((w) => w.length > 1)];
56
+ for (let n = slug.length - 1; n >= 2; n--) tries.push(slug.slice(0, n));
57
+ for (const q of tries) {
58
+ const hit = rank(items, q, { limit: 1 })[0];
59
+ if (hit) return hit;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /** "Copy details": what a report needs, nothing more (no stack). */
65
+ export function detailsText(d, page = "") {
66
+ return [
67
+ `Den ${d.version ?? "unknown"}`,
68
+ `Status ${d.status}`,
69
+ d.status === 500
70
+ ? `Route ${d.method ?? "GET"} ${d.route ?? ""}`
71
+ : `Path ${d.path ?? ""}`,
72
+ `Request ${d.requestId}`,
73
+ `Time ${d.time}`,
74
+ ...(page ? [`Page ${page}`] : []),
75
+ ...(d.engine ? [d.engine.message] : []),
76
+ ].join("\n");
77
+ }
78
+
79
+ function pageItems(sh) {
80
+ const st = sh.getState?.() ?? {};
81
+ const rows = (sh.orderedNav?.() ?? sh.nav?.() ?? [])
82
+ .filter((n) => !n.when || n.when(st))
83
+ .map((n) => ({
84
+ label: sh.t(n.label),
85
+ keywords: [n.id, ...(n.keywords ?? [])],
86
+ href: n.href,
87
+ }));
88
+ rows.push({
89
+ label: sh.t("n.settings"),
90
+ keywords: ["settings", "preferences"],
91
+ href: "/settings",
92
+ });
93
+ return rows;
94
+ }
95
+
96
+ function readData() {
97
+ try {
98
+ return JSON.parse(document.getElementById("den-error")?.textContent ?? "");
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ async function copy(text, btn, t) {
105
+ try {
106
+ await navigator.clipboard.writeText(text);
107
+ } catch {
108
+ const ta = document.createElement("textarea");
109
+ ta.value = text;
110
+ document.body.appendChild(ta);
111
+ ta.select();
112
+ try {
113
+ document.execCommand("copy");
114
+ } catch {
115
+ /* nothing more to try */
116
+ }
117
+ ta.remove();
118
+ }
119
+ const label = btn.textContent;
120
+ btn.textContent = t("err.copied");
121
+ setTimeout(() => (btn.textContent = label), 1500);
122
+ }
123
+
124
+ export function mountErrorPage(root) {
125
+ const sh = window.denShell;
126
+ const d = readData();
127
+ if (!root || !sh || !d) return;
128
+ const t = sh.t;
129
+ const header = (title, sub) =>
130
+ `<div class="ph"><div><h1>${esc(title)}</h1>` +
131
+ `<div class="sub">${sub}</div></div></div>`;
132
+
133
+ if (d.status === 404) {
134
+ document.title = document.title.replace(/Error$/, t("err.404.title"));
135
+ const items = pageItems(sh);
136
+ const hit = suggestPage(d.path, items);
137
+ const lines = [];
138
+ if (d.plugin) {
139
+ const name = esc(d.plugin.name || d.plugin.id);
140
+ lines.push(
141
+ `<p class="lead">${t(
142
+ d.plugin.installed
143
+ ? "err.404.pluginNoPage"
144
+ : d.plugin.locked
145
+ ? "err.404.pluginLocked"
146
+ : "err.404.plugin",
147
+ { name: `<b>${name}</b>` },
148
+ )}</p>`,
149
+ );
150
+ } else lines.push(`<p class="lead">${esc(t("err.404.lead"))}</p>`);
151
+ if (d.engine)
152
+ lines.push(
153
+ `<p>${esc(t("err.404.stale", { onDisk: d.engine.onDisk ?? "?", loaded: d.engine.loaded ?? "?" }))}</p>`,
154
+ );
155
+ if (hit)
156
+ lines.push(
157
+ `<p class="den-err-mean">${t("err.404.mean", {
158
+ page: `<a href="${esc(hit.href)}">${esc(hit.label)}</a>`,
159
+ })}</p>`,
160
+ );
161
+ root.innerHTML =
162
+ header(
163
+ t("err.404.title"),
164
+ // The path sits where the language puts it ("Nothing at {path}").
165
+ esc(t("err.404.sub", { path: "\u0000" })).replace(
166
+ "\u0000",
167
+ `<span class="mono">${esc(d.path)}</span>`,
168
+ ),
169
+ ) +
170
+ `<div class="empty">${lines.join("")}` +
171
+ `<div class="acts"><a class="btn primary" href="/local-dev">${esc(t("err.404.workspaces"))}</a>` +
172
+ `<button type="button" class="btn" id="den-err-search">${esc(t("err.404.search"))} <kbd>⌘K</kbd></button></div>` +
173
+ `<h2 class="sec">${esc(t("err.404.pages"))}</h2><ul class="pages">` +
174
+ items
175
+ .map((i) => `<li><a href="${esc(i.href)}">${esc(i.label)}</a></li>`)
176
+ .join("") +
177
+ `</ul></div>`;
178
+ root.querySelector("#den-err-search")?.addEventListener("click", () => {
179
+ document.getElementById("den-search")?.click();
180
+ });
181
+ return;
182
+ }
183
+
184
+ if (d.status === 422) {
185
+ // R26a's 422 (0.3.0-dev.40): each problem (file, key path, message),
186
+ // the command that shows them in a terminal, and "Reload once fixed".
187
+ // Settings and Logs are config-free, so the rail reaches the fix.
188
+ document.title = document.title.replace(/Error$/, t("err.422.title"));
189
+ const issues =
190
+ Array.isArray(d.issues) && d.issues.length
191
+ ? d.issues
192
+ : [{ file: "", key: "", message: d.message ?? "" }];
193
+ const cmd = `${d.cli || "den"} validate`;
194
+ root.innerHTML =
195
+ header(t("err.422.title"), esc(t("err.422.sub"))) +
196
+ `<div class="empty wide"><p class="lead">${esc(t("err.422.lead"))}</p>` +
197
+ `<div class="tablewrap"><table class="svc" id="den-err-issues"><thead><tr>` +
198
+ `<th>${esc(t("err.422.file"))}</th><th>${esc(t("err.422.key"))}</th><th>${esc(t("err.422.problem"))}</th>` +
199
+ `</tr></thead><tbody>` +
200
+ issues
201
+ .map(
202
+ (i) =>
203
+ `<tr><td class="mono">${esc(i.file)}</td><td class="mono"><b>${esc(i.key)}</b></td><td>${esc(i.message)}</td></tr>`,
204
+ )
205
+ .join("") +
206
+ `</tbody></table></div>` +
207
+ `<p>${esc(t("err.422.cmd"))}</p>` +
208
+ `<pre class="code" id="den-err-cmd">${esc(cmd)}</pre>` +
209
+ `<dl class="facts">` +
210
+ `<dt>${esc(t("err.500.id"))}</dt><dd class="mono" id="den-err-id">${esc(d.requestId)}</dd>` +
211
+ `</dl><div class="acts">` +
212
+ `<button type="button" class="btn primary" id="den-err-retry">${esc(t("err.422.reload"))}</button>` +
213
+ `<a class="btn" href="/logs">${esc(t("err.422.logs"))}</a>` +
214
+ `<a class="btn" href="/settings">${esc(t("err.422.settings"))}</a>` +
215
+ `</div></div>`;
216
+ root
217
+ .querySelector("#den-err-retry")
218
+ ?.addEventListener("click", () => location.reload());
219
+ return;
220
+ }
221
+
222
+ document.title = document.title.replace(/Error$/, t("err.500.title"));
223
+ const what = `${d.method ?? "GET"} ${d.route ?? ""}`;
224
+ const when = new Date(d.time);
225
+ root.innerHTML =
226
+ header(
227
+ t("err.500.title"),
228
+ `<span class="mono">${esc(t("err.500.sub", { what }))}</span>`,
229
+ ) +
230
+ `<div class="empty"><p class="lead">${esc(t("err.500.lead"))}</p>` +
231
+ `<dl class="facts">` +
232
+ `<dt>${esc(t("err.500.what"))}</dt><dd class="mono">${esc(what)}</dd>` +
233
+ `<dt>${esc(t("err.500.time"))}</dt><dd><time datetime="${esc(d.time)}">${esc(isNaN(when) ? d.time : when.toLocaleString(sh.lang?.() && sh.lang() !== "en" ? sh.lang() : undefined))}</time></dd>` +
234
+ `<dt>${esc(t("err.500.id"))}</dt><dd class="mono" id="den-err-id">${esc(d.requestId)}</dd>` +
235
+ `<dt>${esc(t("err.500.version"))}</dt><dd class="mono">${esc(d.version ?? "unknown")}</dd>` +
236
+ `</dl><div class="acts">` +
237
+ `<button type="button" class="btn primary" id="den-err-retry">${esc(t("err.retry"))}</button>` +
238
+ `<button type="button" class="btn" id="den-err-copy">${esc(t("err.copy"))}</button>` +
239
+ `</div></div>`;
240
+ root
241
+ .querySelector("#den-err-retry")
242
+ ?.addEventListener("click", () => location.reload());
243
+ const copyBtn = root.querySelector("#den-err-copy");
244
+ copyBtn?.addEventListener("click", () =>
245
+ copy(detailsText(d, location.pathname), copyBtn, t),
246
+ );
247
+ }