@monster-orange/den 0.0.0 → 0.2.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 (255) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +68 -0
  5. package/docs/distributions.md +499 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +101 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +403 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +124 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +757 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/agent-actor.ts +77 -0
  91. package/src/shared/applications.ts +138 -0
  92. package/src/shared/auth/api-token.ts +416 -0
  93. package/src/shared/auth/auth0-helper.ts +284 -0
  94. package/src/shared/auth/headless-login.ts +265 -0
  95. package/src/shared/auth/jwt-helper.ts +186 -0
  96. package/src/shared/auth/sudo-vault.ts +69 -0
  97. package/src/shared/auth/test-accounts.ts +98 -0
  98. package/src/shared/auth/token-injection.ts +184 -0
  99. package/src/shared/auth/token-providers/auth0.ts +80 -0
  100. package/src/shared/auth/token-providers/index.ts +182 -0
  101. package/src/shared/auth/token-refresh.ts +273 -0
  102. package/src/shared/banner.ts +15 -0
  103. package/src/shared/brand.ts +79 -0
  104. package/src/shared/cli-tabs.ts +131 -0
  105. package/src/shared/constants.ts +128 -0
  106. package/src/shared/den-config.ts +688 -0
  107. package/src/shared/dev-metrics/emit.ts +354 -0
  108. package/src/shared/edge/config.ts +229 -0
  109. package/src/shared/edge/render.ts +221 -0
  110. package/src/shared/edge/runtime.ts +268 -0
  111. package/src/shared/edge/status.ts +74 -0
  112. package/src/shared/edge/types.ts +62 -0
  113. package/src/shared/engine.ts +67 -0
  114. package/src/shared/environment/aws-profiles.ts +49 -0
  115. package/src/shared/environment/env-selector.ts +161 -0
  116. package/src/shared/environment/playwright-env.ts +82 -0
  117. package/src/shared/environment-manager/catalog.ts +77 -0
  118. package/src/shared/environment-manager/clone-guard.ts +163 -0
  119. package/src/shared/environment-manager/dag.ts +286 -0
  120. package/src/shared/environment-manager/default-workspace.ts +182 -0
  121. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  122. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  123. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  124. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  125. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  126. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  127. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  128. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  129. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  130. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  131. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  132. package/src/shared/environment-manager/derivation/text.ts +40 -0
  133. package/src/shared/environment-manager/derivation/types.ts +69 -0
  134. package/src/shared/environment-manager/health.ts +635 -0
  135. package/src/shared/environment-manager/index.ts +6 -0
  136. package/src/shared/environment-manager/load-applications.ts +108 -0
  137. package/src/shared/environment-manager/log-tail.ts +213 -0
  138. package/src/shared/environment-manager/make-targets.ts +78 -0
  139. package/src/shared/environment-manager/opctl.ts +335 -0
  140. package/src/shared/environment-manager/readiness.ts +93 -0
  141. package/src/shared/environment-manager/register-local.ts +72 -0
  142. package/src/shared/environment-manager/secrets-check.ts +260 -0
  143. package/src/shared/environment-manager/service-container.ts +47 -0
  144. package/src/shared/environment-manager/service-env.ts +110 -0
  145. package/src/shared/environment-manager/service-runner.ts +1603 -0
  146. package/src/shared/environment-manager/sync-targets.ts +66 -0
  147. package/src/shared/environment-manager/up-plan.ts +326 -0
  148. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  149. package/src/shared/environment-manager/workspaces.ts +860 -0
  150. package/src/shared/manifest/deps.ts +27 -0
  151. package/src/shared/manifest/merge-override.ts +75 -0
  152. package/src/shared/manifest/migrate-manifest.ts +101 -0
  153. package/src/shared/manifest/placement.ts +321 -0
  154. package/src/shared/manifest/read-manifest.ts +278 -0
  155. package/src/shared/manifest/schema.ts +313 -0
  156. package/src/shared/manifest/service-links.ts +93 -0
  157. package/src/shared/manifest/validate-layers.ts +229 -0
  158. package/src/shared/manifest/validate-manifest.ts +200 -0
  159. package/src/shared/manifest/workspace-manifests.ts +92 -0
  160. package/src/shared/open-url.ts +122 -0
  161. package/src/shared/org.ts +44 -0
  162. package/src/shared/paths.ts +190 -0
  163. package/src/shared/perf/optional-fields.ts +105 -0
  164. package/src/shared/perf/report-normalizer.ts +404 -0
  165. package/src/shared/perf/report-paths.ts +19 -0
  166. package/src/shared/perf/report-store.ts +110 -0
  167. package/src/shared/perf/report-types.ts +70 -0
  168. package/src/shared/perf/test-paths.ts +25 -0
  169. package/src/shared/process/log-rotation.ts +147 -0
  170. package/src/shared/process/spawn-streaming.ts +229 -0
  171. package/src/shared/product.ts +92 -0
  172. package/src/shared/profile.ts +141 -0
  173. package/src/shared/redact.ts +107 -0
  174. package/src/shared/repo-kind.ts +32 -0
  175. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  176. package/src/shared/reporting/slack-reporter.ts +340 -0
  177. package/src/shared/results/result-reader.ts +266 -0
  178. package/src/shared/results/result-writer.ts +168 -0
  179. package/src/shared/secrets/doppler.ts +126 -0
  180. package/src/shared/secrets/env.ts +48 -0
  181. package/src/shared/secrets/index.ts +197 -0
  182. package/src/shared/secrets/keys.ts +33 -0
  183. package/src/shared/secrets/onepassword.ts +75 -0
  184. package/src/shared/secrets/types.ts +52 -0
  185. package/src/shared/service-url.ts +119 -0
  186. package/src/shared/smoke/branch-resolver.ts +141 -0
  187. package/src/shared/smoke/git-remote.ts +70 -0
  188. package/src/shared/smoke/repo-cloner.ts +151 -0
  189. package/src/shared/stacks/config.ts +137 -0
  190. package/src/shared/stacks/manifest-commands.ts +118 -0
  191. package/src/shared/stacks/render.ts +253 -0
  192. package/src/shared/stacks/runtime.ts +334 -0
  193. package/src/shared/stacks/status.ts +99 -0
  194. package/src/shared/stacks/types.ts +53 -0
  195. package/src/shared/terminal/tabs.ts +212 -0
  196. package/src/shared/test-discovery/module-scanner.ts +569 -0
  197. package/src/shared/types/index.ts +701 -0
  198. package/src/shared/ui-marker.ts +19 -0
  199. package/src/ui/cli/applications-command.ts +148 -0
  200. package/src/ui/cli/config-command.ts +68 -0
  201. package/src/ui/cli/create-main.ts +9 -0
  202. package/src/ui/cli/create.ts +360 -0
  203. package/src/ui/cli/edge-command.ts +81 -0
  204. package/src/ui/cli/flag-parser.ts +246 -0
  205. package/src/ui/cli/graph-command.ts +570 -0
  206. package/src/ui/cli/health-display.ts +63 -0
  207. package/src/ui/cli/index.ts +3742 -0
  208. package/src/ui/cli/init.ts +855 -0
  209. package/src/ui/cli/invocation-target.ts +175 -0
  210. package/src/ui/cli/logs-command.ts +104 -0
  211. package/src/ui/cli/mcp-main.ts +13 -0
  212. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  213. package/src/ui/cli/module-selection.ts +31 -0
  214. package/src/ui/cli/prompts.ts +767 -0
  215. package/src/ui/cli/results-viewer.ts +436 -0
  216. package/src/ui/cli/secrets-preflight.ts +91 -0
  217. package/src/ui/cli/stack-command.ts +117 -0
  218. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  219. package/src/ui/cli/test-runner.ts +1319 -0
  220. package/src/ui/cli/web-command.ts +1073 -0
  221. package/src/ui/mcp/api.ts +284 -0
  222. package/src/ui/mcp/guards.ts +141 -0
  223. package/src/ui/mcp/server.ts +147 -0
  224. package/src/ui/mcp/tools.ts +565 -0
  225. package/src/ui/server/audit.ts +71 -0
  226. package/src/ui/server/changelog-parser.ts +105 -0
  227. package/src/ui/server/daemon-reaper.ts +129 -0
  228. package/src/ui/server/editor-manager.ts +301 -0
  229. package/src/ui/server/github-service.ts +223 -0
  230. package/src/ui/server/graph-prefs.ts +89 -0
  231. package/src/ui/server/health-row.ts +15 -0
  232. package/src/ui/server/http-helpers.ts +98 -0
  233. package/src/ui/server/index.ts +3350 -0
  234. package/src/ui/server/log-history.ts +453 -0
  235. package/src/ui/server/perf-report.ts +42 -0
  236. package/src/ui/server/public/environments.html +496 -0
  237. package/src/ui/server/public/favicon.ico +0 -0
  238. package/src/ui/server/public/graph-model.js +416 -0
  239. package/src/ui/server/public/graph.css +976 -0
  240. package/src/ui/server/public/graph.js +986 -0
  241. package/src/ui/server/public/header.js +1373 -0
  242. package/src/ui/server/public/index.html +2595 -0
  243. package/src/ui/server/public/local-dev.html +5344 -0
  244. package/src/ui/server/public/logs.html +1496 -0
  245. package/src/ui/server/public/panel.js +787 -0
  246. package/src/ui/server/public/perf-report.js +359 -0
  247. package/src/ui/server/public/release.html +2111 -0
  248. package/src/ui/server/public/services.html +1103 -0
  249. package/src/ui/server/public/workspace-picker.js +69 -0
  250. package/src/ui/server/readonly-routes.ts +200 -0
  251. package/src/ui/server/release-deploy-session.ts +246 -0
  252. package/src/ui/server/release-service.ts +1125 -0
  253. package/src/ui/server/sudo-store.ts +111 -0
  254. package/src/ui/server/ui-assets.ts +73 -0
  255. package/src/ui/server/workspace-routes.ts +2185 -0
@@ -0,0 +1,787 @@
1
+ /**
2
+ * The graph's details panel. Everything in it comes from three registries,
3
+ * so a new panel (tests, security, releases, a distribution's own) is one
4
+ * entry in each:
5
+ *
6
+ * ACTIONS { id, label, for: ["service"|"repo"], when(x), tier: "common"|"commands"|"tools"|"danger",
7
+ * primaryWhen?(x), disabled?(x) → reason string, confirm?: true, run(x) }
8
+ * TABS { id, label, for, when?(x), order }
9
+ * SECTIONS { id, tab, title?, for, when?(x), order, render(x) → HTMLElement|string }
10
+ *
11
+ * Kinds: "service" (x = a model service) and "repo" (x = { repo, services }).
12
+ * Tiering (graph-model.js resolveActions): one primary, up to two common
13
+ * actions beside it, the rest in More grouped as Manifest commands / Tools /
14
+ * Careful; Careful actions confirm inline on a second click.
15
+ *
16
+ * graph.js binds the context (model, hooks, view helpers) with bindPanel().
17
+ */
18
+ import {
19
+ commandTier,
20
+ connections,
21
+ entryOf,
22
+ fmtMs,
23
+ healthLine,
24
+ isLocal,
25
+ orgOf,
26
+ placeGlyph,
27
+ placementText,
28
+ remoteWhy,
29
+ repoPlacement,
30
+ resolveActions,
31
+ status,
32
+ statusWord,
33
+ } from "/graph-model.js";
34
+
35
+ /** ctx: { model(), st, hooks, toast(msg), selectTab(id), focusOn(sel), refresh() } */
36
+ let C = null;
37
+ export function bindPanel(ctx) {
38
+ C = ctx;
39
+ }
40
+
41
+ export const esc = (s) =>
42
+ String(s ?? "").replace(
43
+ /[&<>"']/g,
44
+ (c) =>
45
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
46
+ c
47
+ ],
48
+ );
49
+
50
+ const has = (s, c) => s.commands.includes(c);
51
+ /** Runs here: local, has a runtime, and is on disk. */
52
+ const runsHere = (s) => isLocal(s) && s.runs !== "none" && s.cloned;
53
+ const repoEntry = (r) => entryOf(C.model(), r.repo);
54
+ const anyHere = (r) => r.services.some(runsHere);
55
+ const LIFECYCLE = ["up", "down", "restart"];
56
+
57
+ export const ACTIONS = [
58
+ // ── service ──
59
+ {
60
+ id: "start",
61
+ label: "Start",
62
+ for: ["service"],
63
+ tier: "common",
64
+ when: (s) => runsHere(s) && has(s, "up") && s.health.ok !== true,
65
+ primaryWhen: (s) => s.health.ok === false,
66
+ run: (s) => C.hooks.runCmd(s, "up"),
67
+ },
68
+ {
69
+ id: "start-remote",
70
+ label: "Start",
71
+ for: ["service"],
72
+ tier: "common",
73
+ when: (s) => !isLocal(s),
74
+ disabled: remoteWhy,
75
+ run: () => {},
76
+ },
77
+ {
78
+ id: "open",
79
+ label: "Open ↗",
80
+ for: ["service"],
81
+ tier: "common",
82
+ when: (s) => !!(s.open || s.target?.url),
83
+ primaryWhen: (s) => s.health.ok !== false || !isLocal(s),
84
+ run: (s) => window.open(s.open || s.target.url, "_blank", "noopener"),
85
+ },
86
+ {
87
+ id: "logs",
88
+ label: "Logs",
89
+ for: ["service"],
90
+ tier: "common",
91
+ when: runsHere,
92
+ primaryWhen: () => true,
93
+ run: () => C.selectTab("logs"),
94
+ },
95
+ {
96
+ id: "restart",
97
+ label: "Restart",
98
+ for: ["service"],
99
+ tier: "common",
100
+ when: (s) => runsHere(s) && has(s, "restart"),
101
+ run: (s) => C.hooks.runCmd(s, "restart"),
102
+ },
103
+ {
104
+ id: "up-deps",
105
+ label: "Start with dependencies",
106
+ for: ["service"],
107
+ tier: "common",
108
+ when: (s) => runsHere(s) && has(s, "up"),
109
+ run: (s) => C.hooks.upWithDeps(s.repo),
110
+ },
111
+ // ── repo (system-level: runs against the repo's entrypoint, as the CLI does) ──
112
+ {
113
+ id: "repo-start",
114
+ label: "Start",
115
+ for: ["repo"],
116
+ tier: "common",
117
+ when: (r) => runsHere(repoEntry(r)) && has(repoEntry(r), "up"),
118
+ primaryWhen: (r) =>
119
+ r.services.some((s) => runsHere(s) && s.health.ok === false),
120
+ run: (r) => C.hooks.runCmd(repoEntry(r), "up"),
121
+ },
122
+ {
123
+ id: "repo-logs",
124
+ label: "Logs",
125
+ for: ["repo"],
126
+ tier: "common",
127
+ when: anyHere,
128
+ primaryWhen: () => true,
129
+ run: (r) => C.focusOn({ id: repoEntry(r).id, tab: "logs" }),
130
+ },
131
+ {
132
+ id: "repo-restart",
133
+ label: "Restart",
134
+ for: ["repo"],
135
+ tier: "common",
136
+ when: (r) => runsHere(repoEntry(r)) && has(repoEntry(r), "restart"),
137
+ run: (r) => C.hooks.runCmd(repoEntry(r), "restart"),
138
+ },
139
+ {
140
+ id: "repo-up-deps",
141
+ label: "Start with dependencies",
142
+ for: ["repo"],
143
+ tier: "common",
144
+ when: anyHere,
145
+ run: (r) => C.hooks.upWithDeps(r.repo),
146
+ },
147
+ // ── tools ──
148
+ {
149
+ id: "terminal",
150
+ label: "Terminal in repo",
151
+ for: ["service", "repo"],
152
+ tier: "tools",
153
+ when: (x) => {
154
+ const s = x.services ? x.services[0] : x;
155
+ return isLocal(s) && s.cloned && !!s.path;
156
+ },
157
+ run: (x) => C.hooks.openTerminal((x.services ? x.services[0] : x).path),
158
+ },
159
+ {
160
+ id: "editor",
161
+ label: "Open in editor",
162
+ for: ["service", "repo"],
163
+ tier: "tools",
164
+ when: (x) => {
165
+ const s = x.services ? x.services[0] : x;
166
+ return isLocal(s) && s.cloned && !!s.path;
167
+ },
168
+ run: (x) => C.hooks.openEditor((x.services ? x.services[0] : x).path),
169
+ },
170
+ {
171
+ id: "logs-page",
172
+ label: "Open in Logs",
173
+ for: ["service"],
174
+ tier: "tools",
175
+ when: runsHere,
176
+ run: (s) => C.hooks.openLogsPage(s),
177
+ },
178
+ {
179
+ id: "output",
180
+ label: "Show output pane",
181
+ for: ["service", "repo"],
182
+ tier: "tools",
183
+ when: (x) => (x.services ? anyHere(x) : runsHere(x)),
184
+ run: (x) => C.hooks.showOutput(x.services ? repoEntry(x) : x),
185
+ },
186
+ {
187
+ id: "recheck",
188
+ label: "Re-check health",
189
+ for: ["service", "repo"],
190
+ tier: "tools",
191
+ when: () => true,
192
+ run: (x) => C.recheck(x.services ? x.services : [x]),
193
+ },
194
+ {
195
+ id: "autostart",
196
+ label: "Auto-start",
197
+ for: ["service", "repo"],
198
+ tier: "tools",
199
+ when: (x) => isLocal(x.services ? x.services[0] : x),
200
+ labelFor: (x) =>
201
+ (x.services ? x.services[0] : x).autostart
202
+ ? "Auto-start: on (turn off)"
203
+ : "Auto-start: off (turn on)",
204
+ run: (x) => {
205
+ const s = x.services ? x.services[0] : x;
206
+ C.hooks.toggleAutostart(s.repo, !s.autostart);
207
+ },
208
+ },
209
+ {
210
+ id: "mode",
211
+ label: "Switch mode",
212
+ for: ["service", "repo"],
213
+ tier: "tools",
214
+ when: (x) => isLocal(x.services ? x.services[0] : x),
215
+ labelFor: (x) =>
216
+ (x.services ? x.services[0] : x).mode === "managed"
217
+ ? "Use my local checkout"
218
+ : "Use a managed clone",
219
+ run: (x) => {
220
+ const s = x.services ? x.services[0] : x;
221
+ C.hooks.switchMode(
222
+ s.repo,
223
+ s.mode === "managed" ? "dev-managed" : "managed",
224
+ );
225
+ },
226
+ },
227
+ {
228
+ id: "sync",
229
+ label: "Sync",
230
+ for: ["service", "repo"],
231
+ tier: "tools",
232
+ when: (x) => {
233
+ const s = x.services ? x.services[0] : x;
234
+ return isLocal(s) && s.mode === "managed";
235
+ },
236
+ labelFor: (x) =>
237
+ (x.services ? x.services : [x]).some((s) => !s.cloned)
238
+ ? "Clone"
239
+ : "Sync (down, pull, setup)",
240
+ run: (x) => C.hooks.sync(x.repo),
241
+ },
242
+ {
243
+ id: "copy",
244
+ label: "Copy address",
245
+ for: ["service"],
246
+ tier: "tools",
247
+ when: () => true,
248
+ run: (s) => {
249
+ navigator.clipboard?.writeText(s.id).then(
250
+ () => C.toast("Copied " + s.id),
251
+ () => {},
252
+ );
253
+ },
254
+ },
255
+ // ── careful ──
256
+ {
257
+ id: "stop",
258
+ label: "Stop",
259
+ for: ["service"],
260
+ tier: "danger",
261
+ confirm: true,
262
+ when: (s) => runsHere(s) && has(s, "down"),
263
+ run: (s) => C.hooks.runCmd(s, "down"),
264
+ },
265
+ {
266
+ id: "down-deps",
267
+ label: "Stop with dependencies",
268
+ for: ["service"],
269
+ tier: "danger",
270
+ confirm: true,
271
+ when: (s) => runsHere(s) && has(s, "down"),
272
+ run: (s) => C.hooks.downWithDeps(s.repo),
273
+ },
274
+ {
275
+ id: "repo-stop",
276
+ label: "Stop",
277
+ for: ["repo"],
278
+ tier: "danger",
279
+ confirm: true,
280
+ when: (r) => runsHere(repoEntry(r)) && has(repoEntry(r), "down"),
281
+ run: (r) => C.hooks.runCmd(repoEntry(r), "down"),
282
+ },
283
+ {
284
+ id: "repo-down-deps",
285
+ label: "Stop with dependencies",
286
+ for: ["repo"],
287
+ tier: "danger",
288
+ confirm: true,
289
+ when: anyHere,
290
+ run: (r) => C.hooks.downWithDeps(r.repo),
291
+ },
292
+ ];
293
+
294
+ /** The manifest's own commands (setup, seed, reset-db, …) become actions too. */
295
+ function commandActions(s) {
296
+ if (!runsHere(s)) return [];
297
+ return s.commands
298
+ .filter((c) => !LIFECYCLE.includes(c))
299
+ .map((c) => ({
300
+ id: "cmd:" + c,
301
+ label: "Run " + c,
302
+ for: ["service"],
303
+ tier: commandTier(c),
304
+ confirm: commandTier(c) === "danger",
305
+ when: () => true,
306
+ run: (x) => C.hooks.runCmd(x, c),
307
+ }));
308
+ }
309
+
310
+ export function actionsFor(x) {
311
+ const kind = x.services ? "repo" : "service";
312
+ return resolveActions(
313
+ [...ACTIONS, ...(kind === "service" ? commandActions(x) : [])],
314
+ x,
315
+ kind,
316
+ );
317
+ }
318
+
319
+ export const TABS = [
320
+ { id: "services", label: "Services", for: ["repo"], order: 0 },
321
+ { id: "overview", label: "Overview", for: ["service", "repo"], order: 1 },
322
+ { id: "logs", label: "Logs", for: ["service"], when: runsHere, order: 2 },
323
+ { id: "config", label: "Config", for: ["service", "repo"], order: 3 },
324
+ { id: "activity", label: "Activity", for: ["service", "repo"], order: 4 },
325
+ ];
326
+
327
+ const kv = (rows) =>
328
+ `<dl class="kv">${rows
329
+ .filter(Boolean)
330
+ .map(([k, v]) => `<dt>${esc(k)}</dt><dd>${v}</dd>`)
331
+ .join("")}</dl>`;
332
+ const none = (t) => `<span class="dg-none">${esc(t)}</span>`;
333
+ const checkText = (s) =>
334
+ s.health.url
335
+ ? `<code>GET ${esc(s.health.url)}</code>`
336
+ : s.health.check
337
+ ? `<code>${esc(s.health.check)}</code>`
338
+ : none("none declared");
339
+ function chip(id) {
340
+ const s = C.model().byId.get(id);
341
+ if (!s) return `<span class="dg-chip">${esc(id)}</span>`;
342
+ return `<button class="dg-chip" data-goto="${esc(id)}"><span class="dg-dot ${status(s)}"></span>${esc(id)}${s.runs === "remote" ? " ☁" : ""}</button>`;
343
+ }
344
+ const refChip = (r) =>
345
+ r.id
346
+ ? chip(r.id)
347
+ : `<button class="dg-chip" data-goto-repo="${esc(r.repo)}">${esc(r.repo)} ${placeGlyph(repoPlacement(C.model(), r.repo))}</button>`;
348
+ function logsText(l) {
349
+ if (!l) return null;
350
+ if (typeof l === "string") return l;
351
+ if (l.type === "shell") return l.cmd;
352
+ if (l.type === "cloudwatch")
353
+ return `cloudwatch ${l.logGroup}${l.logStream ? " / " + l.logStream : ""}`;
354
+ return l.type;
355
+ }
356
+ const hhmm = (ts) => {
357
+ const d = new Date(ts);
358
+ return isNaN(d) ? "" : d.toTimeString().slice(0, 5);
359
+ };
360
+
361
+ export const SECTIONS = [
362
+ // Overview: is it OK, where is it, what does it touch
363
+ {
364
+ id: "remote-note",
365
+ tab: "overview",
366
+ for: ["service"],
367
+ order: 0,
368
+ when: (s) => !isLocal(s),
369
+ render: (s) =>
370
+ `<p class="dg-callout">☁ Runs in <b>${esc(s.target?.env || "a remote env")}</b>${s.target?.account ? " · " + esc(s.target.account) : ""}. Start, stop and logs happen there, not on this machine.</p>`,
371
+ },
372
+ {
373
+ id: "health",
374
+ tab: "overview",
375
+ title: "Health",
376
+ for: ["service"],
377
+ order: 10,
378
+ render: (s) =>
379
+ kv([
380
+ [
381
+ "Status",
382
+ `<span class="dg-pill ${status(s)}">${esc(statusWord(s))}</span> ${s.health.ms != null ? `<span class="dg-muted">${fmtMs(s.health.ms)}</span>` : ""}${s.health.error ? ` <span class="dg-muted">${esc(s.health.error)}</span>` : ""}`,
383
+ ],
384
+ ["Check", checkText(s)],
385
+ (s.open || s.target?.url) && [
386
+ "Opens",
387
+ `<a href="${esc(s.open || s.target.url)}" target="_blank" rel="noopener">${esc(s.open || s.target.url)}</a>`,
388
+ ],
389
+ ]),
390
+ },
391
+ {
392
+ id: "health-roll",
393
+ tab: "overview",
394
+ title: "Health",
395
+ for: ["repo"],
396
+ order: 10,
397
+ render: (r) => {
398
+ const ok = r.services.filter((s) => s.health.ok).length;
399
+ const p = repoPlacement(C.model(), r.repo);
400
+ return kv([
401
+ [
402
+ "Services",
403
+ `<span class="dg-pill ${ok === r.services.length ? "ok" : "bad"}">${ok}/${r.services.length} healthy</span>`,
404
+ ],
405
+ [
406
+ "Placement",
407
+ esc(
408
+ {
409
+ local: "⌂ all local",
410
+ remote: "☁ all remote",
411
+ none: "no runtime",
412
+ hybrid:
413
+ "⇄ hybrid: " +
414
+ r.services.map((s) => `${s.name} ${s.runs}`).join(", "),
415
+ }[p],
416
+ ),
417
+ ],
418
+ ]);
419
+ },
420
+ },
421
+ {
422
+ id: "connections",
423
+ tab: "overview",
424
+ title: "Connections",
425
+ for: ["service", "repo"],
426
+ order: 20,
427
+ render: (x) => {
428
+ const c = connections(C.model(), x);
429
+ return `<div class="dg-conn"><div><h4>Depends on</h4>${c.deps.length ? `<div class="dg-chips">${c.deps.map(refChip).join("")}</div>` : none("nothing")}</div><div><h4>Used by</h4>${c.users.length ? `<div class="dg-chips">${c.users.map(refChip).join("")}</div>` : none("nothing in this workspace")}</div></div>`;
430
+ },
431
+ },
432
+ {
433
+ id: "missing",
434
+ tab: "overview",
435
+ title: "Missing dependencies",
436
+ for: ["service", "repo"],
437
+ order: 25,
438
+ when: (x) => (x.services ? x.services : [x]).some((s) => s.missing.length),
439
+ render: (x) => {
440
+ const deps = [
441
+ ...new Set((x.services ? x.services : [x]).flatMap((s) => s.missing)),
442
+ ];
443
+ return `<p class="dg-muted" style="margin:0 0 8px;font-size:13px">Declared but not in this workspace.</p><ul class="cmds">${deps.map((d) => `<li><code>${esc(d)}</code><button class="dg-btn sm" data-add-dep="${esc(d)}">Add</button></li>`).join("")}</ul>`;
444
+ },
445
+ },
446
+ {
447
+ id: "about",
448
+ tab: "overview",
449
+ title: "About",
450
+ for: ["service"],
451
+ order: 30,
452
+ render: (s) =>
453
+ kv([
454
+ [
455
+ "Role",
456
+ [s.entry && "entry point", s.infra && "infrastructure"]
457
+ .filter(Boolean)
458
+ .join(", ") || "service",
459
+ ],
460
+ ["Application", esc(s.app)],
461
+ ["Branch", s.branch ? `⎇ ${esc(s.branch)}` : none("unknown")],
462
+ [
463
+ "Starts",
464
+ s.autostart ? "with the workspace" : "on demand (auto-start off)",
465
+ ],
466
+ !s.cloned && ["On disk", "not cloned yet: Sync clones it"],
467
+ ]),
468
+ },
469
+ {
470
+ id: "about-repo",
471
+ tab: "overview",
472
+ title: "About",
473
+ for: ["repo"],
474
+ order: 30,
475
+ render: (r) =>
476
+ kv([
477
+ ["Application", esc(r.services[0].app)],
478
+ [
479
+ "Branch",
480
+ r.services[0].branch
481
+ ? `⎇ ${esc(r.services[0].branch)}`
482
+ : none("unknown"),
483
+ ],
484
+ ["Mode", esc(r.services[0].mode)],
485
+ [
486
+ "Starts",
487
+ r.services[0].autostart
488
+ ? "with the workspace"
489
+ : "on demand (auto-start off)",
490
+ ],
491
+ ]),
492
+ },
493
+ // Services (repo): each row with its own primary action
494
+ {
495
+ id: "svc-list",
496
+ tab: "services",
497
+ for: ["repo"],
498
+ order: 0,
499
+ render: (r) =>
500
+ `<table class="svc"><tbody>${r.services
501
+ .map((s) => {
502
+ const a = actionsFor(s);
503
+ return `<tr><td><span class="dg-dot ${status(s)}"></span></td><td><button class="nm" data-goto="${esc(s.id)}">${esc(s.name)}</button>${s.entry ? ' <span class="dg-tag entry">entry</span>' : ""}${!isLocal(s) ? ' <span class="dg-badge remote">☁ ' + esc(s.target?.env || "remote") + "</span>" : ""}<div class="h">${esc(healthLine(s))}</div></td><td class="a">${a.primary ? actBtn(a.primary, s, "sm") : ""}</td></tr>`;
504
+ })
505
+ .join("")}</tbody></table>`,
506
+ },
507
+ // Logs: the live tail inline
508
+ {
509
+ id: "logs",
510
+ tab: "logs",
511
+ for: ["service"],
512
+ order: 0,
513
+ render: (s) => logTail(s),
514
+ },
515
+ // Config
516
+ {
517
+ id: "commands",
518
+ tab: "config",
519
+ title: "Commands",
520
+ for: ["service"],
521
+ order: 10,
522
+ render: (s) =>
523
+ s.commands.length
524
+ ? `<ul class="cmds">${s.commands.map((c) => `<li><code>${esc(c)}</code>${runsHere(s) ? `<button class="dg-btn sm" data-run="${esc(c)}" data-key="${esc(s.id)}" ${commandTier(c) === "danger" || c === "down" ? "data-confirm" : ""}>Run</button>` : ""}</li>`).join("")}</ul>`
525
+ : none(isLocal(s) ? "none declared" : "none here: it runs remotely"),
526
+ },
527
+ {
528
+ id: "source",
529
+ tab: "config",
530
+ title: "Where it comes from",
531
+ for: ["service", "repo"],
532
+ order: 20,
533
+ render: (x) => {
534
+ const s = x.services ? x.services[0] : x;
535
+ return kv([
536
+ ["Repo", s.path ? `<code>${esc(s.path)}</code>` : none("unknown")],
537
+ [
538
+ "Manifest",
539
+ s.manifestLayers.length
540
+ ? `<span class="dg-layers">${s.manifestLayers.map((l) => `<span>${esc(l)}</span>`).join("")}</span>`
541
+ : none("none found"),
542
+ ],
543
+ ["Mode", esc(s.mode)],
544
+ !x.services && [
545
+ "Logs from",
546
+ logsText(s.logs)
547
+ ? `<code>${esc(logsText(s.logs))}</code>`
548
+ : isLocal(s)
549
+ ? none("the service's own log file")
550
+ : none("the remote env"),
551
+ ],
552
+ !x.services && ["Health check", checkText(s)],
553
+ ]);
554
+ },
555
+ },
556
+ // Activity: the audit trail (R14), yours and agents'
557
+ {
558
+ id: "activity",
559
+ tab: "activity",
560
+ for: ["service", "repo"],
561
+ order: 0,
562
+ render: (x) => activityList(x),
563
+ },
564
+ ];
565
+
566
+ function actBtn(a, x, cls = "") {
567
+ const why = a.disabled?.(x);
568
+ const label = a.labelFor ? a.labelFor(x) : a.label;
569
+ return `<button class="dg-btn ${cls}" data-action="${esc(a.id)}" data-key="${esc(x.services ? "repo:" + x.repo : x.id)}" ${a.confirm ? "data-confirm" : ""} ${why ? `disabled title="${esc(why)}"` : ""}>${esc(label)}</button>`;
570
+ }
571
+ export { actBtn };
572
+
573
+ const GROUP_LABEL = {
574
+ common: "More actions",
575
+ commands: "Manifest commands",
576
+ tools: "Tools",
577
+ danger: "Careful",
578
+ };
579
+ function actionBar(x) {
580
+ const { primary, common, groups } = actionsFor(x);
581
+ const key = esc(x.services ? "repo:" + x.repo : x.id);
582
+ const menu = groups.length
583
+ ? `<div class="dg-more"><button class="dg-btn" data-more aria-haspopup="menu" aria-expanded="false">More ▾</button><div class="dg-menu" role="menu" hidden>${groups
584
+ .map(
585
+ ([t, l]) =>
586
+ `<div class="dg-mg ${t}" role="group" aria-label="${GROUP_LABEL[t]}"><div class="dg-ml">${GROUP_LABEL[t]}</div>${l
587
+ .map((a) => {
588
+ const why = a.disabled?.(x);
589
+ return `<button role="menuitem" data-action="${esc(a.id)}" data-key="${key}" ${a.confirm ? "data-confirm" : ""} ${why ? `disabled title="${esc(why)}"` : ""}>${esc(a.labelFor ? a.labelFor(x) : a.label)}</button>`;
590
+ })
591
+ .join("")}</div>`,
592
+ )
593
+ .join("")}</div></div>`
594
+ : "";
595
+ return `<div class="dg-actions">${primary ? actBtn(primary, x, "primary") : ""}${common.map((a) => actBtn(a, x)).join("")}${menu}</div>`;
596
+ }
597
+
598
+ /** The entity behind a selection: { kind, x }. */
599
+ export function entity(sel) {
600
+ const m = C.model();
601
+ if (sel.repo)
602
+ return m.byRepo.has(sel.repo)
603
+ ? {
604
+ kind: "repo",
605
+ x: { repo: sel.repo, services: m.byRepo.get(sel.repo) },
606
+ }
607
+ : null;
608
+ const s = m.byId.get(sel.id);
609
+ return s ? { kind: "service", x: s } : null;
610
+ }
611
+
612
+ // Live sections (log tail, activity fetch) register a disposer here; a
613
+ // re-render or close disposes them, so one panel holds at most one stream.
614
+ let disposers = [];
615
+ export function disposePanel() {
616
+ for (const d of disposers) d();
617
+ disposers = [];
618
+ }
619
+
620
+ /** Fill `el` with the panel for `sel`. Returns false if the selection is gone. */
621
+ export function renderPanel(el, sel) {
622
+ disposePanel();
623
+ const e = entity(sel);
624
+ if (!e) return false;
625
+ const { kind, x } = e;
626
+ const st = C.st;
627
+ const m = C.model();
628
+ const s0 = kind === "repo" ? x.services[0] : x;
629
+ const org = orgOf(m, s0.org);
630
+ const tabs = TABS.filter(
631
+ (t) => t.for.includes(kind) && (!t.when || t.when(x)),
632
+ ).sort((a, b) => a.order - b.order);
633
+ if (!tabs.find((t) => t.id === st.tab)) st.tab = tabs[0].id;
634
+ const ok =
635
+ kind === "repo" ? x.services.every((s) => s.health.ok) : x.health.ok;
636
+ const word =
637
+ kind === "repo"
638
+ ? `${x.services.filter((s) => s.health.ok).length}/${x.services.length} healthy`
639
+ : statusWord(x);
640
+ const place = kind === "repo" ? repoPlacement(m, x.repo) : x.runs;
641
+ const placeLabel =
642
+ kind === "repo"
643
+ ? {
644
+ local: "⌂ local",
645
+ remote: "☁ remote",
646
+ hybrid: "⇄ hybrid",
647
+ none: "no runtime",
648
+ }[place]
649
+ : placementText(x);
650
+ const pillCls = ok
651
+ ? "ok"
652
+ : kind === "repo"
653
+ ? x.services.some((s) => s.health.ok === false)
654
+ ? "bad"
655
+ : ""
656
+ : ok === false
657
+ ? "bad"
658
+ : "";
659
+ el.innerHTML = `${st.popped ? "" : '<div class="dg-resize" data-resize title="Drag to resize"></div>'}<div class="dg-dh">
660
+ <nav class="dg-crumbs" aria-label="Location"><span style="color:${esc(org.color)}">${esc(org.label)}</span> › <span>${esc(s0.app)}</span>${kind === "service" && m.byRepo.get(x.repo).length > 1 ? ` › <button data-goto-repo-panel="${esc(x.repo)}">${esc(x.repo)}</button>` : ""}</nav>
661
+ <div class="t"><h2>${esc(kind === "repo" ? x.repo : x.name)}</h2><span class="dg-pill ${pillCls}">${esc(word)}</span>${place !== "local" || m.mixed ? `<span class="dg-badge ${place}">${esc(placeLabel)}</span>` : ""}
662
+ <span class="dg-hbtns"><button class="dg-ib" data-popout title="${st.popped ? "Dock as side panel" : "Expand to pop-up"}" aria-label="${st.popped ? "Dock as side panel" : "Expand to pop-up"}">${st.popped ? "⇲" : "⤢"}</button><button class="dg-ib" data-close aria-label="Close details">×</button></span></div>
663
+ <div class="sub">${esc(kind === "repo" ? `${x.services.length} services · ${s0.path}` : x.id)}</div>
664
+ ${actionBar(x)}
665
+ <div class="dg-tabs" role="tablist">${tabs.map((t) => `<button role="tab" aria-selected="${t.id === st.tab}" data-tab="${t.id}">${esc(t.label)}</button>`).join("")}</div>
666
+ </div><div class="dg-pbody" role="tabpanel"></div>`;
667
+ const body = el.querySelector(".dg-pbody");
668
+ for (const c of SECTIONS.filter(
669
+ (c) => c.tab === st.tab && c.for.includes(kind) && (!c.when || c.when(x)),
670
+ ).sort((a, b) => a.order - b.order)) {
671
+ const sec = document.createElement("section");
672
+ sec.className = "dg-sec";
673
+ sec.dataset.section = c.id;
674
+ if (c.title)
675
+ sec.insertAdjacentHTML("beforeend", `<h3>${esc(c.title)}</h3>`);
676
+ const out = c.render(x);
677
+ if (typeof out === "string") sec.insertAdjacentHTML("beforeend", out);
678
+ else if (out) sec.appendChild(out);
679
+ body.appendChild(sec);
680
+ }
681
+ return true;
682
+ }
683
+
684
+ /** Run an action by id for a data-key ("repo:<r>" or a service id). */
685
+ export function runAction(id, key) {
686
+ const e = entity(
687
+ key.startsWith("repo:") ? { repo: key.slice(5) } : { id: key },
688
+ );
689
+ if (!e) return;
690
+ const a = actionsFor(e.x).all.find((y) => y.id === id);
691
+ if (a && !a.disabled?.(e.x)) a.run(e.x);
692
+ }
693
+
694
+ const TAIL_MAX = 500;
695
+ function logTail(s) {
696
+ const wrap = document.createElement("div");
697
+ wrap.innerHTML = `<div class="dg-logbar"><button class="dg-btn sm" data-log="pause" aria-pressed="false">Pause</button><button class="dg-btn sm" data-log="wrap" aria-pressed="false">Wrap</button><input type="search" placeholder="Filter lines" aria-label="Filter log lines"><button class="dg-btn sm" data-log="page">Open in Logs ↗</button></div><pre class="dg-log" aria-live="off"></pre>`;
698
+ const pre = wrap.querySelector("pre");
699
+ const filter = wrap.querySelector("input");
700
+ const lines = [];
701
+ let paused = false,
702
+ queued = false,
703
+ open = true;
704
+ const draw = () => {
705
+ queued = false;
706
+ const q = filter.value.trim().toLowerCase();
707
+ const shown = q ? lines.filter((l) => l.toLowerCase().includes(q)) : lines;
708
+ const atEnd = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 8;
709
+ pre.textContent = shown.length
710
+ ? shown.join("\n")
711
+ : open
712
+ ? "Waiting for log lines…"
713
+ : "The log stream ended.";
714
+ if (atEnd) pre.scrollTop = pre.scrollHeight;
715
+ };
716
+ const schedule = () => {
717
+ if (!queued && !paused) {
718
+ queued = true;
719
+ requestAnimationFrame(draw);
720
+ }
721
+ };
722
+ draw();
723
+ const es = new EventSource(
724
+ `/api/service/taillog?${new URLSearchParams({ repo: s.repo, service: s.name })}`,
725
+ );
726
+ es.onmessage = (ev) => {
727
+ let msg;
728
+ try {
729
+ msg = JSON.parse(ev.data);
730
+ } catch {
731
+ return;
732
+ }
733
+ if (msg.type !== "line") return;
734
+ lines.push(msg.text);
735
+ if (lines.length > TAIL_MAX) lines.splice(0, lines.length - TAIL_MAX);
736
+ schedule();
737
+ };
738
+ es.onerror = () => {
739
+ es.close();
740
+ open = false;
741
+ schedule();
742
+ };
743
+ disposers.push(() => es.close());
744
+ filter.addEventListener("input", () => {
745
+ queued = false;
746
+ draw();
747
+ });
748
+ wrap.addEventListener("click", (e) => {
749
+ const b = e.target.closest("[data-log]");
750
+ if (!b) return;
751
+ e.stopPropagation();
752
+ if (b.dataset.log === "pause") {
753
+ paused = !paused;
754
+ b.setAttribute("aria-pressed", String(paused));
755
+ b.textContent = paused ? "Resume" : "Pause";
756
+ if (!paused) draw();
757
+ }
758
+ if (b.dataset.log === "wrap") {
759
+ const on = pre.classList.toggle("wrap");
760
+ b.setAttribute("aria-pressed", String(on));
761
+ }
762
+ if (b.dataset.log === "page") C.hooks.openLogsPage(s);
763
+ });
764
+ return wrap;
765
+ }
766
+
767
+ function activityList(x) {
768
+ const el = document.createElement("div");
769
+ el.innerHTML = none("Loading…");
770
+ const q = x.services
771
+ ? `repo=${encodeURIComponent(x.repo)}`
772
+ : `address=${encodeURIComponent(x.repo + "/" + x.name)}`;
773
+ let live = true;
774
+ disposers.push(() => (live = false));
775
+ fetch(`/api/activity?${q}`)
776
+ .then((r) => (r.ok ? r.json() : { entries: [] }))
777
+ .catch(() => ({ entries: [] }))
778
+ .then(({ entries = [] }) => {
779
+ if (!live) return;
780
+ el.innerHTML = entries.length
781
+ ? `<ol class="act">${entries.map((a) => `<li><time datetime="${esc(a.ts)}">${esc(hhmm(a.ts))}</time><span class="dg-who ${a.actor === "agent" ? "agent" : ""}">${esc(a.actor === "agent" ? "[agent]" + (a.client ? " " + a.client : "") : "you")}</span><span>${esc(a.verb)} ${esc(a.detail)}</span></li>`).join("")}</ol>`
782
+ : none(
783
+ "No actions yet. Starts, restarts and syncs, yours and agents', show here.",
784
+ );
785
+ });
786
+ return el;
787
+ }