@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,2595 @@
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
+ <title>__DEN_TITLE__</title>
7
+ <link rel="icon" href="/favicon.ico?v=2" />
8
+ <style>
9
+ *,
10
+ *::before,
11
+ *::after {
12
+ box-sizing: border-box;
13
+ margin: 0;
14
+ padding: 0;
15
+ }
16
+
17
+ :root {
18
+ --bg: #0d1117;
19
+ --surface: #161b22;
20
+ --border: #30363d;
21
+ --text: #c9d1d9;
22
+ --muted: #8b949e;
23
+ --accent: #58a6ff;
24
+ --green: #3fb950;
25
+ --red: #f85149;
26
+ --yellow: #d29922;
27
+ --mono: "SF Mono", "Cascadia Code", "Fira Code", "Menlo", monospace;
28
+ }
29
+
30
+ html,
31
+ body {
32
+ height: 100%;
33
+ background: var(--bg);
34
+ color: var(--text);
35
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
36
+ font-size: 13px;
37
+ }
38
+ body {
39
+ display: flex;
40
+ flex-direction: column;
41
+ overflow: hidden;
42
+ }
43
+
44
+ /* ── Bar 1: top navbar ─────────────────────────────────────────────────── */
45
+ .topbar {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 10px;
49
+ padding: 7px 14px;
50
+ background: var(--surface);
51
+ border-bottom: 1px solid var(--border);
52
+ flex-shrink: 0;
53
+ z-index: 10;
54
+ }
55
+ .topbar h1 {
56
+ font-family: var(--mono);
57
+ font-size: 14px;
58
+ font-weight: 600;
59
+ letter-spacing: -0.02em;
60
+ margin-right: 4px;
61
+ }
62
+
63
+ /* ── Env badge (subbar selector pill) ────────────────────────────────── */
64
+ .env-badge {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 5px;
68
+ padding: 2px 8px 2px 6px;
69
+ border-radius: 999px;
70
+ background: var(--surface);
71
+ border: 1px solid var(--border);
72
+ font-size: 11px;
73
+ font-weight: 500;
74
+ color: var(--muted);
75
+ }
76
+ .env-badge::before {
77
+ content: "env";
78
+ font-size: 9px;
79
+ font-weight: 600;
80
+ letter-spacing: 0.05em;
81
+ color: var(--muted);
82
+ opacity: 0.6;
83
+ text-transform: uppercase;
84
+ }
85
+ .env-badge select {
86
+ background: transparent;
87
+ border: none;
88
+ outline: none;
89
+ color: var(--text);
90
+ font-size: 11px;
91
+ font-weight: 600;
92
+ cursor: pointer;
93
+ padding: 0;
94
+ -webkit-appearance: none;
95
+ appearance: none;
96
+ }
97
+ .env-badge select option {
98
+ background: var(--surface);
99
+ }
100
+
101
+ /* ── Topbar env pill ──────────────────────────────────────────────────── */
102
+ #topbar-env-badge {
103
+ display: none;
104
+ align-items: center;
105
+ padding: 2px 7px;
106
+ border-radius: 999px;
107
+ border: 1px solid var(--border);
108
+ font-size: 10px;
109
+ font-weight: 600;
110
+ letter-spacing: 0.03em;
111
+ color: var(--muted);
112
+ white-space: nowrap;
113
+ }
114
+
115
+ /* ── Bar 2: sub-navbar ─────────────────────────────────────────────────── */
116
+ .subbar {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 8px;
120
+ padding: 5px 14px;
121
+ background: var(--surface);
122
+ border-bottom: 1px solid var(--border);
123
+ flex-shrink: 0;
124
+ }
125
+
126
+ /* ── Shared ────────────────────────────────────────────────────────────── */
127
+ .spacer {
128
+ flex: 1;
129
+ }
130
+
131
+ .dot {
132
+ width: 7px;
133
+ height: 7px;
134
+ border-radius: 50%;
135
+ background: var(--muted);
136
+ flex-shrink: 0;
137
+ display: inline-block;
138
+ }
139
+ .dot.ok {
140
+ background: var(--green);
141
+ }
142
+ .dot.err {
143
+ background: var(--red);
144
+ }
145
+ .dot.running {
146
+ background: var(--yellow);
147
+ animation: pulse 1s ease-in-out infinite;
148
+ }
149
+ .dot.passed {
150
+ background: var(--green);
151
+ }
152
+ .dot.failed {
153
+ background: var(--red);
154
+ }
155
+ @keyframes pulse {
156
+ 0%,
157
+ 100% {
158
+ opacity: 1;
159
+ }
160
+ 50% {
161
+ opacity: 0.35;
162
+ }
163
+ }
164
+
165
+ .seg {
166
+ display: flex;
167
+ gap: 2px;
168
+ background: var(--bg);
169
+ border: 1px solid var(--border);
170
+ border-radius: 6px;
171
+ padding: 2px;
172
+ }
173
+ .seg button {
174
+ background: none;
175
+ border: none;
176
+ color: var(--muted);
177
+ padding: 3px 10px;
178
+ border-radius: 4px;
179
+ cursor: pointer;
180
+ font-size: 12px;
181
+ font-weight: 500;
182
+ transition: color 0.1s;
183
+ white-space: nowrap;
184
+ }
185
+ .seg button:hover {
186
+ color: var(--text);
187
+ }
188
+ .seg button.active {
189
+ background: var(--border);
190
+ color: var(--text);
191
+ }
192
+
193
+ nav {
194
+ display: flex;
195
+ gap: 2px;
196
+ }
197
+ nav a {
198
+ color: var(--muted);
199
+ text-decoration: none;
200
+ padding: 3px 10px;
201
+ border-radius: 4px;
202
+ font-size: 12px;
203
+ font-weight: 500;
204
+ transition: color 0.1s;
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 5px;
208
+ }
209
+ nav a:hover {
210
+ color: var(--text);
211
+ background: var(--border);
212
+ }
213
+ nav a.active {
214
+ color: var(--text);
215
+ }
216
+
217
+ .badge {
218
+ background: var(--yellow);
219
+ color: #0d1117;
220
+ font-size: 10px;
221
+ font-weight: 700;
222
+ padding: 1px 5px;
223
+ border-radius: 8px;
224
+ line-height: 1.4;
225
+ display: none;
226
+ }
227
+ .badge.visible {
228
+ display: inline;
229
+ }
230
+
231
+ select {
232
+ background: var(--bg);
233
+ border: 1px solid var(--border);
234
+ color: var(--text);
235
+ padding: 4px 8px;
236
+ border-radius: 6px;
237
+ font-size: 12px;
238
+ cursor: pointer;
239
+ outline: none;
240
+ max-width: 220px;
241
+ }
242
+ select:focus {
243
+ border-color: var(--accent);
244
+ }
245
+ .text-input {
246
+ background: var(--bg);
247
+ border: 1px solid var(--border);
248
+ color: var(--text);
249
+ padding: 4px 8px;
250
+ border-radius: 6px;
251
+ font-size: 12px;
252
+ outline: none;
253
+ min-width: 240px;
254
+ }
255
+ .text-input:focus {
256
+ border-color: var(--accent);
257
+ }
258
+ .sec-run {
259
+ padding: 10px 12px;
260
+ }
261
+ .sec-run h3 {
262
+ font-size: 12px;
263
+ font-weight: 600;
264
+ margin-bottom: 6px;
265
+ }
266
+ .sec-table {
267
+ border-collapse: collapse;
268
+ width: 100%;
269
+ font-size: 12px;
270
+ }
271
+ .sec-table th,
272
+ .sec-table td {
273
+ text-align: left;
274
+ padding: 4px 8px;
275
+ border-bottom: 1px solid var(--border);
276
+ }
277
+ .sec-table td a {
278
+ color: var(--accent, #58a6ff);
279
+ }
280
+ .sec-finding td {
281
+ color: var(--muted);
282
+ padding-left: 22px;
283
+ font-size: 11px;
284
+ }
285
+ .sec-hit.sec-high,
286
+ span.sec-high {
287
+ color: #f85149;
288
+ font-weight: 600;
289
+ }
290
+ .sec-hit.sec-medium,
291
+ span.sec-medium {
292
+ color: #d29922;
293
+ font-weight: 600;
294
+ }
295
+ .sec-hit.sec-low,
296
+ span.sec-low {
297
+ color: #8b949e;
298
+ }
299
+ .sec-err {
300
+ color: #f85149;
301
+ }
302
+ .smoke-override {
303
+ display: none;
304
+ align-items: center;
305
+ gap: 6px;
306
+ }
307
+ .smoke-override.active {
308
+ display: flex;
309
+ }
310
+ .smoke-override-label {
311
+ font-size: 11px;
312
+ color: var(--muted);
313
+ white-space: nowrap;
314
+ }
315
+ .toggle-input {
316
+ accent-color: var(--accent);
317
+ cursor: pointer;
318
+ }
319
+ /* Per-perf-test optional launch fields (generic, declarative) */
320
+ #perf-opt-fields {
321
+ display: contents;
322
+ }
323
+ .perf-opt-section {
324
+ display: flex;
325
+ align-items: center;
326
+ gap: 10px;
327
+ flex-wrap: wrap;
328
+ }
329
+ .perf-opt-section-head {
330
+ font-weight: 600;
331
+ }
332
+ .perf-opt-disabled {
333
+ opacity: 0.45;
334
+ }
335
+
336
+ /* ── Multi-check module picker ─────────────────────────────────────────── */
337
+ .module-picker {
338
+ position: relative;
339
+ }
340
+ .module-picker-btn {
341
+ display: flex;
342
+ align-items: center;
343
+ gap: 6px;
344
+ background: var(--bg);
345
+ border: 1px solid var(--border);
346
+ color: var(--text);
347
+ padding: 4px 8px;
348
+ border-radius: 6px;
349
+ font-size: 12px;
350
+ cursor: pointer;
351
+ outline: none;
352
+ white-space: nowrap;
353
+ min-width: 120px;
354
+ max-width: 240px;
355
+ }
356
+ .module-picker-btn:focus,
357
+ .module-picker-btn.open {
358
+ border-color: var(--accent);
359
+ }
360
+ #module-picker-label {
361
+ flex: 1;
362
+ text-align: left;
363
+ overflow: hidden;
364
+ text-overflow: ellipsis;
365
+ }
366
+ .module-picker-arrow {
367
+ font-size: 10px;
368
+ color: var(--muted);
369
+ flex-shrink: 0;
370
+ }
371
+ .module-picker-dropdown {
372
+ display: none;
373
+ position: absolute;
374
+ top: calc(100% + 4px);
375
+ left: 0;
376
+ z-index: 100;
377
+ background: var(--surface);
378
+ border: 1px solid var(--border);
379
+ border-radius: 6px;
380
+ min-width: 180px;
381
+ max-height: 260px;
382
+ overflow-y: auto;
383
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
384
+ }
385
+ .module-picker-dropdown.open {
386
+ display: block;
387
+ }
388
+ .module-check-item {
389
+ display: flex;
390
+ align-items: center;
391
+ gap: 8px;
392
+ padding: 6px 12px;
393
+ cursor: pointer;
394
+ font-size: 12px;
395
+ color: var(--text);
396
+ white-space: nowrap;
397
+ }
398
+ .module-check-item:hover {
399
+ background: var(--border);
400
+ }
401
+ .module-check-item.module-check-all {
402
+ font-weight: 600;
403
+ }
404
+ .module-check-item input[type="checkbox"] {
405
+ accent-color: var(--accent);
406
+ cursor: pointer;
407
+ }
408
+ .module-check-divider {
409
+ height: 1px;
410
+ background: var(--border);
411
+ margin: 2px 0;
412
+ }
413
+
414
+ .status {
415
+ display: flex;
416
+ align-items: center;
417
+ gap: 6px;
418
+ font-size: 12px;
419
+ color: var(--muted);
420
+ }
421
+
422
+ /* ── Health indicators ─────────────────────────────────────────────────── */
423
+ .health {
424
+ display: flex;
425
+ align-items: center;
426
+ gap: 10px;
427
+ padding: 0 4px;
428
+ }
429
+ .health-item {
430
+ display: flex;
431
+ align-items: center;
432
+ gap: 5px;
433
+ font-size: 11px;
434
+ color: var(--muted);
435
+ }
436
+ .health-group {
437
+ display: flex;
438
+ align-items: center;
439
+ gap: 4px;
440
+ font-size: 11px;
441
+ color: var(--muted);
442
+ }
443
+ .health-group-label {
444
+ color: var(--muted);
445
+ margin-right: 1px;
446
+ }
447
+ .health-group-dots {
448
+ display: flex;
449
+ align-items: center;
450
+ gap: 3px;
451
+ }
452
+
453
+ /* ── Buttons ───────────────────────────────────────────────────────────── */
454
+ .run-btn {
455
+ background: var(--accent);
456
+ color: #0d1117;
457
+ border: none;
458
+ padding: 5px 18px;
459
+ border-radius: 6px;
460
+ font-size: 12px;
461
+ font-weight: 700;
462
+ cursor: pointer;
463
+ transition: opacity 0.1s;
464
+ white-space: nowrap;
465
+ }
466
+ .run-btn:hover:not(:disabled) {
467
+ opacity: 0.85;
468
+ }
469
+ .run-btn:disabled {
470
+ opacity: 0.45;
471
+ cursor: not-allowed;
472
+ }
473
+
474
+ /* ── Bar 2 context slots ───────────────────────────────────────────────── */
475
+ .bar2-slot {
476
+ display: none;
477
+ align-items: center;
478
+ gap: 8px;
479
+ }
480
+ .bar2-slot.active {
481
+ display: flex;
482
+ flex: 1;
483
+ }
484
+
485
+ /* ── Views ─────────────────────────────────────────────────────────────── */
486
+ .view {
487
+ flex: 1;
488
+ display: none;
489
+ overflow: hidden;
490
+ min-height: 0;
491
+ flex-direction: column;
492
+ }
493
+ .view.active {
494
+ display: flex;
495
+ }
496
+
497
+ /* ── Pane bar ──────────────────────────────────────────────────────────── */
498
+ .pane-bar {
499
+ display: flex;
500
+ align-items: center;
501
+ justify-content: space-between;
502
+ padding: 3px 12px;
503
+ background: var(--surface);
504
+ border-bottom: 1px solid var(--border);
505
+ flex-shrink: 0;
506
+ }
507
+ .pane-bar span {
508
+ font-size: 10px;
509
+ font-weight: 600;
510
+ text-transform: uppercase;
511
+ letter-spacing: 0.07em;
512
+ color: var(--muted);
513
+ }
514
+ .pane-bar button {
515
+ background: none;
516
+ border: none;
517
+ color: var(--muted);
518
+ font-size: 11px;
519
+ cursor: pointer;
520
+ padding: 2px 6px;
521
+ border-radius: 3px;
522
+ }
523
+ .pane-bar button:hover {
524
+ color: var(--text);
525
+ background: var(--border);
526
+ }
527
+ .pane-bar-left {
528
+ display: flex;
529
+ align-items: center;
530
+ gap: 10px;
531
+ }
532
+ .report-tabs {
533
+ display: none;
534
+ }
535
+ .report-tabs.active {
536
+ display: flex;
537
+ }
538
+
539
+ /* ── TESTS VIEW ────────────────────────────────────────────────────────── */
540
+ #view-tests {
541
+ flex-direction: column;
542
+ }
543
+
544
+ .controls {
545
+ display: flex;
546
+ align-items: center;
547
+ gap: 8px;
548
+ flex-wrap: wrap;
549
+ padding: 6px 14px;
550
+ background: var(--surface);
551
+ border-bottom: 1px solid var(--border);
552
+ flex-shrink: 0;
553
+ }
554
+
555
+ /* ── Source-of-truth bar (which repo/branch is being tested) ───────────── */
556
+ .test-sources-bar {
557
+ display: none;
558
+ flex-direction: column;
559
+ gap: 2px;
560
+ padding: 4px 14px;
561
+ background: var(--surface);
562
+ border-bottom: 1px solid var(--border);
563
+ font-size: 11px;
564
+ color: var(--muted);
565
+ flex-shrink: 0;
566
+ }
567
+ .test-sources-bar.visible {
568
+ display: flex;
569
+ }
570
+ .test-source {
571
+ display: flex;
572
+ align-items: center;
573
+ gap: 6px;
574
+ white-space: nowrap;
575
+ overflow: hidden;
576
+ text-overflow: ellipsis;
577
+ }
578
+ .test-source-label {
579
+ color: var(--muted);
580
+ flex-shrink: 0;
581
+ }
582
+ .test-source-repo {
583
+ color: var(--text);
584
+ font-weight: 600;
585
+ }
586
+ .test-source-ws {
587
+ color: var(--accent);
588
+ }
589
+ .test-source-branch {
590
+ color: var(--text);
591
+ font-family: var(--mono);
592
+ font-size: 10.5px;
593
+ }
594
+ .test-source-sha {
595
+ color: var(--muted);
596
+ font-family: var(--mono);
597
+ font-size: 10.5px;
598
+ }
599
+ .test-source-path {
600
+ color: var(--muted);
601
+ font-family: var(--mono);
602
+ font-size: 10.5px;
603
+ overflow: hidden;
604
+ text-overflow: ellipsis;
605
+ }
606
+ .test-source-empty {
607
+ color: var(--muted);
608
+ font-style: italic;
609
+ }
610
+
611
+ .tests-main {
612
+ flex: 1;
613
+ display: flex;
614
+ overflow: hidden;
615
+ min-height: 0;
616
+ }
617
+
618
+ .left-panel {
619
+ display: flex;
620
+ flex-direction: column;
621
+ min-height: 0;
622
+ min-width: 0;
623
+ overflow: hidden;
624
+ }
625
+
626
+ .tests-main.layout-top {
627
+ flex-direction: column;
628
+ }
629
+ .tests-main.layout-top .left-panel {
630
+ flex: 0 0 42%;
631
+ border-bottom: 1px solid var(--border);
632
+ }
633
+ .tests-main.layout-top .report-pane {
634
+ flex: 1;
635
+ }
636
+
637
+ .tests-main.layout-side {
638
+ flex-direction: row;
639
+ }
640
+ .tests-main.layout-side .left-panel {
641
+ flex: 0 0 340px;
642
+ border-right: 1px solid var(--border);
643
+ }
644
+ .tests-main.layout-side .report-pane {
645
+ flex: 1;
646
+ }
647
+ .tests-main.layout-side .controls {
648
+ padding: 6px 10px;
649
+ flex-wrap: wrap;
650
+ }
651
+ .tests-main.layout-side .pillars {
652
+ flex-wrap: wrap;
653
+ }
654
+
655
+ .output-pane {
656
+ flex: 1;
657
+ display: flex;
658
+ flex-direction: column;
659
+ min-height: 0;
660
+ }
661
+ #test-output {
662
+ flex: 1;
663
+ overflow-y: auto;
664
+ padding: 10px 14px;
665
+ font-family: var(--mono);
666
+ font-size: 11.5px;
667
+ line-height: 1.65;
668
+ white-space: pre-wrap;
669
+ word-break: break-all;
670
+ min-height: 0;
671
+ }
672
+ #test-output .cmd {
673
+ color: var(--accent);
674
+ }
675
+ #test-output .err {
676
+ color: var(--red);
677
+ }
678
+ #test-output .done-ok {
679
+ color: var(--green);
680
+ }
681
+ #test-output .done-fail {
682
+ color: var(--red);
683
+ }
684
+
685
+ .divider {
686
+ flex-shrink: 0;
687
+ background: var(--border);
688
+ transition: background 0.15s;
689
+ z-index: 1;
690
+ }
691
+ .tests-main.layout-side .divider {
692
+ width: 5px;
693
+ cursor: col-resize;
694
+ }
695
+ .tests-main.layout-top .divider {
696
+ height: 5px;
697
+ cursor: row-resize;
698
+ }
699
+ .divider:hover,
700
+ .divider.active {
701
+ background: var(--accent);
702
+ }
703
+ body.dragging {
704
+ user-select: none;
705
+ cursor: col-resize;
706
+ }
707
+ body.dragging-v {
708
+ user-select: none;
709
+ cursor: row-resize;
710
+ }
711
+
712
+ .report-pane {
713
+ display: flex;
714
+ flex-direction: column;
715
+ min-height: 0;
716
+ }
717
+ #report-frame {
718
+ flex: 1;
719
+ border: none;
720
+ width: 100%;
721
+ background: #fff;
722
+ display: none;
723
+ }
724
+ #report-placeholder {
725
+ flex: 1;
726
+ display: flex;
727
+ align-items: center;
728
+ justify-content: center;
729
+ color: var(--muted);
730
+ font-size: 12px;
731
+ }
732
+ #perf-report-container,
733
+ #report-raw-output {
734
+ flex: 1;
735
+ overflow: auto;
736
+ display: none;
737
+ min-height: 0;
738
+ }
739
+ #report-raw-output {
740
+ padding: 10px 14px;
741
+ font-family: var(--mono);
742
+ font-size: 11.5px;
743
+ line-height: 1.65;
744
+ white-space: pre-wrap;
745
+ word-break: break-all;
746
+ }
747
+ .perf-report {
748
+ padding: 14px;
749
+ display: flex;
750
+ flex-direction: column;
751
+ gap: 14px;
752
+ }
753
+ .perf-run-header,
754
+ .perf-module {
755
+ border: 1px solid var(--border);
756
+ border-radius: 10px;
757
+ background: var(--surface);
758
+ }
759
+ .perf-run-header {
760
+ padding: 14px;
761
+ display: flex;
762
+ justify-content: space-between;
763
+ gap: 12px;
764
+ align-items: flex-start;
765
+ }
766
+ .perf-run-header h2,
767
+ .perf-module-header h3,
768
+ .perf-section h4 {
769
+ margin: 0;
770
+ }
771
+ .perf-run-header p {
772
+ margin-top: 6px;
773
+ color: var(--muted);
774
+ }
775
+ .perf-run-meta {
776
+ display: flex;
777
+ flex-wrap: wrap;
778
+ gap: 10px;
779
+ color: var(--muted);
780
+ font-size: 11px;
781
+ justify-content: flex-end;
782
+ }
783
+ .perf-module-header {
784
+ display: flex;
785
+ justify-content: space-between;
786
+ align-items: center;
787
+ padding: 12px 14px;
788
+ border-bottom: 1px solid var(--border);
789
+ cursor: pointer;
790
+ list-style: none;
791
+ }
792
+ .perf-module-header::-webkit-details-marker {
793
+ display: none;
794
+ }
795
+ .perf-module-title-wrap {
796
+ display: flex;
797
+ align-items: center;
798
+ gap: 10px;
799
+ }
800
+ .perf-module-toggle {
801
+ display: inline-flex;
802
+ align-items: center;
803
+ justify-content: center;
804
+ width: 22px;
805
+ height: 22px;
806
+ border-radius: 6px;
807
+ background: rgba(13, 17, 23, 0.75);
808
+ border: 1px solid rgba(48, 54, 61, 0.8);
809
+ color: var(--muted);
810
+ font-size: 15px;
811
+ line-height: 1;
812
+ transition:
813
+ transform 0.15s ease,
814
+ color 0.15s ease,
815
+ border-color 0.15s ease;
816
+ flex-shrink: 0;
817
+ }
818
+ .perf-module-header:hover .perf-module-toggle {
819
+ color: var(--text);
820
+ border-color: var(--accent);
821
+ }
822
+ .perf-module[open] .perf-module-toggle {
823
+ transform: rotate(90deg);
824
+ }
825
+ .perf-module-title {
826
+ font-weight: 600;
827
+ }
828
+ .perf-badge {
829
+ text-transform: uppercase;
830
+ font-size: 10px;
831
+ font-weight: 700;
832
+ letter-spacing: 0.04em;
833
+ border-radius: 999px;
834
+ padding: 4px 8px;
835
+ }
836
+ .perf-ok {
837
+ background: rgba(63, 185, 80, 0.15);
838
+ color: var(--green);
839
+ }
840
+ .perf-fail {
841
+ background: rgba(248, 81, 73, 0.15);
842
+ color: var(--red);
843
+ }
844
+ .perf-section {
845
+ padding: 12px 14px;
846
+ border-top: 1px solid rgba(48, 54, 61, 0.5);
847
+ }
848
+ .perf-section:first-of-type {
849
+ border-top: none;
850
+ }
851
+ .perf-stat-grid {
852
+ display: grid;
853
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
854
+ gap: 10px;
855
+ margin-top: 10px;
856
+ }
857
+ .perf-stat {
858
+ border: 1px solid rgba(48, 54, 61, 0.7);
859
+ border-radius: 8px;
860
+ padding: 10px;
861
+ background: rgba(13, 17, 23, 0.55);
862
+ display: flex;
863
+ flex-direction: column;
864
+ gap: 6px;
865
+ }
866
+ .perf-stat-label {
867
+ color: var(--muted);
868
+ font-size: 11px;
869
+ }
870
+ .perf-table {
871
+ width: 100%;
872
+ border-collapse: collapse;
873
+ margin-top: 10px;
874
+ font-size: 12px;
875
+ }
876
+ .perf-table th,
877
+ .perf-table td {
878
+ border-top: 1px solid rgba(48, 54, 61, 0.7);
879
+ padding: 8px 6px;
880
+ text-align: left;
881
+ vertical-align: top;
882
+ }
883
+ .perf-table th {
884
+ color: var(--muted);
885
+ font-size: 11px;
886
+ text-transform: uppercase;
887
+ letter-spacing: 0.04em;
888
+ border-top: none;
889
+ }
890
+ .perf-empty {
891
+ margin-top: 10px;
892
+ color: var(--muted);
893
+ font-size: 12px;
894
+ }
895
+ .perf-muted {
896
+ color: var(--muted);
897
+ font-size: 12px;
898
+ }
899
+ .perf-value-chip {
900
+ display: inline-flex;
901
+ gap: 4px;
902
+ margin: 0 8px 6px 0;
903
+ padding: 3px 8px;
904
+ border-radius: 999px;
905
+ border: 1px solid rgba(48, 54, 61, 0.7);
906
+ background: rgba(13, 17, 23, 0.55);
907
+ font-size: 11px;
908
+ white-space: nowrap;
909
+ }
910
+
911
+ /* ── SERVICES VIEW ─────────────────────────────────────────────────────── */
912
+ #view-services {
913
+ flex-direction: column;
914
+ }
915
+
916
+ .services-pane {
917
+ padding: 12px 14px;
918
+ flex-shrink: 0;
919
+ display: flex;
920
+ flex-wrap: wrap;
921
+ gap: 10px;
922
+ }
923
+
924
+ .service-card {
925
+ background: var(--surface);
926
+ border: 1px solid var(--border);
927
+ border-radius: 8px;
928
+ padding: 12px 14px;
929
+ min-width: 220px;
930
+ }
931
+ .service-card-header {
932
+ display: flex;
933
+ align-items: center;
934
+ gap: 8px;
935
+ margin-bottom: 10px;
936
+ }
937
+ .service-card-header .dot {
938
+ width: 8px;
939
+ height: 8px;
940
+ }
941
+ .service-name {
942
+ font-size: 12px;
943
+ font-weight: 600;
944
+ color: var(--text);
945
+ }
946
+ .service-repo {
947
+ font-size: 11px;
948
+ color: var(--muted);
949
+ }
950
+
951
+ .cmd-row {
952
+ display: flex;
953
+ flex-wrap: wrap;
954
+ gap: 6px;
955
+ }
956
+ .cmd-btn {
957
+ background: var(--bg);
958
+ border: 1px solid var(--border);
959
+ color: var(--text);
960
+ padding: 4px 10px;
961
+ border-radius: 5px;
962
+ font-size: 11px;
963
+ font-weight: 500;
964
+ cursor: pointer;
965
+ transition:
966
+ border-color 0.1s,
967
+ color 0.1s;
968
+ }
969
+ .cmd-btn:hover:not(:disabled) {
970
+ border-color: var(--accent);
971
+ color: var(--accent);
972
+ }
973
+ .cmd-btn:disabled {
974
+ opacity: 0.45;
975
+ cursor: not-allowed;
976
+ }
977
+ .logs-btn {
978
+ border-color: var(--muted);
979
+ }
980
+ .logs-btn:hover:not(:disabled) {
981
+ border-color: var(--yellow) !important;
982
+ color: var(--yellow) !important;
983
+ }
984
+ .logs-btn.streaming {
985
+ border-color: var(--yellow);
986
+ color: var(--yellow);
987
+ }
988
+
989
+ .empty-state {
990
+ padding: 40px 14px;
991
+ color: var(--muted);
992
+ font-size: 12px;
993
+ text-align: center;
994
+ }
995
+
996
+ /* exec output strip */
997
+ .cmd-strip {
998
+ flex: 1;
999
+ display: flex;
1000
+ flex-direction: column;
1001
+ min-height: 0;
1002
+ border-top: 1px solid var(--border);
1003
+ }
1004
+ .cmd-strip.hidden {
1005
+ display: none;
1006
+ }
1007
+ #cmd-output {
1008
+ flex: 1;
1009
+ overflow-y: auto;
1010
+ padding: 6px 14px;
1011
+ font-family: var(--mono);
1012
+ font-size: 11px;
1013
+ line-height: 1.55;
1014
+ white-space: pre-wrap;
1015
+ word-break: break-all;
1016
+ }
1017
+ #cmd-output .cmd {
1018
+ color: var(--accent);
1019
+ }
1020
+ #cmd-output .err {
1021
+ color: var(--red);
1022
+ }
1023
+ #cmd-output .done-ok {
1024
+ color: var(--green);
1025
+ }
1026
+ #cmd-output .done-fail {
1027
+ color: var(--red);
1028
+ }
1029
+
1030
+ /* ── LOGS VIEW ─────────────────────────────────────────────────────────── */
1031
+ #view-logs {
1032
+ flex-direction: column;
1033
+ }
1034
+
1035
+ .logs-area {
1036
+ flex: 1;
1037
+ display: flex;
1038
+ overflow: hidden;
1039
+ min-height: 0;
1040
+ }
1041
+ .logs-area.dir-cols {
1042
+ flex-direction: row;
1043
+ }
1044
+ .logs-area.dir-rows {
1045
+ flex-direction: column;
1046
+ }
1047
+
1048
+ .log-pane {
1049
+ flex: 1;
1050
+ display: flex;
1051
+ flex-direction: column;
1052
+ min-height: 0;
1053
+ min-width: 0;
1054
+ overflow: hidden;
1055
+ border-right: 1px solid var(--border);
1056
+ }
1057
+ .log-pane:last-child {
1058
+ border-right: none;
1059
+ }
1060
+ .logs-area.dir-rows .log-pane {
1061
+ border-right: none;
1062
+ border-bottom: 1px solid var(--border);
1063
+ }
1064
+ .logs-area.dir-rows .log-pane:last-child {
1065
+ border-bottom: none;
1066
+ }
1067
+
1068
+ .log-pane-title {
1069
+ font-family: var(--mono);
1070
+ font-size: 11px;
1071
+ font-weight: 600;
1072
+ color: var(--text);
1073
+ letter-spacing: 0;
1074
+ }
1075
+
1076
+ .log-output {
1077
+ flex: 1;
1078
+ overflow-y: auto;
1079
+ padding: 8px 12px;
1080
+ font-family: var(--mono);
1081
+ font-size: 11px;
1082
+ line-height: 1.55;
1083
+ white-space: pre-wrap;
1084
+ word-break: break-all;
1085
+ min-height: 0;
1086
+ }
1087
+ .log-output .log-cmd {
1088
+ color: var(--accent);
1089
+ }
1090
+ .log-output .log-err {
1091
+ color: var(--red);
1092
+ }
1093
+ .log-output .log-done {
1094
+ color: var(--muted);
1095
+ font-style: italic;
1096
+ }
1097
+
1098
+ .aws-auth-banner {
1099
+ display: flex;
1100
+ flex-wrap: wrap;
1101
+ align-items: center;
1102
+ gap: 8px;
1103
+ padding: 8px 12px;
1104
+ background: color-mix(in srgb, var(--yellow, #d97706) 12%, var(--bg));
1105
+ border-top: 1px solid
1106
+ color-mix(in srgb, var(--yellow, #d97706) 40%, transparent);
1107
+ }
1108
+ .aws-auth-msg {
1109
+ font-size: 12px;
1110
+ color: var(--yellow, #d97706);
1111
+ flex: 1;
1112
+ }
1113
+ .aws-auth-btn {
1114
+ font-size: 11px;
1115
+ padding: 3px 10px;
1116
+ border-radius: 4px;
1117
+ cursor: pointer;
1118
+ background: var(--yellow, #d97706);
1119
+ color: #000;
1120
+ border: none;
1121
+ font-weight: 600;
1122
+ }
1123
+ .aws-auth-btn:disabled {
1124
+ opacity: 0.5;
1125
+ cursor: default;
1126
+ }
1127
+ .aws-auth-output {
1128
+ width: 100%;
1129
+ margin: 0;
1130
+ padding: 6px 8px;
1131
+ font-family: var(--mono);
1132
+ font-size: 11px;
1133
+ line-height: 1.5;
1134
+ background: var(--bg2);
1135
+ border-radius: 4px;
1136
+ white-space: pre-wrap;
1137
+ max-height: 120px;
1138
+ overflow-y: auto;
1139
+ }
1140
+
1141
+ .logs-empty {
1142
+ flex: 1;
1143
+ display: flex;
1144
+ align-items: center;
1145
+ justify-content: center;
1146
+ color: var(--muted);
1147
+ font-size: 12px;
1148
+ }
1149
+
1150
+ /* ── Logs toolbar ──────────────────────────────────────────────────────── */
1151
+ .logs-toolbar {
1152
+ flex-shrink: 0;
1153
+ display: flex;
1154
+ align-items: center;
1155
+ gap: 6px;
1156
+ flex-wrap: wrap;
1157
+ padding: 5px 14px;
1158
+ background: var(--surface);
1159
+ border-bottom: 1px solid var(--border);
1160
+ }
1161
+ .logs-toolbar .logs-label {
1162
+ font-size: 11px;
1163
+ color: var(--muted);
1164
+ margin-right: 2px;
1165
+ }
1166
+ </style>
1167
+ </head>
1168
+ <body>
1169
+ <!-- ── Bar 1: __DEN_CLI__ + health + nav ──────────────────────────────────────── -->
1170
+ <div class="topbar">
1171
+ <img
1172
+ src="/favicon.ico"
1173
+ width="16"
1174
+ height="16"
1175
+ alt=""
1176
+ style="flex-shrink: 0"
1177
+ />
1178
+ <h1>__DEN_CLI__</h1>
1179
+ <button
1180
+ class="v-pill"
1181
+ id="version-pill"
1182
+ onclick="openChangelog()"
1183
+ title="View changelog"
1184
+ >
1185
+ <span class="v-pip" id="v-pip"></span><span class="v-txt">v…</span>
1186
+ </button>
1187
+ <a
1188
+ id="app-link"
1189
+ href="#"
1190
+ target="_blank"
1191
+ rel="noopener"
1192
+ title="Open app"
1193
+ style="
1194
+ font-size: 11px;
1195
+ color: var(--muted);
1196
+ text-decoration: none;
1197
+ display: none;
1198
+ "
1199
+ >↗ open app</a
1200
+ >
1201
+ <span id="topbar-env-badge"></span>
1202
+ <span
1203
+ id="ws-pill"
1204
+ style="
1205
+ font-size: 11px;
1206
+ font-family: var(--mono);
1207
+ padding: 2px 8px;
1208
+ border-radius: 10px;
1209
+ border: 1px solid #388bfd44;
1210
+ color: #58a6ff;
1211
+ background: #58a6ff10;
1212
+ display: none;
1213
+ "
1214
+ ></span>
1215
+ <div class="health" id="health" title="Service health"></div>
1216
+ <div class="spacer"></div>
1217
+ <nav>
1218
+ <a href="/">Local Dev</a>
1219
+ <a href="/services">Services</a>
1220
+ <a href="/environments">Environments</a>
1221
+ <a href="/release">Releases</a>
1222
+ <a href="/logs">Logs <span class="badge" id="log-badge"></span></a>
1223
+ <a href="/tests" id="nav-tests" class="active">Tests</a>
1224
+ </nav>
1225
+ </div>
1226
+
1227
+ <!-- ── Bar 2: env selector (universal) + context actions (right) ──────────── -->
1228
+ <div class="subbar">
1229
+ <div class="env-badge">
1230
+ <select id="env-sel" title="Environment">
1231
+ <option value="local">local</option>
1232
+ </select>
1233
+ </div>
1234
+
1235
+ <!-- Tests context: run status + layout toggle + run button -->
1236
+ <div class="bar2-slot active" id="bar2-tests">
1237
+ <div class="status">
1238
+ <span class="dot" id="run-dot"></span>
1239
+ <span id="run-status">idle</span>
1240
+ </div>
1241
+ <div class="spacer"></div>
1242
+ <div class="seg" id="layout-seg">
1243
+ <button data-layout="top" class="active" title="Controls on top">
1244
+ top
1245
+ </button>
1246
+ <button data-layout="side" title="Controls on left">side</button>
1247
+ </div>
1248
+ <button class="run-btn" id="run-btn">&#9654; Run</button>
1249
+ </div>
1250
+
1251
+ <!-- Services context: exec status -->
1252
+ <div class="bar2-slot" id="bar2-services">
1253
+ <div class="spacer"></div>
1254
+ <span id="svc-status" style="font-size: 11px; color: var(--muted)"
1255
+ >idle</span
1256
+ >
1257
+ </div>
1258
+ </div>
1259
+
1260
+ <!-- ── TESTS VIEW ─────────────────────────────────────────────────────────── -->
1261
+ <div class="view active" id="view-tests">
1262
+ <div class="tests-main layout-top" id="tests-main">
1263
+ <div class="left-panel">
1264
+ <div class="controls">
1265
+ <div class="seg pillars" id="pillars">
1266
+ <button class="active" data-pillar="synthetic">
1267
+ &#127760; browser
1268
+ </button>
1269
+ <button data-pillar="integration">&#128279; api</button>
1270
+ <button data-pillar="smoke">&#128168; smoke</button>
1271
+ <button data-pillar="performance">&#9889; perf</button>
1272
+ <button data-pillar="journeys">&#129517; journeys</button>
1273
+ <button data-pillar="security">&#128737;&#65039; security</button>
1274
+ <button data-pillar="suites">&#129514; suites</button>
1275
+ </div>
1276
+ <div class="module-picker" id="module-picker">
1277
+ <button
1278
+ class="module-picker-btn"
1279
+ id="module-picker-btn"
1280
+ type="button"
1281
+ >
1282
+ <span id="module-picker-label">All modules</span>
1283
+ <span class="module-picker-arrow">▾</span>
1284
+ </button>
1285
+ <div class="module-picker-dropdown" id="module-picker-dropdown">
1286
+ <label class="module-check-item module-check-all">
1287
+ <input type="checkbox" id="module-check-all" checked />
1288
+ <span>All modules</span>
1289
+ </label>
1290
+ <div class="module-check-divider"></div>
1291
+ <div id="module-check-list"></div>
1292
+ </div>
1293
+ </div>
1294
+ <div class="smoke-override" id="smoke-api-url-wrap">
1295
+ <span class="smoke-override-label">api url</span>
1296
+ <input
1297
+ id="smoke-api-url"
1298
+ class="text-input"
1299
+ type="text"
1300
+ placeholder="Optional override; blank uses repo/env default"
1301
+ spellcheck="false"
1302
+ />
1303
+ </div>
1304
+ <label class="smoke-override" id="local-checkout-wrap">
1305
+ <input
1306
+ id="use-local-checkout"
1307
+ class="toggle-input"
1308
+ type="checkbox"
1309
+ />
1310
+ <span class="smoke-override-label">use local checkout</span>
1311
+ </label>
1312
+ <label class="smoke-override" id="security-mode-wrap">
1313
+ <span class="smoke-override-label">scan</span>
1314
+ <select id="security-mode" class="text-input">
1315
+ <option value="baseline">
1316
+ baseline: passive, local + beta
1317
+ </option>
1318
+ <option value="api">api: OpenAPI, active, local only</option>
1319
+ <option value="full">full: active attacks, local only</option>
1320
+ </select>
1321
+ </label>
1322
+ <!-- Per-perf-test optional launch fields, rendered generically from
1323
+ each selected module's declared `optionalFields` (manifest.yml). -->
1324
+ <div id="perf-opt-fields"></div>
1325
+ </div>
1326
+
1327
+ <div class="test-sources-bar" id="test-sources-bar"></div>
1328
+
1329
+ <div class="output-pane">
1330
+ <div class="pane-bar">
1331
+ <span>Output</span>
1332
+ <button id="clear-test-output">Clear</button>
1333
+ <button
1334
+ id="copy-test-output"
1335
+ title="Copy to clipboard"
1336
+ style="padding: 2px 5px; line-height: 1"
1337
+ >
1338
+ <svg
1339
+ width="13"
1340
+ height="13"
1341
+ viewBox="0 0 13 13"
1342
+ fill="none"
1343
+ stroke="currentColor"
1344
+ stroke-width="1.3"
1345
+ stroke-linecap="round"
1346
+ stroke-linejoin="round"
1347
+ >
1348
+ <rect x="4" y="0.5" width="8.5" height="8.5" rx="1.5" />
1349
+ <path
1350
+ d="M8.5 9v2.5a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.5"
1351
+ />
1352
+ </svg>
1353
+ </button>
1354
+ </div>
1355
+ <pre id="test-output"></pre>
1356
+ </div>
1357
+ </div>
1358
+
1359
+ <div class="divider" id="divider"></div>
1360
+
1361
+ <div class="report-pane">
1362
+ <div class="pane-bar">
1363
+ <div class="pane-bar-left">
1364
+ <span id="report-pane-title">Playwright Report</span>
1365
+ <div class="seg report-tabs" id="report-view-tabs">
1366
+ <button
1367
+ data-report-view="rendered"
1368
+ class="active"
1369
+ type="button"
1370
+ >
1371
+ Rendered Report
1372
+ </button>
1373
+ <button data-report-view="raw" type="button">Raw Output</button>
1374
+ </div>
1375
+ </div>
1376
+ <button id="open-report-tab">Open in new tab &#8599;</button>
1377
+ </div>
1378
+ <div id="report-placeholder">
1379
+ Run tests to see the Playwright report
1380
+ </div>
1381
+ <div id="perf-report-container"></div>
1382
+ <pre id="report-raw-output"></pre>
1383
+ <iframe id="report-frame" title="Playwright Report"></iframe>
1384
+ </div>
1385
+ </div>
1386
+ </div>
1387
+
1388
+ <!-- ── SERVICES VIEW ──────────────────────────────────────────────────────── -->
1389
+ <div class="view" id="view-services">
1390
+ <div class="services-pane" id="services-pane">
1391
+ <div class="empty-state">Loading services&#8230;</div>
1392
+ </div>
1393
+
1394
+ <!-- Command output strip — shown when an exec command is running -->
1395
+ <div class="cmd-strip hidden" id="cmd-strip">
1396
+ <div class="pane-bar">
1397
+ <span id="cmd-strip-title">Command Output</span>
1398
+ <button id="stop-cmd" style="display: none">Stop</button>
1399
+ <button id="clear-cmd-output">Clear</button>
1400
+ </div>
1401
+ <pre id="cmd-output"></pre>
1402
+ </div>
1403
+ </div>
1404
+
1405
+ <script type="module">
1406
+ import {
1407
+ initHealth,
1408
+ refreshHealth,
1409
+ initWorkspacePill,
1410
+ applyDenConfig,
1411
+ initLogBadge,
1412
+ storeOpenLog,
1413
+ initChangelog,
1414
+ copyToClipboard,
1415
+ } from "/header.js";
1416
+ import {
1417
+ buildReportPaneModel,
1418
+ getNextReportViewOnPillarChange,
1419
+ renderPerfReportHtml,
1420
+ } from "/perf-report.js";
1421
+
1422
+ // ── Module-level state ────────────────────────────────────────────────────
1423
+ let currentView = "tests";
1424
+ let pillar = "synthetic";
1425
+ let testEs = null;
1426
+ let reportView = "playwright";
1427
+ let latestPerfReportUrl = null;
1428
+
1429
+ let execEs = null;
1430
+
1431
+ // Release any open SSE socket on reload / navigation / bfcache-enter so the
1432
+ // reloaded page isn't starved of a connection by a lingering stream.
1433
+ window.addEventListener("pagehide", () => {
1434
+ try {
1435
+ testEs?.close();
1436
+ } catch {}
1437
+ testEs = null;
1438
+ try {
1439
+ execEs?.close();
1440
+ } catch {}
1441
+ execEs = null;
1442
+ });
1443
+
1444
+ // ── Init ──────────────────────────────────────────────────────────────────
1445
+ async function init() {
1446
+ // Populate env selector
1447
+ const envs = await fetch("/api/environments")
1448
+ .then((r) => r.json())
1449
+ .catch(() => ["local"]);
1450
+ const envSel = document.getElementById("env-sel");
1451
+ envSel.innerHTML = envs
1452
+ .map((e) => `<option value="${e}">${e}</option>`)
1453
+ .join("");
1454
+ const savedEnv = localStorage.getItem("__DEN_CLI__-env");
1455
+ if (savedEnv && envs.includes(savedEnv)) envSel.value = savedEnv;
1456
+ updateAppLink(envSel.value);
1457
+
1458
+ // Restore pillar
1459
+ const savedPillar =
1460
+ localStorage.getItem("__DEN_CLI__-pillar") || "synthetic";
1461
+ pillar = savedPillar;
1462
+ document
1463
+ .querySelectorAll("[data-pillar]")
1464
+ .forEach((b) =>
1465
+ b.classList.toggle("active", b.dataset.pillar === pillar),
1466
+ );
1467
+ document.getElementById("smoke-api-url").value =
1468
+ localStorage.getItem("__DEN_CLI__-smoke-api-url") || "";
1469
+ document.getElementById("use-local-checkout").checked =
1470
+ localStorage.getItem("__DEN_CLI__-use-local-checkout") === "1";
1471
+ syncTargetingControls();
1472
+ resetReportPane();
1473
+ if (pillar === "security") renderSecurityFindings();
1474
+
1475
+ // Restore layout
1476
+ const savedLayout = localStorage.getItem("__DEN_CLI__-layout") || "top";
1477
+ applyLayout(savedLayout);
1478
+
1479
+ // Load data
1480
+ await Promise.all([
1481
+ loadModules(pillar),
1482
+ initHealth(),
1483
+ initWorkspacePill("env-sel", { onSwitch: onWorkspaceSwitch }),
1484
+ applyDenConfig(),
1485
+ initChangelog(),
1486
+ ]);
1487
+ initLogBadge();
1488
+
1489
+ // Route to the correct view
1490
+ routeFromHash();
1491
+ }
1492
+
1493
+ // ── Workspace ─────────────────────────────────────────────────────────────
1494
+ // Module discovery, test sources and the run itself all resolve repos from
1495
+ // the FOCUSED workspace server-side (discoverRepos → getActiveWorkspace), so
1496
+ // switching focus has to re-pull everything this page derives from it.
1497
+ async function onWorkspaceSwitch() {
1498
+ await loadModules(pillar);
1499
+ refreshHealth(document.getElementById("env-sel").value, {
1500
+ force: true,
1501
+ });
1502
+ if (currentView === "services") loadServices();
1503
+ }
1504
+
1505
+ // ── Routing ───────────────────────────────────────────────────────────────
1506
+ window.addEventListener("hashchange", routeFromHash);
1507
+
1508
+ function routeFromHash() {
1509
+ const hash = location.hash.replace("#", "") || "tests";
1510
+ showView(["tests", "services"].includes(hash) ? hash : "tests");
1511
+ }
1512
+
1513
+ function showView(view) {
1514
+ currentView = view;
1515
+
1516
+ document
1517
+ .querySelectorAll(".view")
1518
+ .forEach((el) =>
1519
+ el.classList.toggle("active", el.id === `view-${view}`),
1520
+ );
1521
+ document
1522
+ .querySelectorAll(".bar2-slot")
1523
+ .forEach((el) =>
1524
+ el.classList.toggle("active", el.id === `bar2-${view}`),
1525
+ );
1526
+
1527
+ if (view === "services") loadServices();
1528
+ }
1529
+
1530
+ // ── App link ─────────────────────────────────────────────────────────────
1531
+ async function updateAppLink(env) {
1532
+ const link = document.getElementById("app-link");
1533
+ const badge = document.getElementById("topbar-env-badge");
1534
+ if (!link) return;
1535
+ try {
1536
+ const { url } = await fetch(`/api/app-url?env=${env}`).then((r) =>
1537
+ r.json(),
1538
+ );
1539
+ if (url) {
1540
+ link.href = url;
1541
+ link.style.display = "";
1542
+ link.title = url;
1543
+ if (badge) {
1544
+ badge.textContent = env;
1545
+ badge.style.display = "inline-flex";
1546
+ }
1547
+ } else {
1548
+ link.style.display = "none";
1549
+ if (badge) badge.style.display = "none";
1550
+ }
1551
+ } catch {
1552
+ link.style.display = "none";
1553
+ if (badge) badge.style.display = "none";
1554
+ }
1555
+ }
1556
+
1557
+ // ── Env ───────────────────────────────────────────────────────────────────
1558
+ document.getElementById("env-sel").addEventListener("change", () => {
1559
+ const env = document.getElementById("env-sel").value;
1560
+ localStorage.setItem("__DEN_CLI__-env", env);
1561
+ updateAppLink(env);
1562
+
1563
+ // Update health dots for the new env immediately
1564
+ refreshHealth(env);
1565
+
1566
+ // Reload context-sensitive views
1567
+ if (currentView === "services") loadServices();
1568
+ if (pillar === "security") renderSecurityFindings();
1569
+ });
1570
+
1571
+ // ── Layout (tests view) ───────────────────────────────────────────────────
1572
+ document.getElementById("layout-seg").addEventListener("click", (e) => {
1573
+ const btn = e.target.closest("[data-layout]");
1574
+ if (!btn) return;
1575
+ applyLayout(btn.dataset.layout);
1576
+ localStorage.setItem("__DEN_CLI__-layout", btn.dataset.layout);
1577
+ });
1578
+
1579
+ function applyLayout(layout) {
1580
+ const main = document.getElementById("tests-main");
1581
+ main.className = `tests-main layout-${layout}`;
1582
+ document
1583
+ .querySelectorAll("#layout-seg [data-layout]")
1584
+ .forEach((b) =>
1585
+ b.classList.toggle("active", b.dataset.layout === layout),
1586
+ );
1587
+ const savedSize = localStorage.getItem(`__DEN_CLI__-size-${layout}`);
1588
+ document.querySelector(".left-panel").style.flex = savedSize
1589
+ ? `0 0 ${savedSize}px`
1590
+ : "";
1591
+ }
1592
+
1593
+ // ── Draggable divider ─────────────────────────────────────────────────────
1594
+ document.getElementById("divider").addEventListener("mousedown", (e) => {
1595
+ e.preventDefault();
1596
+ const main = document.getElementById("tests-main");
1597
+ const leftPanel = document.querySelector(".left-panel");
1598
+ const isHoriz = main.classList.contains("layout-side");
1599
+ const startPos = isHoriz ? e.clientX : e.clientY;
1600
+ const startSize = isHoriz
1601
+ ? leftPanel.getBoundingClientRect().width
1602
+ : leftPanel.getBoundingClientRect().height;
1603
+ const MIN = 160;
1604
+
1605
+ document.getElementById("divider").classList.add("active");
1606
+ document.body.classList.add(isHoriz ? "dragging" : "dragging-v");
1607
+
1608
+ function onMove(ev) {
1609
+ const delta = (isHoriz ? ev.clientX : ev.clientY) - startPos;
1610
+ const mainSize = isHoriz
1611
+ ? main.getBoundingClientRect().width
1612
+ : main.getBoundingClientRect().height;
1613
+ leftPanel.style.flex = `0 0 ${Math.min(Math.max(MIN, startSize + delta), mainSize - MIN)}px`;
1614
+ }
1615
+
1616
+ function onUp() {
1617
+ document.removeEventListener("mousemove", onMove);
1618
+ document.removeEventListener("mouseup", onUp);
1619
+ document.getElementById("divider").classList.remove("active");
1620
+ document.body.classList.remove("dragging", "dragging-v");
1621
+ const sz = isHoriz
1622
+ ? leftPanel.getBoundingClientRect().width
1623
+ : leftPanel.getBoundingClientRect().height;
1624
+ localStorage.setItem(
1625
+ `__DEN_CLI__-size-${isHoriz ? "side" : "top"}`,
1626
+ Math.round(sz),
1627
+ );
1628
+ }
1629
+
1630
+ document.addEventListener("mousemove", onMove);
1631
+ document.addEventListener("mouseup", onUp);
1632
+ });
1633
+
1634
+ // ── Pillar / module ───────────────────────────────────────────────────────
1635
+ document
1636
+ .getElementById("pillars")
1637
+ .addEventListener("click", async (e) => {
1638
+ const btn = e.target.closest("[data-pillar]");
1639
+ if (!btn) return;
1640
+ const nextPillar = btn.dataset.pillar;
1641
+ document
1642
+ .querySelectorAll("[data-pillar]")
1643
+ .forEach((b) => b.classList.remove("active"));
1644
+ btn.classList.add("active");
1645
+ reportView = getNextReportViewOnPillarChange({
1646
+ currentPillar: pillar,
1647
+ nextPillar,
1648
+ currentReportView: reportView,
1649
+ });
1650
+ const leavingSecurity = pillar === "security";
1651
+ pillar = nextPillar;
1652
+ localStorage.setItem("__DEN_CLI__-pillar", pillar);
1653
+ syncTargetingControls();
1654
+ // The findings table shares the perf container: clear it on the way in
1655
+ // or out of security; other pillar switches keep their report.
1656
+ if (leavingSecurity || pillar === "security") resetReportPane();
1657
+ else syncReportPane();
1658
+ if (pillar === "security") renderSecurityFindings();
1659
+ await loadModules(pillar);
1660
+ });
1661
+
1662
+ function syncTargetingControls() {
1663
+ document
1664
+ .getElementById("smoke-api-url-wrap")
1665
+ .classList.toggle("active", pillar === "smoke");
1666
+ document
1667
+ .getElementById("local-checkout-wrap")
1668
+ .classList.toggle(
1669
+ "active",
1670
+ pillar === "smoke" || pillar === "performance",
1671
+ );
1672
+ document
1673
+ .getElementById("security-mode-wrap")
1674
+ .classList.toggle("active", pillar === "security");
1675
+ // Perf optional fields render only for the performance pillar; clear them
1676
+ // for every other pillar so stale controls never linger.
1677
+ if (pillar !== "performance") {
1678
+ document.getElementById("perf-opt-fields").replaceChildren();
1679
+ } else {
1680
+ renderPerfOptionalFields();
1681
+ }
1682
+ }
1683
+
1684
+ function appendSpan(parent, cls, text) {
1685
+ const el = document.createElement("span");
1686
+ if (cls) el.className = cls;
1687
+ el.textContent = text;
1688
+ parent.appendChild(el);
1689
+ }
1690
+
1691
+ async function loadTestSources(p) {
1692
+ const bar = document.getElementById("test-sources-bar");
1693
+ bar.replaceChildren();
1694
+ if (p !== "synthetic" && p !== "smoke") {
1695
+ bar.classList.remove("visible");
1696
+ return;
1697
+ }
1698
+ let sources = [];
1699
+ try {
1700
+ sources = await fetch(`/api/test-sources?pillar=${p}`).then((r) =>
1701
+ r.json(),
1702
+ );
1703
+ } catch {
1704
+ sources = [];
1705
+ }
1706
+ bar.classList.add("visible");
1707
+ if (!Array.isArray(sources) || sources.length === 0) {
1708
+ const empty = document.createElement("div");
1709
+ empty.className = "test-source test-source-empty";
1710
+ empty.textContent =
1711
+ "No source repo detected for this pillar — add one to your workspace or register-repos.";
1712
+ bar.appendChild(empty);
1713
+ return;
1714
+ }
1715
+ for (const s of sources) {
1716
+ const row = document.createElement("div");
1717
+ row.className = "test-source";
1718
+ if (s.repoPath) row.title = s.repoPath;
1719
+ appendSpan(row, "test-source-label", "source:");
1720
+ appendSpan(row, "test-source-repo", s.repoName ?? "(unknown)");
1721
+ if (s.workspace)
1722
+ appendSpan(row, "test-source-ws", `@ ${s.workspace}`);
1723
+ appendSpan(row, "test-source-label", "·");
1724
+ appendSpan(row, "test-source-branch", s.branch ?? "(no branch)");
1725
+ if (s.shortSha) appendSpan(row, "test-source-sha", `(${s.shortSha})`);
1726
+ if (s.repoPath)
1727
+ appendSpan(row, "test-source-path", `— ${s.repoPath}`);
1728
+ bar.appendChild(row);
1729
+ }
1730
+ }
1731
+
1732
+ // Most-recent /api/modules payload for the current pillar, keyed by name.
1733
+ // Used by the generic perf optional-field renderer to look up each selected
1734
+ // module's declared `optionalFields`.
1735
+ let currentModulesByName = {};
1736
+
1737
+ async function loadModules(p) {
1738
+ loadTestSources(p);
1739
+ const modules = await fetch(`/api/modules?pillar=${p}`)
1740
+ .then((r) => r.json())
1741
+ .catch(() => []);
1742
+ currentModulesByName = {};
1743
+ for (const m of modules) currentModulesByName[m.name] = m;
1744
+ const list = document.getElementById("module-check-list");
1745
+ list.innerHTML = modules
1746
+ .map(
1747
+ (m) =>
1748
+ `<label class="module-check-item">
1749
+ <input type="checkbox" value="${m.name}" checked>
1750
+ <span>${m.icon} ${m.displayName}${m.testCount ? ` <span style="color:var(--muted)">(${m.testCount})</span>` : ""}</span>
1751
+ </label>`,
1752
+ )
1753
+ .join("");
1754
+
1755
+ // Restore saved selection. Ignore it when it names none of the modules
1756
+ // now on offer — after a workspace switch the saved names belong to the
1757
+ // previous workspace's repos, and applying them would silently uncheck
1758
+ // everything (which then runs the whole suite).
1759
+ const saved = localStorage.getItem(`__DEN_CLI__-modules-${p}`);
1760
+ if (saved) {
1761
+ try {
1762
+ const sel = JSON.parse(saved);
1763
+ const boxes = [...list.querySelectorAll("input[type=checkbox]")];
1764
+ if (
1765
+ Array.isArray(sel) &&
1766
+ sel.length > 0 &&
1767
+ boxes.some((cb) => sel.includes(cb.value))
1768
+ ) {
1769
+ boxes.forEach((cb) => {
1770
+ cb.checked = sel.includes(cb.value);
1771
+ });
1772
+ }
1773
+ } catch {}
1774
+ }
1775
+ syncAllCheckbox();
1776
+ updatePickerLabel();
1777
+ if (p === "performance") renderPerfOptionalFields();
1778
+ }
1779
+
1780
+ function getSelectedModules() {
1781
+ return [
1782
+ ...document.querySelectorAll(
1783
+ "#module-check-list input[type=checkbox]:checked",
1784
+ ),
1785
+ ].map((cb) => cb.value);
1786
+ }
1787
+
1788
+ function syncAllCheckbox() {
1789
+ const all = document.querySelectorAll(
1790
+ "#module-check-list input[type=checkbox]",
1791
+ );
1792
+ const chk = document.querySelectorAll(
1793
+ "#module-check-list input[type=checkbox]:checked",
1794
+ );
1795
+ document.getElementById("module-check-all").checked =
1796
+ all.length === chk.length;
1797
+ }
1798
+
1799
+ function updatePickerLabel() {
1800
+ const sel = getSelectedModules();
1801
+ const all = document.querySelectorAll(
1802
+ "#module-check-list input[type=checkbox]",
1803
+ ).length;
1804
+ const label = document.getElementById("module-picker-label");
1805
+ if (sel.length === 0 || sel.length === all) {
1806
+ label.textContent = "All modules";
1807
+ } else if (sel.length === 1) {
1808
+ label.textContent = sel[0];
1809
+ } else {
1810
+ label.textContent = `${sel.length} modules`;
1811
+ }
1812
+ localStorage.setItem(
1813
+ `__DEN_CLI__-modules-${pillar}`,
1814
+ JSON.stringify(sel.length === all ? [] : sel),
1815
+ );
1816
+ }
1817
+
1818
+ // "All" checkbox toggles every item
1819
+ document
1820
+ .getElementById("module-check-all")
1821
+ .addEventListener("change", (e) => {
1822
+ document
1823
+ .querySelectorAll("#module-check-list input[type=checkbox]")
1824
+ .forEach((cb) => {
1825
+ cb.checked = e.target.checked;
1826
+ });
1827
+ updatePickerLabel();
1828
+ if (pillar === "performance") renderPerfOptionalFields();
1829
+ });
1830
+
1831
+ // Individual checkbox → sync "All" + label
1832
+ document
1833
+ .getElementById("module-check-list")
1834
+ .addEventListener("change", () => {
1835
+ syncAllCheckbox();
1836
+ updatePickerLabel();
1837
+ if (pillar === "performance") renderPerfOptionalFields();
1838
+ });
1839
+
1840
+ // Toggle dropdown open/close
1841
+ const pickerBtn = document.getElementById("module-picker-btn");
1842
+ const pickerDropdown = document.getElementById("module-picker-dropdown");
1843
+ pickerBtn.addEventListener("click", (e) => {
1844
+ e.stopPropagation();
1845
+ const open = pickerDropdown.classList.toggle("open");
1846
+ pickerBtn.classList.toggle("open", open);
1847
+ });
1848
+ document.addEventListener("click", () => {
1849
+ pickerDropdown.classList.remove("open");
1850
+ pickerBtn.classList.remove("open");
1851
+ });
1852
+ pickerDropdown.addEventListener("click", (e) => e.stopPropagation());
1853
+ document
1854
+ .getElementById("smoke-api-url")
1855
+ .addEventListener("input", (e) => {
1856
+ localStorage.setItem("__DEN_CLI__-smoke-api-url", e.target.value);
1857
+ });
1858
+ document
1859
+ .getElementById("use-local-checkout")
1860
+ .addEventListener("change", (e) => {
1861
+ localStorage.setItem(
1862
+ "__DEN_CLI__-use-local-checkout",
1863
+ e.target.checked ? "1" : "0",
1864
+ );
1865
+ });
1866
+
1867
+ // ── Per-perf-test optional launch fields (declarative) ────────────────────
1868
+ // Each perf module declares `optionalFields` in its manifest.yml. We render
1869
+ // one section per SELECTED module that has fields, scoped per test so two
1870
+ // modules can both declare e.g. a `smoke` field without colliding. Values
1871
+ // persist in localStorage keyed by module+field and serialize into `opts`.
1872
+
1873
+ const perfOptKey = (mod, field) => `__DEN_CLI__-perf-opt:${mod}:${field}`;
1874
+
1875
+ // Effective current value for a field (stored override, else its default).
1876
+ function perfFieldValue(mod, f) {
1877
+ const stored = localStorage.getItem(perfOptKey(mod, f.name));
1878
+ if (stored !== null) {
1879
+ if (f.type === "boolean") return stored === "1" || stored === "true";
1880
+ if (f.type === "number") return stored === "" ? "" : Number(stored);
1881
+ return stored;
1882
+ }
1883
+ return f.default !== undefined
1884
+ ? f.default
1885
+ : f.type === "boolean"
1886
+ ? false
1887
+ : "";
1888
+ }
1889
+
1890
+ function perfFieldIsDisabled(mod, f, fields) {
1891
+ if (!f.disabledWhen) return false;
1892
+ const ctrl = fields.find((x) => x.name === f.disabledWhen.field);
1893
+ if (!ctrl) return false;
1894
+ const v = perfFieldValue(mod, ctrl);
1895
+ if (typeof f.disabledWhen.equals === "boolean") {
1896
+ const truthy = v === true || v === 1 || v === "1" || v === "true";
1897
+ return truthy === f.disabledWhen.equals;
1898
+ }
1899
+ return String(v) === String(f.disabledWhen.equals);
1900
+ }
1901
+
1902
+ function renderPerfOptionalFields() {
1903
+ const container = document.getElementById("perf-opt-fields");
1904
+ container.replaceChildren();
1905
+ if (pillar !== "performance") return;
1906
+
1907
+ const selected = getSelectedModules();
1908
+ const allMods = document.querySelectorAll(
1909
+ "#module-check-list input[type=checkbox]",
1910
+ ).length;
1911
+ const isAll = selected.length === 0 || selected.length === allMods;
1912
+ // "All modules" → every perf module; otherwise just the selected ones.
1913
+ const names = isAll ? Object.keys(currentModulesByName) : selected;
1914
+ const withFields = names
1915
+ .map((n) => currentModulesByName[n])
1916
+ .filter(
1917
+ (m) =>
1918
+ m &&
1919
+ Array.isArray(m.optionalFields) &&
1920
+ m.optionalFields.length > 0,
1921
+ );
1922
+
1923
+ const multi = withFields.length > 1;
1924
+ for (const m of withFields) {
1925
+ const section = document.createElement("div");
1926
+ section.className = "perf-opt-section";
1927
+ if (multi) {
1928
+ const head = document.createElement("div");
1929
+ head.className = "smoke-override-label perf-opt-section-head";
1930
+ head.textContent = `${m.icon || ""} ${m.displayName}`.trim();
1931
+ section.appendChild(head);
1932
+ }
1933
+ for (const f of m.optionalFields) {
1934
+ section.appendChild(renderPerfField(m.name, f, m.optionalFields));
1935
+ }
1936
+ container.appendChild(section);
1937
+ }
1938
+ }
1939
+
1940
+ function renderPerfField(mod, f, fields) {
1941
+ const disabled = perfFieldIsDisabled(mod, f, fields);
1942
+ const val = perfFieldValue(mod, f);
1943
+
1944
+ const wrap = document.createElement("label");
1945
+ wrap.className = "smoke-override active perf-opt-field";
1946
+ if (f.description) wrap.title = f.description;
1947
+ wrap.classList.toggle("perf-opt-disabled", disabled);
1948
+
1949
+ const onChange = () => {
1950
+ renderPerfOptionalFields();
1951
+ };
1952
+
1953
+ if (f.type === "boolean") {
1954
+ const input = document.createElement("input");
1955
+ input.type = "checkbox";
1956
+ input.className = "toggle-input";
1957
+ input.checked =
1958
+ val === true || val === 1 || val === "1" || val === "true";
1959
+ input.disabled = disabled;
1960
+ input.addEventListener("change", () => {
1961
+ localStorage.setItem(
1962
+ perfOptKey(mod, f.name),
1963
+ input.checked ? "1" : "0",
1964
+ );
1965
+ onChange();
1966
+ });
1967
+ wrap.appendChild(input);
1968
+ const span = document.createElement("span");
1969
+ span.className = "smoke-override-label";
1970
+ span.textContent = f.label;
1971
+ wrap.appendChild(span);
1972
+ return wrap;
1973
+ }
1974
+
1975
+ // Non-boolean → label text then control
1976
+ const span = document.createElement("span");
1977
+ span.className = "smoke-override-label";
1978
+ span.textContent = f.label;
1979
+ wrap.appendChild(span);
1980
+
1981
+ if (f.type === "select") {
1982
+ const sel = document.createElement("select");
1983
+ sel.className = "text-input";
1984
+ sel.disabled = disabled;
1985
+ for (const opt of f.options || []) {
1986
+ const o = document.createElement("option");
1987
+ o.value = opt.value;
1988
+ o.textContent = opt.label;
1989
+ if (String(opt.value) === String(val)) o.selected = true;
1990
+ sel.appendChild(o);
1991
+ }
1992
+ sel.addEventListener("change", () => {
1993
+ localStorage.setItem(perfOptKey(mod, f.name), sel.value);
1994
+ onChange();
1995
+ });
1996
+ wrap.appendChild(sel);
1997
+ } else {
1998
+ const input = document.createElement("input");
1999
+ input.className = "text-input";
2000
+ input.type = f.type === "number" ? "number" : "text";
2001
+ input.value = val === undefined || val === null ? "" : String(val);
2002
+ input.disabled = disabled;
2003
+ input.addEventListener("input", () => {
2004
+ localStorage.setItem(perfOptKey(mod, f.name), input.value);
2005
+ });
2006
+ input.addEventListener("change", onChange);
2007
+ wrap.appendChild(input);
2008
+ }
2009
+ return wrap;
2010
+ }
2011
+
2012
+ // Build the `opts` payload: { [module]: { [field]: value } } for selected
2013
+ // perf modules. Disabled fields are omitted (server also skips them).
2014
+ function buildPerfOpts() {
2015
+ const selected = getSelectedModules();
2016
+ const allMods = document.querySelectorAll(
2017
+ "#module-check-list input[type=checkbox]",
2018
+ ).length;
2019
+ const isAll = selected.length === 0 || selected.length === allMods;
2020
+ const names = isAll ? Object.keys(currentModulesByName) : selected;
2021
+ const opts = {};
2022
+ for (const n of names) {
2023
+ const m = currentModulesByName[n];
2024
+ if (
2025
+ !m ||
2026
+ !Array.isArray(m.optionalFields) ||
2027
+ m.optionalFields.length === 0
2028
+ )
2029
+ continue;
2030
+ const fieldVals = {};
2031
+ for (const f of m.optionalFields) {
2032
+ if (perfFieldIsDisabled(n, f, m.optionalFields)) continue;
2033
+ fieldVals[f.name] = perfFieldValue(n, f);
2034
+ }
2035
+ if (Object.keys(fieldVals).length > 0) opts[n] = fieldVals;
2036
+ }
2037
+ return opts;
2038
+ }
2039
+
2040
+ // ── Run tests ─────────────────────────────────────────────────────────────
2041
+ document.getElementById("run-btn").addEventListener("click", startRun);
2042
+
2043
+ function startRun() {
2044
+ if (testEs) {
2045
+ testEs.close();
2046
+ testEs = null;
2047
+ }
2048
+
2049
+ const selected = getSelectedModules();
2050
+ const allMods = document.querySelectorAll(
2051
+ "#module-check-list input[type=checkbox]",
2052
+ ).length;
2053
+ const isAll = selected.length === 0 || selected.length === allMods;
2054
+ const env = document.getElementById("env-sel").value;
2055
+ const params = new URLSearchParams({ pillar, env });
2056
+ if (!isAll) selected.forEach((m) => params.append("module", m));
2057
+ const smokeApiUrl = document
2058
+ .getElementById("smoke-api-url")
2059
+ .value.trim();
2060
+ if (pillar === "smoke" && smokeApiUrl)
2061
+ params.set("baseApiUrl", smokeApiUrl);
2062
+ if (
2063
+ (pillar === "smoke" || pillar === "performance") &&
2064
+ document.getElementById("use-local-checkout").checked
2065
+ ) {
2066
+ params.set("useLocalCheckout", "1");
2067
+ }
2068
+ if (pillar === "security")
2069
+ params.set("mode", document.getElementById("security-mode").value);
2070
+ // Per-perf-test declarative optional fields → opts={module:{field:value}}.
2071
+ let optLabel = "";
2072
+ if (pillar === "performance") {
2073
+ const opts = buildPerfOpts();
2074
+ if (Object.keys(opts).length > 0) {
2075
+ params.set("opts", JSON.stringify(opts));
2076
+ // Compact summary of the fields set across selected tests.
2077
+ const fieldNames = [
2078
+ ...new Set(Object.values(opts).flatMap((o) => Object.keys(o))),
2079
+ ];
2080
+ if (fieldNames.length > 0) optLabel = ` (${fieldNames.join(", ")})`;
2081
+ }
2082
+ }
2083
+
2084
+ const label =
2085
+ (isAll
2086
+ ? ""
2087
+ : selected.length === 1
2088
+ ? selected[0]
2089
+ : `${selected.length} modules`) + optLabel;
2090
+ setRunStatus("running", "running\u2026");
2091
+ document.getElementById("run-btn").disabled = true;
2092
+ resetReportPane();
2093
+ testAppendLine(
2094
+ `\n\u25b6 ${pillar}${label ? " / " + label : ""} \u00b7 ${env}\n`,
2095
+ "cmd",
2096
+ );
2097
+
2098
+ testEs = new EventSource(`/api/run?${params}`);
2099
+ testEs.onmessage = (e) => {
2100
+ const msg = JSON.parse(e.data);
2101
+ if (msg.type === "cmd") {
2102
+ testAppendLine(msg.text, "cmd");
2103
+ } else if (msg.type === "line") {
2104
+ testAppendLine(msg.text, "");
2105
+ } else if (msg.type === "error") {
2106
+ testAppendLine(msg.text, "err");
2107
+ } else if (msg.type === "done") {
2108
+ const ok = msg.exitCode === 0;
2109
+ const dur = fmtDuration(msg.duration);
2110
+ testAppendLine(
2111
+ `\n${ok ? "\u2713 Passed" : "\u2717 Failed"} in ${dur}\n`,
2112
+ ok ? "done-ok" : "done-fail",
2113
+ );
2114
+ setRunStatus(
2115
+ ok ? "passed" : "failed",
2116
+ ok ? `passed (${dur})` : `failed (${dur})`,
2117
+ );
2118
+ document.getElementById("run-btn").disabled = false;
2119
+ testEs.close();
2120
+ testEs = null;
2121
+ if (pillar === "performance") {
2122
+ if (msg.perfReport?.available && msg.perfReport.url) {
2123
+ showPerfReport(msg.perfReport.url);
2124
+ } else {
2125
+ latestPerfReportUrl = null;
2126
+ reportView = "raw";
2127
+ syncReportPane(
2128
+ "Structured perf report unavailable. Showing raw output instead.",
2129
+ );
2130
+ }
2131
+ } else if (
2132
+ pillar !== "journeys" &&
2133
+ pillar !== "security" &&
2134
+ pillar !== "suites"
2135
+ ) {
2136
+ // Journeys, security (ZAP) and suites have no Playwright report — the raw-output pane
2137
+ // (already live during the run) stays in place.
2138
+ showPlaywrightReport();
2139
+ }
2140
+ }
2141
+ };
2142
+ testEs.onerror = () => {
2143
+ testAppendLine("Connection closed", "err");
2144
+ setRunStatus("failed", "error");
2145
+ document.getElementById("run-btn").disabled = false;
2146
+ testEs?.close();
2147
+ testEs = null;
2148
+ };
2149
+ }
2150
+
2151
+ const _progressSpans = {};
2152
+ const _k6ProgressRe = /^(\S+)\s+[\[✓✗]/;
2153
+ const _k6RunningRe = /^running \(\d+m[\d.]+s\), \d+\/\d+ VUs,/;
2154
+
2155
+ function testAppendLine(text, cls) {
2156
+ const out = document.getElementById("test-output");
2157
+ const raw = document.getElementById("report-raw-output");
2158
+
2159
+ // k6 "running (...)" status lines are redundant with the progress bar;
2160
+ // suppress them entirely in the web UI (still in raw output).
2161
+ if (!cls && _k6RunningRe.test(text)) {
2162
+ raw.textContent += text + "\n";
2163
+ return;
2164
+ }
2165
+
2166
+ // k6 progress/result lines: update a single DOM element per scenario
2167
+ // so the UI shows a compact live-updating progress bar.
2168
+ const pm = !cls && text.match(_k6ProgressRe);
2169
+ if (pm) {
2170
+ const key = pm[1];
2171
+ if (_progressSpans[key]) {
2172
+ _progressSpans[key].textContent = text + "\n";
2173
+ out.scrollTop = out.scrollHeight;
2174
+ return;
2175
+ }
2176
+ const span = document.createElement("span");
2177
+ span.className = "k6-progress";
2178
+ span.textContent = text + "\n";
2179
+ _progressSpans[key] = span;
2180
+ out.appendChild(span);
2181
+ out.scrollTop = out.scrollHeight;
2182
+ return;
2183
+ }
2184
+
2185
+ const span = document.createElement("span");
2186
+ if (cls) span.className = cls;
2187
+ span.textContent = text + "\n";
2188
+ out.appendChild(span);
2189
+ out.scrollTop = out.scrollHeight;
2190
+ raw.textContent += text + "\n";
2191
+ raw.scrollTop = raw.scrollHeight;
2192
+ }
2193
+
2194
+ document
2195
+ .getElementById("copy-test-output")
2196
+ .addEventListener("click", () => {
2197
+ const text = document.getElementById("test-output").innerText;
2198
+ const btn = document.getElementById("copy-test-output");
2199
+ const icon =
2200
+ '<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="0.5" width="8.5" height="8.5" rx="1.5"/><path d="M8.5 9v2.5a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.5"/></svg>';
2201
+ const check =
2202
+ '<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="var(--green)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 7l3 3 6-7"/></svg>';
2203
+ copyToClipboard(text)
2204
+ .then(() => {
2205
+ btn.innerHTML = check;
2206
+ setTimeout(() => {
2207
+ btn.innerHTML = icon;
2208
+ }, 1500);
2209
+ })
2210
+ .catch(() => {});
2211
+ });
2212
+
2213
+ document
2214
+ .getElementById("clear-test-output")
2215
+ .addEventListener("click", () => {
2216
+ document.getElementById("test-output").innerHTML = "";
2217
+ document.getElementById("report-raw-output").textContent = "";
2218
+ Object.keys(_progressSpans).forEach((k) => delete _progressSpans[k]);
2219
+ });
2220
+
2221
+ function setRunStatus(state, text) {
2222
+ document.getElementById("run-dot").className = `dot ${state}`;
2223
+ document.getElementById("run-status").textContent = text;
2224
+ }
2225
+
2226
+ function showPlaywrightReport() {
2227
+ const frame = document.getElementById("report-frame");
2228
+ reportView = "playwright";
2229
+ frame.dataset.ready = "1";
2230
+ frame.src = "/report/";
2231
+ syncReportPane();
2232
+ }
2233
+
2234
+ document
2235
+ .getElementById("open-report-tab")
2236
+ .addEventListener("click", () => {
2237
+ const model = buildReportPaneModel({
2238
+ pillar,
2239
+ selectedView: reportView,
2240
+ perfReportUrl: latestPerfReportUrl,
2241
+ });
2242
+ if (model.openUrl) {
2243
+ window.open(model.openUrl, "_blank");
2244
+ }
2245
+ });
2246
+
2247
+ document
2248
+ .getElementById("report-view-tabs")
2249
+ .addEventListener("click", (e) => {
2250
+ const btn = e.target.closest("[data-report-view]");
2251
+ if (!btn || pillar !== "performance") return;
2252
+ reportView = btn.dataset.reportView;
2253
+ syncReportPane();
2254
+ });
2255
+
2256
+ function resetReportPane() {
2257
+ reportView = pillar === "performance" ? "rendered" : "playwright";
2258
+ latestPerfReportUrl = null;
2259
+ document.getElementById("report-frame").removeAttribute("src");
2260
+ document.getElementById("report-frame").dataset.ready = "0";
2261
+ document.getElementById("perf-report-container").innerHTML = "";
2262
+ document.getElementById("report-raw-output").textContent = "";
2263
+ syncReportPane();
2264
+ }
2265
+
2266
+ function syncReportPane(overrideMessage) {
2267
+ const model = buildReportPaneModel({
2268
+ pillar,
2269
+ selectedView: reportView,
2270
+ perfReportUrl: latestPerfReportUrl,
2271
+ });
2272
+ const frame = document.getElementById("report-frame");
2273
+ const placeholder = document.getElementById("report-placeholder");
2274
+ const perfContainer = document.getElementById("perf-report-container");
2275
+ const rawOutput = document.getElementById("report-raw-output");
2276
+ const tabs = document.getElementById("report-view-tabs");
2277
+ const title = document.getElementById("report-pane-title");
2278
+ const openBtn = document.getElementById("open-report-tab");
2279
+
2280
+ title.textContent = model.title;
2281
+ tabs.classList.toggle("active", model.tabsVisible);
2282
+ tabs.querySelectorAll("[data-report-view]").forEach((btn) => {
2283
+ btn.classList.toggle(
2284
+ "active",
2285
+ btn.dataset.reportView === model.activeView,
2286
+ );
2287
+ });
2288
+ openBtn.style.display = model.openUrl ? "" : "none";
2289
+
2290
+ frame.style.display = "none";
2291
+ perfContainer.style.display = "none";
2292
+ rawOutput.style.display = "none";
2293
+ placeholder.style.display = "none";
2294
+
2295
+ if (model.activeView === "playwright") {
2296
+ if (frame.dataset.ready === "1") {
2297
+ frame.style.display = "block";
2298
+ } else {
2299
+ placeholder.textContent = overrideMessage || model.emptyMessage;
2300
+ placeholder.style.display = "flex";
2301
+ }
2302
+ return;
2303
+ }
2304
+
2305
+ if (model.activeView === "raw") {
2306
+ rawOutput.style.display = "block";
2307
+ if (!rawOutput.textContent.trim()) {
2308
+ rawOutput.textContent =
2309
+ overrideMessage || "Raw performance output will appear here.";
2310
+ }
2311
+ return;
2312
+ }
2313
+
2314
+ if (perfContainer.innerHTML.trim()) {
2315
+ perfContainer.style.display = "block";
2316
+ } else {
2317
+ placeholder.textContent = overrideMessage || model.emptyMessage;
2318
+ placeholder.style.display = "flex";
2319
+ }
2320
+ }
2321
+
2322
+ // ── Security findings (newest ZAP run per scan mode, this env) ───────────
2323
+ async function renderSecurityFindings() {
2324
+ const env = document.getElementById("env-sel").value;
2325
+ const box = document.getElementById("perf-report-container");
2326
+ const esc = (v) =>
2327
+ String(v ?? "").replace(
2328
+ /[&<>"']/g,
2329
+ (c) =>
2330
+ ({
2331
+ "&": "&amp;",
2332
+ "<": "&lt;",
2333
+ ">": "&gt;",
2334
+ '"': "&quot;",
2335
+ "'": "&#39;",
2336
+ })[c],
2337
+ );
2338
+ let runs = [];
2339
+ try {
2340
+ runs = await fetch(
2341
+ `/api/security/latest?env=${encodeURIComponent(env)}`,
2342
+ ).then((r) => r.json());
2343
+ } catch {
2344
+ runs = [];
2345
+ }
2346
+ if (pillar !== "security") return;
2347
+ if (!Array.isArray(runs) || runs.length === 0) {
2348
+ box.innerHTML = "";
2349
+ syncReportPane();
2350
+ return;
2351
+ }
2352
+ const RISKS = ["High", "Medium", "Low", "Informational"];
2353
+ const count = (fs, r) => (fs || []).filter((f) => f.risk === r).length;
2354
+ box.innerHTML = runs
2355
+ .map(
2356
+ (run) => `
2357
+ <section class="sec-run">
2358
+ <h3>${esc(run.mode)} scan · ${esc(run.env)} · ${esc(new Date(run.finishedAt).toLocaleString())}</h3>
2359
+ <table class="sec-table">
2360
+ <thead><tr><th>Service</th>${RISKS.map((r) => `<th>${r === "Informational" ? "Info" : r}</th>`).join("")}<th>New</th><th></th></tr></thead>
2361
+ <tbody>${run.results
2362
+ .map((t) =>
2363
+ t.findings === null
2364
+ ? `<tr><td>${esc(t.id)}</td><td colspan="5" class="sec-err">scan failed: ${esc(t.error)}</td><td></td></tr>`
2365
+ : `<tr>
2366
+ <td title="${esc(t.url)}">${esc(t.id)}</td>
2367
+ ${RISKS.map((r) => {
2368
+ const n = count(t.findings, r);
2369
+ return `<td class="sec-${r.toLowerCase()}${n ? " sec-hit" : ""}">${n}</td>`;
2370
+ }).join("")}
2371
+ <td>${t.newSince?.length || ""}</td>
2372
+ <td>${t.report ? `<a href="/reports/security/${t.report.split("/").map(encodeURIComponent).join("/")}" target="_blank" rel="noopener">report &#8599;</a>` : ""}</td>
2373
+ </tr>
2374
+ ${t.findings
2375
+ .filter((f) => f.risk !== "Informational")
2376
+ .map(
2377
+ (f) => `
2378
+ <tr class="sec-finding"><td colspan="7"><span class="sec-${f.risk.toLowerCase()}">${esc(f.risk)}</span> ${esc(f.name)} (&times;${f.count})${t.newSince?.includes(f.pluginId) ? " <b>new</b>" : ""}</td></tr>`,
2379
+ )
2380
+ .join("")}`,
2381
+ )
2382
+ .join("")}
2383
+ </tbody>
2384
+ </table>
2385
+ </section>`,
2386
+ )
2387
+ .join("");
2388
+ syncReportPane();
2389
+ }
2390
+
2391
+ async function showPerfReport(url) {
2392
+ try {
2393
+ const report = await fetch(url).then((r) => {
2394
+ if (!r.ok)
2395
+ throw new Error(`Failed to load perf report (${r.status})`);
2396
+ return r.json();
2397
+ });
2398
+ latestPerfReportUrl = url;
2399
+ reportView = "rendered";
2400
+ document.getElementById("perf-report-container").innerHTML =
2401
+ renderPerfReportHtml(report);
2402
+ syncReportPane();
2403
+ } catch {
2404
+ latestPerfReportUrl = null;
2405
+ reportView = "raw";
2406
+ syncReportPane(
2407
+ "Structured perf report unavailable. Showing raw output instead.",
2408
+ );
2409
+ }
2410
+ }
2411
+
2412
+ // ── Services view ─────────────────────────────────────────────────────────
2413
+ async function loadServices() {
2414
+ const env = document.getElementById("env-sel").value;
2415
+ const pane = document.getElementById("services-pane");
2416
+ pane.innerHTML = '<div class="empty-state">Loading\u2026</div>';
2417
+
2418
+ let services;
2419
+ try {
2420
+ services = await fetch(`/api/services?env=${env}`).then((r) =>
2421
+ r.json(),
2422
+ );
2423
+ } catch {
2424
+ pane.innerHTML =
2425
+ '<div class="empty-state">Failed to load services</div>';
2426
+ return;
2427
+ }
2428
+
2429
+ if (!services.length) {
2430
+ pane.innerHTML = `<div class="empty-state">No services registered for <strong>${env}</strong>.<br>Add a <code>services</code> block to <code>__DEN_MANIFEST_YML__</code>.</div>`;
2431
+ return;
2432
+ }
2433
+
2434
+ pane.innerHTML = services
2435
+ .map((s) => {
2436
+ const cmdBtns = s.commands
2437
+ .map(
2438
+ (cmd) =>
2439
+ `<button class="cmd-btn" data-repo="${s.repo}" data-service="${s.service}" data-cmd="${cmd}" data-type="exec">${cmd}</button>`,
2440
+ )
2441
+ .join("");
2442
+ const logsBtn = s.logs
2443
+ ? `<button class="cmd-btn logs-btn" data-repo="${s.repo}" data-service="${s.service}" data-type="logs">logs ↗</button>`
2444
+ : "";
2445
+ const hasCaps = s.commands.length > 0 || s.logs;
2446
+ return `
2447
+ <div class="service-card">
2448
+ <div class="service-card-header">
2449
+ <span class="dot" id="svc-dot-${s.repo}-${s.service}"></span>
2450
+ <div>
2451
+ <div class="service-name">${s.service}</div>
2452
+ <div class="service-repo">${s.repo}</div>
2453
+ </div>
2454
+ </div>
2455
+ <div class="cmd-row">
2456
+ ${hasCaps ? cmdBtns + logsBtn : '<span style="color:var(--muted);font-size:11px">no capabilities for this env</span>'}
2457
+ </div>
2458
+ </div>`;
2459
+ })
2460
+ .join("");
2461
+
2462
+ pane.querySelectorAll(".cmd-btn").forEach((btn) => {
2463
+ btn.addEventListener("click", () => {
2464
+ const { repo, service, cmd, type } = btn.dataset;
2465
+ if (type === "exec") runExec(repo, service, cmd);
2466
+ else {
2467
+ const env = document.getElementById("env-sel").value;
2468
+ storeOpenLog(repo, service, env);
2469
+ window.location.href = "/logs";
2470
+ }
2471
+ });
2472
+ });
2473
+ }
2474
+
2475
+ function setSvcDot(repo, service, state) {
2476
+ const el = document.getElementById(`svc-dot-${repo}-${service}`);
2477
+ if (el) el.className = `dot ${state}`;
2478
+ }
2479
+
2480
+ document.getElementById("stop-cmd").addEventListener("click", () => {
2481
+ const sb = document.getElementById("stop-cmd");
2482
+ const detachable = sb.dataset.detachable !== "0";
2483
+ execEs?.close();
2484
+ execEs = null;
2485
+ sb.style.display = "none";
2486
+ document
2487
+ .querySelectorAll('.cmd-btn[data-type="exec"]')
2488
+ .forEach((b) => (b.disabled = false));
2489
+ document.getElementById("svc-status").textContent = "idle";
2490
+ cmdAppendLine(
2491
+ detachable ? "--- Detached ---" : "--- Stopped ---",
2492
+ "err",
2493
+ );
2494
+ });
2495
+
2496
+ // ── Service exec ──────────────────────────────────────────────────────────
2497
+ function runExec(repo, service, cmd) {
2498
+ if (execEs) {
2499
+ execEs.close();
2500
+ execEs = null;
2501
+ }
2502
+ const env = document.getElementById("env-sel").value;
2503
+
2504
+ // Disable exec buttons while running, keep logs buttons intact
2505
+ document
2506
+ .querySelectorAll('.cmd-btn[data-type="exec"]')
2507
+ .forEach((b) => (b.disabled = true));
2508
+ const stopBtn = document.getElementById("stop-cmd");
2509
+ stopBtn.style.display = "";
2510
+ stopBtn.textContent = "Stop";
2511
+ setSvcDot(repo, service, "running");
2512
+
2513
+ const strip = document.getElementById("cmd-strip");
2514
+ strip.classList.remove("hidden");
2515
+ document.getElementById("cmd-strip-title").textContent =
2516
+ `${repo} / ${service} \u00b7 ${cmd}`;
2517
+ document.getElementById("svc-status").textContent =
2518
+ `running ${service}:${cmd}\u2026`;
2519
+
2520
+ cmdAppendLine(
2521
+ `\n\u25b6 ${repo}/${service} \u00b7 ${cmd} \u00b7 ${env}\n`,
2522
+ "cmd",
2523
+ );
2524
+
2525
+ const params = new URLSearchParams({ repo, service, cmd, env });
2526
+ execEs = new EventSource(`/api/service/exec?${params}`);
2527
+ execEs.onmessage = (e) => {
2528
+ const msg = JSON.parse(e.data);
2529
+ if (msg.type === "cmd") {
2530
+ cmdAppendLine(msg.text, "cmd");
2531
+ const sb = document.getElementById("stop-cmd");
2532
+ sb.textContent = msg.detached ? "Detach" : "Stop";
2533
+ sb.dataset.detachable = msg.detached ? "1" : "0";
2534
+ } else if (msg.type === "line") {
2535
+ cmdAppendLine(msg.text, "");
2536
+ } else if (msg.type === "error") {
2537
+ cmdAppendLine(msg.text, "err");
2538
+ } else if (msg.type === "done") {
2539
+ const ok = msg.exitCode === 0;
2540
+ const dur = fmtDuration(msg.duration);
2541
+ cmdAppendLine(
2542
+ `\n${ok ? "\u2713 Done" : "\u2717 Failed"} in ${dur}\n`,
2543
+ ok ? "done-ok" : "done-fail",
2544
+ );
2545
+ setSvcDot(repo, service, ok ? "ok" : "err");
2546
+ document.getElementById("svc-status").textContent = "idle";
2547
+ document.getElementById("stop-cmd").style.display = "none";
2548
+ document
2549
+ .querySelectorAll('.cmd-btn[data-type="exec"]')
2550
+ .forEach((b) => (b.disabled = false));
2551
+ execEs?.close();
2552
+ execEs = null;
2553
+ }
2554
+ };
2555
+ execEs.onerror = () => {
2556
+ cmdAppendLine("Connection closed", "err");
2557
+ setSvcDot(repo, service, "err");
2558
+ document.getElementById("svc-status").textContent = "idle";
2559
+ document.getElementById("stop-cmd").style.display = "none";
2560
+ document
2561
+ .querySelectorAll('.cmd-btn[data-type="exec"]')
2562
+ .forEach((b) => (b.disabled = false));
2563
+ execEs?.close();
2564
+ execEs = null;
2565
+ };
2566
+ }
2567
+
2568
+ function cmdAppendLine(text, cls) {
2569
+ const out = document.getElementById("cmd-output");
2570
+ const span = document.createElement("span");
2571
+ if (cls) span.className = cls;
2572
+ span.textContent = text + "\n";
2573
+ out.appendChild(span);
2574
+ out.scrollTop = out.scrollHeight;
2575
+ }
2576
+
2577
+ document
2578
+ .getElementById("clear-cmd-output")
2579
+ .addEventListener("click", () => {
2580
+ document.getElementById("cmd-output").innerHTML = "";
2581
+ document.getElementById("cmd-strip").classList.add("hidden");
2582
+ });
2583
+
2584
+ // ── Helpers ───────────────────────────────────────────────────────────────
2585
+ function fmtDuration(ms) {
2586
+ if (ms < 1000) return `${ms}ms`;
2587
+ const s = Math.floor(ms / 1000),
2588
+ m = Math.floor(s / 60);
2589
+ return m > 0 ? `${m}m ${s % 60}s` : `${s}s`;
2590
+ }
2591
+
2592
+ init();
2593
+ </script>
2594
+ </body>
2595
+ </html>