@yiaany/ghostapi 0.1.6 → 0.1.8

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 (323) hide show
  1. package/CHANGELOG.md +48 -2
  2. package/CONTRIBUTING.md +119 -117
  3. package/LICENSE +21 -21
  4. package/README.md +208 -185
  5. package/SECURITY.md +53 -37
  6. package/dist/actions/gateway.d.ts +179 -0
  7. package/dist/actions/gateway.js +514 -0
  8. package/dist/actions/gateway.js.map +1 -0
  9. package/dist/actions/index.d.ts +2 -0
  10. package/dist/actions/index.js +2 -0
  11. package/dist/actions/index.js.map +1 -0
  12. package/dist/ai/aiClient.js +4 -4
  13. package/dist/ai/aiClient.js.map +1 -1
  14. package/dist/ai/aiGenerator.d.ts +2 -2
  15. package/dist/ai/aiGenerator.js +21 -72
  16. package/dist/ai/aiGenerator.js.map +1 -1
  17. package/dist/ai/prompts.js +51 -51
  18. package/dist/ai/prompts.js.map +1 -1
  19. package/dist/approvals/approvalInbox.d.ts +150 -0
  20. package/dist/approvals/approvalInbox.js +412 -0
  21. package/dist/approvals/approvalInbox.js.map +1 -0
  22. package/dist/approvals/index.d.ts +2 -0
  23. package/dist/approvals/index.js +2 -0
  24. package/dist/approvals/index.js.map +1 -0
  25. package/dist/behavior/behaviorStore.d.ts +2 -0
  26. package/dist/behavior/behaviorStore.js +50 -46
  27. package/dist/behavior/behaviorStore.js.map +1 -1
  28. package/dist/cache/index.d.ts +2 -0
  29. package/dist/cache/index.js +58 -14
  30. package/dist/cache/index.js.map +1 -1
  31. package/dist/cli/index.js +438 -58
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/openUrl.d.ts +10 -0
  34. package/dist/cli/openUrl.js +31 -0
  35. package/dist/cli/openUrl.js.map +1 -0
  36. package/dist/cli/parser.d.ts +155 -0
  37. package/dist/cli/parser.js +504 -2
  38. package/dist/cli/parser.js.map +1 -1
  39. package/dist/config/dataPaths.d.ts +31 -0
  40. package/dist/config/dataPaths.js +45 -0
  41. package/dist/config/dataPaths.js.map +1 -0
  42. package/dist/config/localConfig.d.ts +1 -2
  43. package/dist/config/localConfig.js +27 -21
  44. package/dist/config/localConfig.js.map +1 -1
  45. package/dist/config/serverConfig.d.ts +2 -0
  46. package/dist/config/serverConfig.js +8 -1
  47. package/dist/config/serverConfig.js.map +1 -1
  48. package/dist/contracts/contract.d.ts +76 -0
  49. package/dist/contracts/contract.js +603 -0
  50. package/dist/contracts/contract.js.map +1 -0
  51. package/dist/contracts/index.d.ts +2 -0
  52. package/dist/contracts/index.js +2 -0
  53. package/dist/contracts/index.js.map +1 -0
  54. package/dist/credentials/credentialBroker.d.ts +207 -0
  55. package/dist/credentials/credentialBroker.js +588 -0
  56. package/dist/credentials/credentialBroker.js.map +1 -0
  57. package/dist/credentials/index.d.ts +2 -0
  58. package/dist/credentials/index.js +2 -0
  59. package/dist/credentials/index.js.map +1 -0
  60. package/dist/dashboard/app.js +685 -615
  61. package/dist/dashboard/index.html +13 -7
  62. package/dist/dashboard/styles.css +349 -349
  63. package/dist/egress/capabilities.d.ts +32 -0
  64. package/dist/egress/capabilities.js +165 -0
  65. package/dist/egress/capabilities.js.map +1 -0
  66. package/dist/egress/linuxBootstrap.d.ts +1 -0
  67. package/dist/egress/linuxBootstrap.js +121 -0
  68. package/dist/egress/linuxBootstrap.js.map +1 -0
  69. package/dist/egress/run.d.ts +18 -0
  70. package/dist/egress/run.js +305 -0
  71. package/dist/egress/run.js.map +1 -0
  72. package/dist/errors/errorEngine.d.ts +2 -2
  73. package/dist/errors/errorEngine.js +3 -40
  74. package/dist/errors/errorEngine.js.map +1 -1
  75. package/dist/evals/evals.d.ts +126 -0
  76. package/dist/evals/evals.js +466 -0
  77. package/dist/evals/evals.js.map +1 -0
  78. package/dist/evals/index.d.ts +2 -0
  79. package/dist/evals/index.js +2 -0
  80. package/dist/evals/index.js.map +1 -0
  81. package/dist/evidence/index.d.ts +2 -0
  82. package/dist/evidence/index.js +2 -0
  83. package/dist/evidence/index.js.map +1 -0
  84. package/dist/evidence/report.d.ts +143 -0
  85. package/dist/evidence/report.js +556 -0
  86. package/dist/evidence/report.js.map +1 -0
  87. package/dist/fault/faultLab.d.ts +5 -4
  88. package/dist/fault/faultLab.js +66 -24
  89. package/dist/fault/faultLab.js.map +1 -1
  90. package/dist/index.d.ts +46 -0
  91. package/dist/index.js +23 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/inventory/analysis.d.ts +12 -0
  94. package/dist/inventory/analysis.js +457 -0
  95. package/dist/inventory/analysis.js.map +1 -0
  96. package/dist/inventory/index.d.ts +5 -0
  97. package/dist/inventory/index.js +4 -0
  98. package/dist/inventory/index.js.map +1 -0
  99. package/dist/inventory/inventoryController.d.ts +53 -0
  100. package/dist/inventory/inventoryController.js +1044 -0
  101. package/dist/inventory/inventoryController.js.map +1 -0
  102. package/dist/inventory/types.d.ts +579 -0
  103. package/dist/inventory/types.js +787 -0
  104. package/dist/inventory/types.js.map +1 -0
  105. package/dist/landing/assets/index-CxWsopzP.css +1 -0
  106. package/dist/landing/assets/index-DZlpyhqd.js +55 -0
  107. package/dist/landing/assets/index-Dk-Xezs3.js +55 -0
  108. package/dist/landing/index.html +15 -15
  109. package/dist/ledger/actionLedger.d.ts +125 -0
  110. package/dist/ledger/actionLedger.js +594 -0
  111. package/dist/ledger/actionLedger.js.map +1 -0
  112. package/dist/ledger/index.d.ts +2 -0
  113. package/dist/ledger/index.js +2 -0
  114. package/dist/ledger/index.js.map +1 -0
  115. package/dist/mcp/server.js +4 -3
  116. package/dist/mcp/server.js.map +1 -1
  117. package/dist/policy/index.d.ts +3 -0
  118. package/dist/policy/index.js +2 -0
  119. package/dist/policy/index.js.map +1 -0
  120. package/dist/policy/policy.d.ts +14 -0
  121. package/dist/policy/policy.js +282 -0
  122. package/dist/policy/policy.js.map +1 -0
  123. package/dist/policy/types.d.ts +52 -0
  124. package/dist/policy/types.js +2 -0
  125. package/dist/policy/types.js.map +1 -0
  126. package/dist/productTelemetry/index.d.ts +16 -0
  127. package/dist/productTelemetry/index.js +142 -0
  128. package/dist/productTelemetry/index.js.map +1 -0
  129. package/dist/providers/conformance.d.ts +7 -0
  130. package/dist/providers/conformance.js +48 -0
  131. package/dist/providers/conformance.js.map +1 -0
  132. package/dist/providers/index.d.ts +4 -2
  133. package/dist/providers/index.js +3 -1
  134. package/dist/providers/index.js.map +1 -1
  135. package/dist/providers/packs/resendPack.d.ts +2 -0
  136. package/dist/providers/packs/resendPack.js +147 -0
  137. package/dist/providers/packs/resendPack.js.map +1 -0
  138. package/dist/providers/packs/stripePack.d.ts +2 -0
  139. package/dist/providers/packs/stripePack.js +852 -0
  140. package/dist/providers/packs/stripePack.js.map +1 -0
  141. package/dist/providers/registry.d.ts +7 -3
  142. package/dist/providers/registry.js +55 -4
  143. package/dist/providers/registry.js.map +1 -1
  144. package/dist/providers/resend.js +2 -11
  145. package/dist/providers/resend.js.map +1 -1
  146. package/dist/providers/runtime.d.ts +8 -0
  147. package/dist/providers/runtime.js +48 -0
  148. package/dist/providers/runtime.js.map +1 -0
  149. package/dist/providers/stripeWebhook.d.ts +13 -0
  150. package/dist/providers/stripeWebhook.js +50 -0
  151. package/dist/providers/stripeWebhook.js.map +1 -0
  152. package/dist/providers/types.d.ts +153 -0
  153. package/dist/proxy/cacheKey.js +3 -3
  154. package/dist/proxy/cacheKey.js.map +1 -1
  155. package/dist/proxy/providerDetector.d.ts +2 -7
  156. package/dist/proxy/providerDetector.js +4 -4
  157. package/dist/proxy/providerDetector.js.map +1 -1
  158. package/dist/proxy/proxyHandler.js +116 -12
  159. package/dist/proxy/proxyHandler.js.map +1 -1
  160. package/dist/proxy/requestNormalizer.d.ts +0 -1
  161. package/dist/proxy/requestNormalizer.js +2 -5
  162. package/dist/proxy/requestNormalizer.js.map +1 -1
  163. package/dist/reliability/costGovernance.d.ts +175 -0
  164. package/dist/reliability/costGovernance.js +515 -0
  165. package/dist/reliability/costGovernance.js.map +1 -0
  166. package/dist/reliability/index.d.ts +8 -0
  167. package/dist/reliability/index.js +5 -0
  168. package/dist/reliability/index.js.map +1 -0
  169. package/dist/reliability/reconciliation.d.ts +157 -0
  170. package/dist/reliability/reconciliation.js +522 -0
  171. package/dist/reliability/reconciliation.js.map +1 -0
  172. package/dist/reliability/runtimeHealth.d.ts +60 -0
  173. package/dist/reliability/runtimeHealth.js +356 -0
  174. package/dist/reliability/runtimeHealth.js.map +1 -0
  175. package/dist/reliability/slo.d.ts +131 -0
  176. package/dist/reliability/slo.js +375 -0
  177. package/dist/reliability/slo.js.map +1 -0
  178. package/dist/report/safetyReport.js +7 -3
  179. package/dist/report/safetyReport.js.map +1 -1
  180. package/dist/safety/index.d.ts +2 -0
  181. package/dist/safety/index.js +2 -0
  182. package/dist/safety/index.js.map +1 -0
  183. package/dist/safety/safetyController.d.ts +187 -0
  184. package/dist/safety/safetyController.js +477 -0
  185. package/dist/safety/safetyController.js.map +1 -0
  186. package/dist/scenarios/scenarioBundle.d.ts +75 -0
  187. package/dist/scenarios/scenarioBundle.js +750 -0
  188. package/dist/scenarios/scenarioBundle.js.map +1 -0
  189. package/dist/scenarios/scenarioStore.d.ts +1 -0
  190. package/dist/scenarios/scenarioStore.js +47 -32
  191. package/dist/scenarios/scenarioStore.js.map +1 -1
  192. package/dist/security/headerSanitizer.d.ts +2 -0
  193. package/dist/security/headerSanitizer.js +36 -1
  194. package/dist/security/headerSanitizer.js.map +1 -1
  195. package/dist/security/secrets.js +24 -4
  196. package/dist/security/secrets.js.map +1 -1
  197. package/dist/server/accessControl.d.ts +5 -0
  198. package/dist/server/accessControl.js +100 -0
  199. package/dist/server/accessControl.js.map +1 -0
  200. package/dist/server/createServer.d.ts +0 -5
  201. package/dist/server/createServer.js +9 -29
  202. package/dist/server/createServer.js.map +1 -1
  203. package/dist/server/eventsStore.d.ts +6 -1
  204. package/dist/server/eventsStore.js +83 -15
  205. package/dist/server/eventsStore.js.map +1 -1
  206. package/dist/server/routes.js +36 -10
  207. package/dist/server/routes.js.map +1 -1
  208. package/dist/server/sse.d.ts +2 -1
  209. package/dist/server/sse.js +15 -3
  210. package/dist/server/sse.js.map +1 -1
  211. package/dist/setup/setupGenerator.js +53 -6
  212. package/dist/setup/setupGenerator.js.map +1 -1
  213. package/dist/state/stateStore.d.ts +5 -0
  214. package/dist/state/stateStore.js +48 -62
  215. package/dist/state/stateStore.js.map +1 -1
  216. package/dist/storage/fileStore.d.ts +10 -0
  217. package/dist/storage/fileStore.js +131 -0
  218. package/dist/storage/fileStore.js.map +1 -0
  219. package/dist/teamControl/controlPlane.d.ts +255 -0
  220. package/dist/teamControl/controlPlane.js +918 -0
  221. package/dist/teamControl/controlPlane.js.map +1 -0
  222. package/dist/teamControl/deployment.d.ts +33 -0
  223. package/dist/teamControl/deployment.js +67 -0
  224. package/dist/teamControl/deployment.js.map +1 -0
  225. package/dist/teamControl/index.d.ts +4 -0
  226. package/dist/teamControl/index.js +3 -0
  227. package/dist/teamControl/index.js.map +1 -0
  228. package/dist/tests/testGenerator.js +4 -1
  229. package/dist/tests/testGenerator.js.map +1 -1
  230. package/dist/trust/index.d.ts +2 -0
  231. package/dist/trust/index.js +2 -0
  232. package/dist/trust/index.js.map +1 -0
  233. package/dist/trust/trustLadder.d.ts +182 -0
  234. package/dist/trust/trustLadder.js +560 -0
  235. package/dist/trust/trustLadder.js.map +1 -0
  236. package/dist/worlds/index.d.ts +2 -0
  237. package/dist/worlds/index.js +2 -0
  238. package/dist/worlds/index.js.map +1 -0
  239. package/dist/worlds/worlds.d.ts +160 -0
  240. package/dist/worlds/worlds.js +491 -0
  241. package/dist/worlds/worlds.js.map +1 -0
  242. package/docs/assets/dashboard.png +0 -0
  243. package/docs/ci.md +49 -0
  244. package/docs/commercial/README.md +29 -0
  245. package/docs/commercial/data-inventory.md +36 -0
  246. package/docs/commercial/manual-invoicing.md +44 -0
  247. package/docs/commercial/metrics.md +51 -0
  248. package/docs/commercial/pricing.md +103 -0
  249. package/docs/commercial/privacy-policy-draft.md +41 -0
  250. package/docs/commercial/terms-topics-for-counsel.md +51 -0
  251. package/docs/design-partners/README.md +52 -0
  252. package/docs/design-partners/demo-narratives.md +33 -0
  253. package/docs/design-partners/design-partner-offer.md +39 -0
  254. package/docs/design-partners/discovery-questions.md +28 -0
  255. package/docs/design-partners/feedback-capture-template.md +48 -0
  256. package/docs/design-partners/icp-one-pager.md +43 -0
  257. package/docs/design-partners/onboarding-checklist.md +33 -0
  258. package/docs/design-partners/pilot-success-criteria.md +27 -0
  259. package/docs/design-partners/pricing-interview-script.md +18 -0
  260. package/docs/design-partners/telemetry-plan.md +48 -0
  261. package/docs/development/baseline.md +114 -0
  262. package/docs/development/onboarding-smoke.md +37 -0
  263. package/docs/development/verification-0.1.8.md +25 -0
  264. package/docs/enterprise-product-roadmap-ru.md +1879 -0
  265. package/docs/fundraising/README.md +87 -0
  266. package/docs/fundraising/data-room-checklist.md +72 -0
  267. package/docs/fundraising/demo-script.md +52 -0
  268. package/docs/fundraising/design-partners-50.md +74 -0
  269. package/docs/fundraising/launch-posts.md +37 -0
  270. package/docs/fundraising/metrics-and-evidence.md +30 -0
  271. package/docs/fundraising/roadmap-12-month.md +25 -0
  272. package/docs/fundraising/technical-due-diligence-index.md +37 -0
  273. package/docs/fundraising/yc-application.md +25 -0
  274. package/docs/github-actions.md +84 -0
  275. package/docs/hosted-pilot.md +111 -0
  276. package/docs/mcp.md +52 -50
  277. package/docs/operations/disaster-recovery-runbook.md +81 -0
  278. package/docs/operations/kill-switch-runbook.md +32 -0
  279. package/docs/policy.md +61 -0
  280. package/docs/providers/authoring-packs.md +175 -0
  281. package/docs/providers/stripe-core-pack.md +82 -0
  282. package/docs/release-checklist.md +83 -71
  283. package/docs/release-migration-and-rollback.md +63 -0
  284. package/docs/release-readiness.md +39 -0
  285. package/docs/security/action-gateway-threat-model.md +34 -0
  286. package/docs/security/action-ledger-incident-replay-threat-model.md +42 -0
  287. package/docs/security/approval-inbox-threat-model.md +35 -0
  288. package/docs/security/credential-broker-threat-model.md +52 -0
  289. package/docs/security/egress-threat-model.md +126 -0
  290. package/docs/security/inventory-threat-model.md +104 -0
  291. package/docs/security/kill-switch-budgets-threat-model.md +37 -0
  292. package/docs/security/reliability-threat-model.md +111 -0
  293. package/docs/security/trust-ladder-threat-model.md +33 -0
  294. package/docs/team-control-plane.md +72 -0
  295. package/docs/usage.md +571 -57
  296. package/examples/README.md +10 -0
  297. package/examples/agent-instructions/README.md +39 -39
  298. package/examples/ci-smoke/README.md +21 -0
  299. package/examples/ci-smoke/ghostapi.policy.yaml +22 -0
  300. package/examples/ci-smoke/package.json +9 -0
  301. package/examples/ci-smoke/production-egress.mjs +2 -0
  302. package/examples/ci-smoke/safe.mjs +17 -0
  303. package/examples/evals/README.md +24 -0
  304. package/examples/evals/retry-after.eval.json +52 -0
  305. package/examples/generic-rest/README.md +27 -27
  306. package/examples/github-fetch/README.md +35 -35
  307. package/examples/openai-streaming/README.md +16 -0
  308. package/examples/openai-streaming/streaming-tool-call.mjs +31 -0
  309. package/examples/policy/ghostapi.policy.yaml +27 -0
  310. package/examples/record-replay/README.md +19 -0
  311. package/examples/record-replay/replay-requests.json +22 -0
  312. package/examples/record-replay/stripe-checkout.har.json +30 -0
  313. package/examples/resend-node/README.md +22 -22
  314. package/examples/stripe-node/README.md +35 -29
  315. package/examples/stripe-node/checkout-flow.mjs +61 -0
  316. package/examples/twilio-node/README.md +21 -21
  317. package/examples/worlds/README.md +19 -0
  318. package/examples/worlds/subscription-recovery.mjs +12 -0
  319. package/package.json +26 -11
  320. package/dist/providers/stripe.d.ts +0 -2
  321. package/dist/providers/stripe.js +0 -15
  322. package/dist/providers/stripe.js.map +0 -1
  323. package/docs/assets/ghostapi-avatar.png +0 -0
@@ -1,615 +1,685 @@
1
- // DOM References
2
- const els = {
3
- sidebar: document.getElementById('sidebar'),
4
- toggleBtn: document.getElementById('sidebar-toggle'),
5
- statusIndicator: document.getElementById('status-indicator'),
6
- statusText: document.getElementById('status-text'),
7
- eventList: document.getElementById('event-list'),
8
- eventCount: document.getElementById('event-count'),
9
- emptyDetails: document.getElementById('empty-details'),
10
- detailsContent: document.getElementById('details-content'),
11
- searchModal: document.getElementById('search-modal'),
12
- searchInput: document.getElementById('search-input'),
13
- rulesModal: document.getElementById('rules-modal'),
14
- rulesOutput: document.getElementById('rules-output'),
15
- rulesSubtitle: document.getElementById('rules-subtitle'),
16
- setupModal: document.getElementById('setup-modal'),
17
- setupList: document.getElementById('setup-list'),
18
- setupOutput: document.getElementById('setup-output'),
19
- setupSubtitle: document.getElementById('setup-subtitle'),
20
- scenariosModal: document.getElementById('scenarios-modal'),
21
- scenarioList: document.getElementById('scenario-list'),
22
- scenarioOutput: document.getElementById('scenario-output'),
23
- scenariosSubtitle: document.getElementById('scenarios-subtitle'),
24
- dMethod: document.getElementById('d-method'),
25
- dPath: document.getElementById('d-path'),
26
- dStatus: document.getElementById('d-status'),
27
- dTime: document.getElementById('d-time'),
28
- dProvider: document.getElementById('d-provider'),
29
- dSource: document.getElementById('d-source'),
30
- dReq: document.getElementById('d-req-body'),
31
- dRes: document.getElementById('d-res-body'),
32
- faultToggle: document.getElementById('fault-toggle'),
33
- faultState: document.getElementById('fault-state'),
34
- };
35
-
36
- let events = [];
37
- let activeId = null;
38
- let currentFilter = 'all';
39
- let setupPayload = null;
40
- let selectedSetupText = '';
41
- let scenarios = [];
42
-
43
- // Initialize
44
- async function init() {
45
- setupInteractions();
46
- await fetchFaultLab();
47
- await fetchHistory();
48
- setupSSE();
49
- }
50
-
51
- // Side-effects & Listeners
52
- function setupInteractions() {
53
- els.toggleBtn.onclick = () => els.sidebar.classList.toggle('closed');
54
-
55
- // Command + K
56
- document.addEventListener('keydown', (e) => {
57
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
58
- e.preventDefault();
59
- openSearch();
60
- }
61
- if (e.key === 'Escape') closeSearch();
62
- if (e.key === 'Escape') closeRulesModal();
63
- if (e.key === 'Escape') closeSetupModal();
64
- if (e.key === 'Escape') closeScenariosModal();
65
- });
66
-
67
- // Modal overlay click
68
- els.searchModal.onclick = (e) => {
69
- if (e.target === els.searchModal) closeSearch();
70
- };
71
- els.rulesModal.onclick = (e) => {
72
- if (e.target === els.rulesModal) closeRulesModal();
73
- };
74
- els.setupModal.onclick = (e) => {
75
- if (e.target === els.setupModal) closeSetupModal();
76
- };
77
- els.scenariosModal.onclick = (e) => {
78
- if (e.target === els.scenariosModal) closeScenariosModal();
79
- };
80
-
81
- els.searchInput.addEventListener('input', renderList);
82
- }
83
-
84
- function openSearch() {
85
- els.searchModal.classList.add('open');
86
- els.searchInput.focus();
87
- }
88
-
89
- function closeSearch() {
90
- els.searchModal.classList.remove('open');
91
- els.searchInput.value = '';
92
- renderList();
93
- }
94
-
95
- // Data Handling
96
- async function fetchHistory() {
97
- try {
98
- const res = await fetch('/api/events');
99
- const data = await res.json();
100
- events = data.reverse();
101
- renderList();
102
- } catch(e) {}
103
- }
104
-
105
- function setupSSE() {
106
- const sse = new EventSource('/events');
107
- sse.onopen = () => {
108
- els.statusIndicator.className = 'status-indicator connected';
109
- els.statusText.textContent = 'Connected';
110
- };
111
- sse.onerror = () => {
112
- els.statusIndicator.className = 'status-indicator error';
113
- els.statusText.textContent = 'Reconnecting...';
114
- };
115
- sse.onmessage = (e) => {
116
- const payload = JSON.parse(e.data);
117
- if (payload.type === 'proxy_event') {
118
- events.unshift(payload.event);
119
- if (events.length > 200) events.pop();
120
- renderList();
121
- }
122
- };
123
- }
124
-
125
- // Filter Management
126
- window.setFilter = (filter) => {
127
- currentFilter = filter;
128
- document.querySelectorAll('.nav-scroll-area .nav-item').forEach(el => el.classList.remove('active'));
129
- const activeEl = document.getElementById('nav-' + filter);
130
- if (activeEl) activeEl.classList.add('active');
131
-
132
- if (filter === 'all') {
133
- document.querySelector('.breadcrumb-active').textContent = 'Live Traffic';
134
- } else {
135
- document.querySelector('.breadcrumb-active').textContent = activeEl.querySelector('.nav-item-title').textContent + ' Routing';
136
- }
137
-
138
- renderList();
139
- };
140
-
141
- window.clearStorage = async (target, event) => {
142
- try {
143
- const btnText = event.currentTarget.querySelector('.nav-item-title');
144
- const originalText = btnText.textContent;
145
- btnText.textContent = 'Clearing...';
146
-
147
- await fetch('/api/clear', {
148
- method: 'POST',
149
- headers: { 'Content-Type': 'application/json' },
150
- body: JSON.stringify({ target })
151
- });
152
-
153
- btnText.textContent = 'Cleared!';
154
- setTimeout(() => { btnText.textContent = originalText; }, 1500);
155
- } catch(e) {
156
- console.error(e);
157
- }
158
- };
159
-
160
- async function fetchFaultLab() {
161
- try {
162
- const res = await fetch('/api/fault-lab');
163
- renderFaultLab(await res.json());
164
- } catch(e) {}
165
- }
166
-
167
- window.toggleFaultLab = async (event) => {
168
- const label = event.currentTarget.querySelector('.nav-item-title');
169
- const original = label.textContent;
170
- try {
171
- const enabled = !els.faultToggle.classList.contains('active');
172
- label.textContent = enabled ? 'Arming...' : 'Disarming...';
173
-
174
- const res = await fetch('/api/fault-lab', {
175
- method: 'POST',
176
- headers: { 'Content-Type': 'application/json' },
177
- body: JSON.stringify({ enabled, latencyMs: 0, latencyMinMs: 2000, latencyMaxMs: 5000, errorRate: enabled ? 15 : 15, statusCode: 429, retryAfterSeconds: 2 })
178
- });
179
-
180
- const payload = await res.json();
181
- if (!res.ok) throw new Error(payload?.error?.message || 'Unable to toggle Chaos Mode');
182
- renderFaultLab(payload);
183
- } catch(e) {
184
- console.error(e);
185
- } finally {
186
- label.textContent = original;
187
- }
188
- };
189
-
190
- function renderFaultLab(config) {
191
- els.faultToggle.classList.toggle('active', config.enabled);
192
- els.faultState.textContent = config.enabled ? `${config.errorRate}%` : 'Off';
193
- }
194
-
195
- window.generateAiRules = async (event) => {
196
- const label = event.currentTarget.querySelector('.nav-item-title');
197
- const original = label.textContent;
198
- label.textContent = 'Generating...';
199
- els.rulesModal.classList.add('open');
200
- els.rulesOutput.textContent = 'Generating .cursorrules from package.json...';
201
- els.rulesSubtitle.textContent = 'Scanning dependencies';
202
-
203
- try {
204
- const res = await fetch('/api/ai-rules', { method: 'POST' });
205
- const payload = await res.json();
206
- if (!res.ok) throw new Error(payload?.error?.message || 'Unable to generate AI rules');
207
- els.rulesOutput.textContent = payload.content;
208
- els.rulesSubtitle.textContent = payload.detected.length > 0 ? `Detected: ${payload.detected.join(', ')}` : 'No known provider SDKs detected';
209
- } catch (e) {
210
- els.rulesOutput.textContent = `# GhostAPI Agent Rules\n\nGeneration failed: ${e.message}`;
211
- els.rulesSubtitle.textContent = 'Generation failed';
212
- } finally {
213
- label.textContent = original;
214
- }
215
- };
216
-
217
- window.copyAgentPrompt = async (event) => {
218
- const label = event.currentTarget.querySelector('.nav-item-title');
219
- const original = label.textContent;
220
- label.textContent = 'Copying...';
221
- try {
222
- const res = await fetch('/api/agent-prompt');
223
- const payload = await res.json();
224
- if (!res.ok) throw new Error(payload?.error?.message || 'Unable to generate prompt');
225
- await copyText(payload.content);
226
- els.rulesModal.classList.add('open');
227
- els.rulesSubtitle.textContent = 'Copied to clipboard';
228
- els.rulesOutput.textContent = payload.content;
229
- } catch (e) {
230
- els.rulesModal.classList.add('open');
231
- els.rulesSubtitle.textContent = 'Prompt generation failed';
232
- els.rulesOutput.textContent = `GhostAPI agent prompt failed: ${e.message}`;
233
- } finally {
234
- label.textContent = original;
235
- }
236
- };
237
-
238
- window.openSafetyReport = async (event) => {
239
- const label = event.currentTarget.querySelector('.nav-item-title');
240
- const original = label.textContent;
241
- label.textContent = 'Scanning...';
242
- els.rulesModal.classList.add('open');
243
- els.rulesSubtitle.textContent = 'Scanning repo';
244
- els.rulesOutput.textContent = 'Generating GhostAPI safety report...';
245
- try {
246
- const res = await fetch('/api/safety-report');
247
- const payload = await res.json();
248
- if (!res.ok) throw new Error(payload?.error?.message || 'Unable to generate safety report');
249
- els.rulesSubtitle.textContent = `${payload.findings.length} findings`;
250
- els.rulesOutput.textContent = formatSafetyReport(payload);
251
- } catch (e) {
252
- els.rulesSubtitle.textContent = 'Safety report failed';
253
- els.rulesOutput.textContent = `Safety report failed: ${e.message}`;
254
- } finally {
255
- label.textContent = original;
256
- }
257
- };
258
-
259
- function closeRulesModal() {
260
- els.rulesModal.classList.remove('open');
261
- }
262
-
263
- function copyRules() {
264
- copyText(els.rulesOutput.textContent);
265
- }
266
-
267
- window.generateRepoSetup = async (event) => {
268
- const label = event.currentTarget.querySelector('.nav-item-title');
269
- const original = label.textContent;
270
- label.textContent = 'Generating...';
271
- selectedSetupText = '';
272
- setupPayload = null;
273
- els.setupModal.classList.add('open');
274
- els.setupList.innerHTML = '';
275
- els.setupOutput.textContent = 'Scanning package.json and building Cursor/Cline/Claude setup...';
276
- els.setupSubtitle.textContent = 'Scanning this repo';
277
-
278
- try {
279
- const res = await fetch('/api/setup', { method: 'POST' });
280
- setupPayload = await res.json();
281
- if (!res.ok) throw new Error(setupPayload?.error?.message || 'Unable to generate setup');
282
- renderSetup(setupPayload);
283
- } catch (e) {
284
- els.setupOutput.textContent = `Setup generation failed: ${e.message}`;
285
- els.setupSubtitle.textContent = 'Generation failed';
286
- } finally {
287
- label.textContent = original;
288
- }
289
- };
290
-
291
- function renderSetup(setup) {
292
- els.setupSubtitle.textContent = setup.detected.length > 0 ? `Detected SDKs: ${setup.detected.join(', ')}` : 'No known SDKs detected';
293
- els.setupList.innerHTML = '';
294
-
295
- addSetupItem('Summary', setup.summary);
296
- addSetupItem('Commands', setup.commands.join('\n'));
297
- for (const file of setup.files) addSetupItem(file.path, file.content, file.description);
298
- for (const patch of setup.patches) addSetupItem(patch.title, patch.content, patch.appliesTo);
299
-
300
- selectSetupText('Summary', setup.summary);
301
- }
302
-
303
- function addSetupItem(title, content, description = '') {
304
- const button = document.createElement('button');
305
- button.className = 'setup-item';
306
- button.type = 'button';
307
- button.innerHTML = `<strong>${escapeHtml(title)}</strong><span>${escapeHtml(description || 'Copy-ready setup block')}</span>`;
308
- button.onclick = () => selectSetupText(title, content);
309
- els.setupList.appendChild(button);
310
- }
311
-
312
- function selectSetupText(title, content) {
313
- selectedSetupText = content;
314
- els.setupOutput.textContent = content;
315
- els.setupList.querySelectorAll('.setup-item').forEach((item) => item.classList.toggle('active', item.querySelector('strong')?.textContent === title));
316
- }
317
-
318
- function closeSetupModal() {
319
- els.setupModal.classList.remove('open');
320
- }
321
-
322
- function copySetupOutput() {
323
- copyText(selectedSetupText || els.setupOutput.textContent);
324
- }
325
-
326
- window.openScenarios = async (event) => {
327
- const label = event.currentTarget.querySelector('.nav-item-title');
328
- const original = label.textContent;
329
- label.textContent = 'Loading...';
330
- scenarios = [];
331
- els.scenariosModal.classList.add('open');
332
- els.scenarioList.textContent = '';
333
- els.scenarioOutput.textContent = 'Loading scenario presets...';
334
- els.scenariosSubtitle.textContent = 'Loading presets';
335
-
336
- try {
337
- const res = await fetch('/api/scenarios');
338
- const payload = await res.json();
339
- if (!res.ok) throw new Error(payload?.error?.message || 'Unable to load scenarios');
340
- scenarios = payload;
341
- renderScenarios();
342
- } catch (e) {
343
- els.scenarioOutput.textContent = `Unable to load scenarios: ${e.message}`;
344
- } finally {
345
- label.textContent = original;
346
- }
347
- };
348
-
349
- function renderScenarios() {
350
- els.scenarioList.innerHTML = '';
351
- els.scenariosSubtitle.textContent = `${scenarios.length} presets ready`;
352
- const saveCard = document.createElement('div');
353
- saveCard.className = 'scenario-card scenario-save-card';
354
- const saveTitle = document.createElement('strong');
355
- saveTitle.textContent = 'Save current traffic';
356
- const saveDescription = document.createElement('p');
357
- saveDescription.textContent = 'Turn the latest dashboard traffic into a replayable custom scenario.';
358
- const saveActions = document.createElement('div');
359
- saveActions.className = 'scenario-actions';
360
- saveActions.append(scenarioButton('Save', saveTrafficScenario));
361
- saveCard.append(saveTitle, saveDescription, saveActions);
362
- els.scenarioList.appendChild(saveCard);
363
- for (const scenario of scenarios) {
364
- const card = document.createElement('div');
365
- card.className = 'scenario-card';
366
- const top = document.createElement('div');
367
- top.className = 'scenario-card-top';
368
- const title = document.createElement('strong');
369
- title.textContent = scenario.title;
370
- const provider = document.createElement('span');
371
- provider.className = 'pill';
372
- provider.textContent = scenario.provider;
373
- top.append(title, provider);
374
-
375
- const description = document.createElement('p');
376
- description.textContent = scenario.description;
377
-
378
- const actions = document.createElement('div');
379
- actions.className = 'scenario-actions';
380
- actions.append(
381
- scenarioButton('Replay', () => replayScenario(scenario.id)),
382
- scenarioButton('Export', () => exportScenario(scenario.id)),
383
- scenarioButton('Share', () => shareScenario(scenario.id))
384
- );
385
-
386
- card.append(top, description, actions);
387
- els.scenarioList.appendChild(card);
388
- }
389
- els.scenarioOutput.textContent = JSON.stringify(scenarios[0] ?? {}, null, 2);
390
- }
391
-
392
- window.replayScenario = async (id) => {
393
- const payload = await fetchScenarioAction(`/api/scenarios/${encodeURIComponent(id)}/replay`, { method: 'POST' });
394
- const text = JSON.stringify(payload, null, 2);
395
- els.scenarioOutput.textContent = text;
396
- await copyText(text);
397
- };
398
-
399
- window.exportScenario = async (id) => {
400
- const payload = await fetchScenarioAction(`/api/scenarios/${encodeURIComponent(id)}/export`);
401
- const text = JSON.stringify(payload, null, 2);
402
- els.scenarioOutput.textContent = text;
403
- await copyText(text);
404
- };
405
-
406
- window.shareScenario = async (id) => {
407
- const payload = await fetchScenarioAction(`/api/scenarios/${encodeURIComponent(id)}/share`, { method: 'POST' });
408
- els.scenarioOutput.textContent = JSON.stringify(payload, null, 2);
409
- await copyText(payload.shareText || JSON.stringify(payload));
410
- };
411
-
412
- async function saveTrafficScenario() {
413
- const payload = await fetchScenarioAction('/api/scenarios/save-from-traffic', {
414
- method: 'POST',
415
- headers: { 'content-type': 'application/json' },
416
- body: JSON.stringify({ title: `Saved traffic ${new Date().toLocaleString()}` })
417
- });
418
- const text = JSON.stringify(payload, null, 2);
419
- els.scenarioOutput.textContent = text;
420
- await copyText(text);
421
- const res = await fetch('/api/scenarios');
422
- scenarios = await res.json();
423
- }
424
-
425
- function scenarioButton(label, handler) {
426
- const button = document.createElement('button');
427
- button.type = 'button';
428
- button.textContent = label;
429
- button.onclick = () => handler().catch((error) => {
430
- els.scenarioOutput.textContent = `Scenario action failed: ${error.message}`;
431
- });
432
- return button;
433
- }
434
-
435
- async function fetchScenarioAction(url, options) {
436
- const res = await fetch(url, options);
437
- const payload = await res.json();
438
- if (!res.ok) throw new Error(payload?.error?.message || 'Scenario action failed');
439
- return payload;
440
- }
441
-
442
- function closeScenariosModal() {
443
- els.scenariosModal.classList.remove('open');
444
- }
445
-
446
- // Render Logic
447
- function renderList() {
448
- const query = els.searchInput.value.toLowerCase();
449
- const list = events.filter(e => {
450
- const path = String(e.path ?? '');
451
- const provider = String(e.provider ?? '');
452
- const requestText = safeStringify(e.request).toLowerCase();
453
- const matchesSearch = path.toLowerCase().includes(query) ||
454
- provider.toLowerCase().includes(query) ||
455
- requestText.includes(query);
456
- const matchesFilter = currentFilter === 'all' || provider === currentFilter;
457
- return matchesSearch && matchesFilter;
458
- });
459
-
460
- els.eventCount.textContent = events.length;
461
- els.eventList.innerHTML = '';
462
-
463
- const frag = document.createDocumentFragment();
464
- list.forEach(ev => {
465
- const div = document.createElement('div');
466
- div.className = `event-card ${ev.id === activeId ? 'active' : ''}`;
467
- div.onclick = () => selectEvent(ev.id);
468
-
469
- const method = String(ev.method ?? 'GET');
470
- const statusCode = Number(ev.statusCode ?? 0);
471
- const top = document.createElement('div');
472
- top.className = 'ec-top';
473
- const methodEl = document.createElement('span');
474
- methodEl.className = `ec-method m-${method.toLowerCase()}`;
475
- methodEl.textContent = method;
476
- const time = document.createElement('span');
477
- time.className = 'ec-time';
478
- time.textContent = new Date(ev.timestamp ?? Date.now()).toLocaleTimeString();
479
- top.append(methodEl, time);
480
-
481
- const path = document.createElement('div');
482
- path.className = 'ec-path';
483
- path.textContent = String(ev.path ?? '');
484
-
485
- const bottom = document.createElement('div');
486
- bottom.className = 'ec-bottom';
487
- const status = document.createElement('span');
488
- status.className = `pill pill-status-${strType(statusCode)}`;
489
- status.textContent = String(statusCode || 'n/a');
490
- const duration = document.createElement('span');
491
- duration.className = 'pill';
492
- duration.textContent = `${Number(ev.durationMs ?? 0)}ms`;
493
- const provider = document.createElement('span');
494
- provider.className = 'pill provider-pill';
495
- provider.textContent = String(ev.provider ?? 'unknown');
496
- bottom.append(status, duration, provider);
497
-
498
- div.append(top, path, bottom);
499
- frag.appendChild(div);
500
- });
501
- els.eventList.appendChild(frag);
502
- }
503
-
504
- function selectEvent(id) {
505
- activeId = id;
506
- renderList();
507
- const ev = events.find(e => e.id === id);
508
- if (!ev) return;
509
-
510
- els.emptyDetails.style.display = 'none';
511
- els.detailsContent.classList.add('visible');
512
-
513
- els.dMethod.textContent = ev.method;
514
- els.dMethod.className = `ec-method m-${ev.method.toLowerCase()}`;
515
- els.dPath.textContent = ev.path;
516
-
517
- els.dStatus.textContent = ev.statusCode;
518
- els.dStatus.className = `pill pill-status-${strType(ev.statusCode)}`;
519
-
520
- els.dTime.textContent = `${ev.durationMs}ms`;
521
- els.dProvider.textContent = ev.provider;
522
- els.dSource.textContent = ev.source;
523
-
524
- els.dReq.innerHTML = highlight(ev.request);
525
- els.dRes.innerHTML = highlight(ev.response);
526
- }
527
-
528
- window.generateSelectedTest = async () => {
529
- if (!activeId) return;
530
- try {
531
- const res = await fetch(`/api/events/${encodeURIComponent(activeId)}/test`);
532
- const payload = await res.json();
533
- if (!res.ok) throw new Error(payload?.error?.message || 'Unable to generate test');
534
- els.rulesModal.classList.add('open');
535
- els.rulesSubtitle.textContent = payload.filename;
536
- els.rulesOutput.textContent = payload.content;
537
- await copyText(payload.content);
538
- } catch (e) {
539
- els.rulesModal.classList.add('open');
540
- els.rulesSubtitle.textContent = 'Test generation failed';
541
- els.rulesOutput.textContent = `Test generation failed: ${e.message}`;
542
- }
543
- };
544
-
545
- // Utils
546
- function strType(code) {
547
- if (code >= 200 && code < 300) return '200';
548
- if (code >= 300 && code < 400) return '300';
549
- if (code >= 400 && code < 500) return '400';
550
- return '500';
551
- }
552
-
553
- function highlight(obj) {
554
- if (!obj) return '{}';
555
- const str = JSON.stringify(obj, null, 2);
556
- const scaped = str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
557
- return scaped.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => {
558
- let cls = 'j-num';
559
- if (/^"/.test(match)) { cls = /:$/.test(match) ? 'j-key' : 'j-str'; }
560
- else if (/true|false/.test(match)) cls = 'j-bool';
561
- else if (/null/.test(match)) cls = 'j-null';
562
- return `<span class="${cls}">${match}</span>`;
563
- });
564
- }
565
-
566
- function copy(type) {
567
- const text = (type === 'req') ? els.dReq.textContent : els.dRes.textContent;
568
- copyText(text);
569
- }
570
-
571
- window.closeRulesModal = closeRulesModal;
572
- window.copyRules = copyRules;
573
- window.closeSetupModal = closeSetupModal;
574
- window.copySetupOutput = copySetupOutput;
575
- window.closeScenariosModal = closeScenariosModal;
576
-
577
- function escapeHtml(value) {
578
- return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
579
- }
580
-
581
- function formatSafetyReport(report) {
582
- const findings = report.findings.length > 0
583
- ? report.findings.map((finding) => `- ${finding.severity.toUpperCase()} ${finding.file ? `${finding.file}: ` : ''}${finding.message}`).join('\n')
584
- : '- No high-risk provider usage found.';
585
- return [
586
- '# GhostAPI Safety Report',
587
- '',
588
- `Project: ${report.projectRoot}`,
589
- `Detected SDKs: ${report.detected.length > 0 ? report.detected.join(', ') : 'none'}`,
590
- '',
591
- '## Findings',
592
- findings,
593
- '',
594
- '## Recommendations',
595
- ...report.recommendations.map((item) => `- ${item}`)
596
- ].join('\n');
597
- }
598
-
599
- function safeStringify(value) {
600
- try {
601
- return JSON.stringify(value ?? '');
602
- } catch {
603
- return '';
604
- }
605
- }
606
-
607
- async function copyText(text) {
608
- try {
609
- await navigator.clipboard.writeText(text ?? '');
610
- } catch (error) {
611
- console.warn('Clipboard copy failed', error);
612
- }
613
- }
614
-
615
- init();
1
+ // DOM References
2
+ const els = {
3
+ sidebar: document.getElementById('sidebar'),
4
+ toggleBtn: document.getElementById('sidebar-toggle'),
5
+ statusIndicator: document.getElementById('status-indicator'),
6
+ statusText: document.getElementById('status-text'),
7
+ eventList: document.getElementById('event-list'),
8
+ eventCount: document.getElementById('event-count'),
9
+ emptyDetails: document.getElementById('empty-details'),
10
+ detailsContent: document.getElementById('details-content'),
11
+ searchModal: document.getElementById('search-modal'),
12
+ searchInput: document.getElementById('search-input'),
13
+ rulesModal: document.getElementById('rules-modal'),
14
+ rulesTitle: document.getElementById('rules-title'),
15
+ rulesOutput: document.getElementById('rules-output'),
16
+ rulesSubtitle: document.getElementById('rules-subtitle'),
17
+ rulesCopyButton: document.getElementById('rules-copy-button'),
18
+ setupModal: document.getElementById('setup-modal'),
19
+ setupList: document.getElementById('setup-list'),
20
+ setupOutput: document.getElementById('setup-output'),
21
+ setupSubtitle: document.getElementById('setup-subtitle'),
22
+ setupCopyButton: document.getElementById('setup-copy-button'),
23
+ scenariosModal: document.getElementById('scenarios-modal'),
24
+ scenarioList: document.getElementById('scenario-list'),
25
+ scenarioOutput: document.getElementById('scenario-output'),
26
+ scenariosSubtitle: document.getElementById('scenarios-subtitle'),
27
+ dMethod: document.getElementById('d-method'),
28
+ dPath: document.getElementById('d-path'),
29
+ dStatus: document.getElementById('d-status'),
30
+ dTime: document.getElementById('d-time'),
31
+ dProvider: document.getElementById('d-provider'),
32
+ dSource: document.getElementById('d-source'),
33
+ dReq: document.getElementById('d-req-body'),
34
+ dRes: document.getElementById('d-res-body'),
35
+ faultToggle: document.getElementById('fault-toggle'),
36
+ faultState: document.getElementById('fault-state'),
37
+ };
38
+
39
+ let events = [];
40
+ let activeId = null;
41
+ let currentFilter = 'all';
42
+ let setupPayload = null;
43
+ let selectedSetupText = '';
44
+ let scenarios = [];
45
+
46
+ // Initialize
47
+ async function init() {
48
+ setupInteractions();
49
+ await fetchProviderManifests();
50
+ await fetchFaultLab();
51
+ await fetchHistory();
52
+ setupSSE();
53
+ }
54
+
55
+ async function fetchProviderManifests() {
56
+ try {
57
+ const res = await fetch('/api/providers');
58
+ const manifests = await res.json();
59
+ if (!res.ok || !Array.isArray(manifests)) return;
60
+
61
+ for (const manifest of manifests) {
62
+ const item = document.getElementById(`nav-${manifest.name}`);
63
+ if (!item) continue;
64
+ const enabledCapabilities = Object.entries(manifest.capabilities ?? {}).filter(([, enabled]) => enabled).map(([name]) => name);
65
+ item.title = `${manifest.displayName}: ${enabledCapabilities.join(', ') || 'no declared capabilities'}`;
66
+ const right = document.createElement('div');
67
+ right.className = 'nav-item-right';
68
+ const badge = document.createElement('div');
69
+ badge.className = 'sc-badge';
70
+ badge.textContent = manifest.packVersion ? `${manifest.packVersion} / ${manifest.apiVersions.default}` : manifest.implementation;
71
+ right.appendChild(badge);
72
+ item.appendChild(right);
73
+ }
74
+ } catch(error) {
75
+ console.warn('Provider manifest load failed', error);
76
+ }
77
+ }
78
+
79
+ // Side-effects & Listeners
80
+ function setupInteractions() {
81
+ els.toggleBtn.onclick = () => els.sidebar.classList.toggle('closed');
82
+
83
+ // Command + K
84
+ document.addEventListener('keydown', (e) => {
85
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
86
+ e.preventDefault();
87
+ openSearch();
88
+ }
89
+ if (e.key === 'Escape') closeSearch();
90
+ if (e.key === 'Escape') closeRulesModal();
91
+ if (e.key === 'Escape') closeSetupModal();
92
+ if (e.key === 'Escape') closeScenariosModal();
93
+ });
94
+
95
+ // Modal overlay click
96
+ els.searchModal.onclick = (e) => {
97
+ if (e.target === els.searchModal) closeSearch();
98
+ };
99
+ els.rulesModal.onclick = (e) => {
100
+ if (e.target === els.rulesModal) closeRulesModal();
101
+ };
102
+ els.setupModal.onclick = (e) => {
103
+ if (e.target === els.setupModal) closeSetupModal();
104
+ };
105
+ els.scenariosModal.onclick = (e) => {
106
+ if (e.target === els.scenariosModal) closeScenariosModal();
107
+ };
108
+
109
+ els.searchInput.addEventListener('input', renderList);
110
+ }
111
+
112
+ function openSearch() {
113
+ els.searchModal.classList.add('open');
114
+ els.searchInput.focus();
115
+ }
116
+
117
+ function closeSearch() {
118
+ els.searchModal.classList.remove('open');
119
+ els.searchInput.value = '';
120
+ renderList();
121
+ }
122
+
123
+ // Data Handling
124
+ async function fetchHistory() {
125
+ try {
126
+ const res = await fetch('/api/events');
127
+ const data = await res.json();
128
+ if (!res.ok || !Array.isArray(data)) throw new Error('Unable to load event history');
129
+ events = data.reverse();
130
+ renderList();
131
+ } catch(e) {
132
+ console.warn('Event history load failed', e);
133
+ }
134
+ }
135
+
136
+ function setupSSE() {
137
+ const sse = new EventSource('/events');
138
+ sse.onopen = () => {
139
+ els.statusIndicator.className = 'status-indicator connected';
140
+ els.statusText.textContent = 'Connected';
141
+ };
142
+ sse.onerror = () => {
143
+ els.statusIndicator.className = 'status-indicator error';
144
+ els.statusText.textContent = 'Reconnecting...';
145
+ };
146
+ sse.onmessage = (e) => {
147
+ try {
148
+ const payload = JSON.parse(e.data);
149
+ if (payload.type === 'proxy_event' && payload.event && typeof payload.event === 'object') {
150
+ events.unshift(payload.event);
151
+ if (events.length > 200) events.pop();
152
+ renderList();
153
+ }
154
+ } catch (error) {
155
+ console.warn('Ignored malformed SSE event', error);
156
+ }
157
+ };
158
+ }
159
+
160
+ // Filter Management
161
+ window.setFilter = (filter) => {
162
+ currentFilter = filter;
163
+ document.querySelectorAll('.nav-scroll-area .nav-item').forEach(el => el.classList.remove('active'));
164
+ const activeEl = document.getElementById('nav-' + filter);
165
+ if (activeEl) activeEl.classList.add('active');
166
+
167
+ if (filter === 'all') {
168
+ document.querySelector('.breadcrumb-active').textContent = 'Live Traffic';
169
+ } else {
170
+ document.querySelector('.breadcrumb-active').textContent = activeEl.querySelector('.nav-item-title').textContent + ' Routing';
171
+ }
172
+
173
+ renderList();
174
+ };
175
+
176
+ window.clearStorage = async (target, event) => {
177
+ try {
178
+ const btnText = event.currentTarget.querySelector('.nav-item-title');
179
+ const originalText = btnText.textContent;
180
+ btnText.textContent = 'Clearing...';
181
+
182
+ const res = await fetch('/api/clear', {
183
+ method: 'POST',
184
+ headers: { 'Content-Type': 'application/json' },
185
+ body: JSON.stringify({ target })
186
+ });
187
+ const payload = await res.json().catch(() => null);
188
+ if (!res.ok) throw new Error(payload?.error?.message || `Unable to clear ${target}`);
189
+
190
+ btnText.textContent = 'Cleared!';
191
+ setTimeout(() => { btnText.textContent = originalText; }, 1500);
192
+ } catch(e) {
193
+ console.error(e);
194
+ const btnText = event.currentTarget.querySelector('.nav-item-title');
195
+ const originalText = target === 'cache' ? 'Clear Cache' : target === 'state' ? 'Clear State' : 'Clear Storage';
196
+ btnText.textContent = 'Clear failed';
197
+ setTimeout(() => { btnText.textContent = originalText; }, 1500);
198
+ }
199
+ };
200
+
201
+ async function fetchFaultLab() {
202
+ try {
203
+ const res = await fetch('/api/fault-lab');
204
+ renderFaultLab(await res.json());
205
+ } catch(e) {}
206
+ }
207
+
208
+ window.toggleFaultLab = async (event) => {
209
+ const label = event.currentTarget.querySelector('.nav-item-title');
210
+ const original = label.textContent;
211
+ try {
212
+ const enabled = !els.faultToggle.classList.contains('active');
213
+ label.textContent = enabled ? 'Arming...' : 'Disarming...';
214
+
215
+ const res = await fetch('/api/fault-lab', {
216
+ method: 'POST',
217
+ headers: { 'Content-Type': 'application/json' },
218
+ body: JSON.stringify({ enabled, latencyMs: 0, latencyMinMs: 2000, latencyMaxMs: 5000, errorRate: enabled ? 15 : 15, statusCode: 429, retryAfterSeconds: 2 })
219
+ });
220
+
221
+ const payload = await res.json();
222
+ if (!res.ok) throw new Error(payload?.error?.message || 'Unable to toggle Chaos Mode');
223
+ renderFaultLab(payload);
224
+ } catch(e) {
225
+ console.error(e);
226
+ } finally {
227
+ label.textContent = original;
228
+ }
229
+ };
230
+
231
+ function renderFaultLab(config) {
232
+ els.faultToggle.classList.toggle('active', config.enabled);
233
+ els.faultState.textContent = config.enabled ? `${config.errorRate}%` : 'Off';
234
+ }
235
+
236
+ window.generateAiRules = async (event) => {
237
+ const label = event.currentTarget.querySelector('.nav-item-title');
238
+ const original = label.textContent;
239
+ label.textContent = 'Generating...';
240
+ els.rulesModal.classList.add('open');
241
+ els.rulesTitle.textContent = '.cursorrules';
242
+ els.rulesOutput.textContent = 'Generating .cursorrules from package.json...';
243
+ els.rulesSubtitle.textContent = 'Scanning dependencies';
244
+
245
+ try {
246
+ const res = await fetch('/api/ai-rules', { method: 'POST' });
247
+ const payload = await res.json();
248
+ if (!res.ok) throw new Error(payload?.error?.message || 'Unable to generate AI rules');
249
+ els.rulesOutput.textContent = payload.content;
250
+ els.rulesSubtitle.textContent = payload.detected.length > 0 ? `Detected: ${payload.detected.join(', ')}` : 'No known provider SDKs detected';
251
+ } catch (e) {
252
+ els.rulesOutput.textContent = `# GhostAPI Agent Rules\n\nGeneration failed: ${e.message}`;
253
+ els.rulesSubtitle.textContent = 'Generation failed';
254
+ } finally {
255
+ label.textContent = original;
256
+ }
257
+ };
258
+
259
+ window.copyAgentPrompt = async (event) => {
260
+ const label = event.currentTarget.querySelector('.nav-item-title');
261
+ const original = label.textContent;
262
+ label.textContent = 'Copying...';
263
+ try {
264
+ const res = await fetch('/api/agent-prompt');
265
+ const payload = await res.json();
266
+ if (!res.ok) throw new Error(payload?.error?.message || 'Unable to generate prompt');
267
+ await copyText(payload.content);
268
+ els.rulesModal.classList.add('open');
269
+ els.rulesTitle.textContent = 'Agent Prompt';
270
+ els.rulesSubtitle.textContent = 'Copied to clipboard';
271
+ els.rulesOutput.textContent = payload.content;
272
+ } catch (e) {
273
+ els.rulesModal.classList.add('open');
274
+ els.rulesSubtitle.textContent = 'Prompt generation failed';
275
+ els.rulesOutput.textContent = `GhostAPI agent prompt failed: ${e.message}`;
276
+ } finally {
277
+ label.textContent = original;
278
+ }
279
+ };
280
+
281
+ window.openSafetyReport = async (event) => {
282
+ const label = event.currentTarget.querySelector('.nav-item-title');
283
+ const original = label.textContent;
284
+ label.textContent = 'Scanning...';
285
+ els.rulesModal.classList.add('open');
286
+ els.rulesTitle.textContent = 'Safety Report';
287
+ els.rulesSubtitle.textContent = 'Scanning repo';
288
+ els.rulesOutput.textContent = 'Generating GhostAPI safety report...';
289
+ try {
290
+ const res = await fetch('/api/safety-report');
291
+ const payload = await res.json();
292
+ if (!res.ok) throw new Error(payload?.error?.message || 'Unable to generate safety report');
293
+ els.rulesSubtitle.textContent = `${payload.findings.length} findings`;
294
+ els.rulesOutput.textContent = formatSafetyReport(payload);
295
+ } catch (e) {
296
+ els.rulesSubtitle.textContent = 'Safety report failed';
297
+ els.rulesOutput.textContent = `Safety report failed: ${e.message}`;
298
+ } finally {
299
+ label.textContent = original;
300
+ }
301
+ };
302
+
303
+ function closeRulesModal() {
304
+ els.rulesModal.classList.remove('open');
305
+ }
306
+
307
+ async function copyRules() {
308
+ const copied = await copyText(els.rulesOutput.textContent);
309
+ flashButton(els.rulesCopyButton, copied ? 'Copied' : 'Copy failed');
310
+ }
311
+
312
+ window.generateRepoSetup = async (event) => {
313
+ const label = event.currentTarget.querySelector('.nav-item-title');
314
+ const original = label.textContent;
315
+ label.textContent = 'Generating...';
316
+ selectedSetupText = '';
317
+ setupPayload = null;
318
+ els.setupModal.classList.add('open');
319
+ els.setupList.innerHTML = '';
320
+ els.setupOutput.textContent = 'Scanning package.json and building Cursor/Cline/Claude setup...';
321
+ els.setupSubtitle.textContent = 'Scanning this repo';
322
+
323
+ try {
324
+ const res = await fetch('/api/setup', { method: 'POST' });
325
+ setupPayload = await res.json();
326
+ if (!res.ok) throw new Error(setupPayload?.error?.message || 'Unable to generate setup');
327
+ renderSetup(setupPayload);
328
+ } catch (e) {
329
+ els.setupOutput.textContent = `Setup generation failed: ${e.message}`;
330
+ els.setupSubtitle.textContent = 'Generation failed';
331
+ } finally {
332
+ label.textContent = original;
333
+ }
334
+ };
335
+
336
+ function renderSetup(setup) {
337
+ els.setupSubtitle.textContent = setup.detected.length > 0 ? `Detected SDKs: ${setup.detected.join(', ')}` : 'No known SDKs detected';
338
+ els.setupList.innerHTML = '';
339
+
340
+ addSetupItem('Summary', setup.summary);
341
+ addSetupItem('Commands', setup.commands.join('\n'));
342
+ for (const file of setup.files) addSetupItem(file.path, file.content, file.description);
343
+ for (const patch of setup.patches) addSetupItem(patch.title, patch.content, patch.appliesTo);
344
+
345
+ selectSetupText('Summary', setup.summary);
346
+ }
347
+
348
+ function addSetupItem(title, content, description = '') {
349
+ const button = document.createElement('button');
350
+ button.className = 'setup-item';
351
+ button.type = 'button';
352
+ button.innerHTML = `<strong>${escapeHtml(title)}</strong><span>${escapeHtml(description || 'Copy-ready setup block')}</span>`;
353
+ button.onclick = () => selectSetupText(title, content);
354
+ els.setupList.appendChild(button);
355
+ }
356
+
357
+ function selectSetupText(title, content) {
358
+ selectedSetupText = content;
359
+ els.setupOutput.textContent = content;
360
+ els.setupList.querySelectorAll('.setup-item').forEach((item) => item.classList.toggle('active', item.querySelector('strong')?.textContent === title));
361
+ }
362
+
363
+ function closeSetupModal() {
364
+ els.setupModal.classList.remove('open');
365
+ }
366
+
367
+ async function copySetupOutput() {
368
+ const copied = await copyText(selectedSetupText || els.setupOutput.textContent);
369
+ flashButton(els.setupCopyButton, copied ? 'Copied' : 'Copy failed');
370
+ }
371
+
372
+ window.openScenarios = async (event) => {
373
+ const label = event.currentTarget.querySelector('.nav-item-title');
374
+ const original = label.textContent;
375
+ label.textContent = 'Loading...';
376
+ scenarios = [];
377
+ els.scenariosModal.classList.add('open');
378
+ els.scenarioList.textContent = '';
379
+ els.scenarioOutput.textContent = 'Loading scenario presets...';
380
+ els.scenariosSubtitle.textContent = 'Loading presets';
381
+
382
+ try {
383
+ const res = await fetch('/api/scenarios');
384
+ const payload = await res.json();
385
+ if (!res.ok) throw new Error(payload?.error?.message || 'Unable to load scenarios');
386
+ scenarios = payload;
387
+ renderScenarios();
388
+ } catch (e) {
389
+ els.scenarioOutput.textContent = `Unable to load scenarios: ${e.message}`;
390
+ } finally {
391
+ label.textContent = original;
392
+ }
393
+ };
394
+
395
+ function renderScenarios() {
396
+ els.scenarioList.innerHTML = '';
397
+ els.scenariosSubtitle.textContent = `${scenarios.length} scenarios ready`;
398
+ const saveCard = document.createElement('div');
399
+ saveCard.className = 'scenario-card scenario-save-card';
400
+ const saveTitle = document.createElement('strong');
401
+ saveTitle.textContent = 'Save current traffic';
402
+ const saveDescription = document.createElement('p');
403
+ saveDescription.textContent = 'Turn the latest dashboard traffic into a reusable custom scenario.';
404
+ const saveActions = document.createElement('div');
405
+ saveActions.className = 'scenario-actions';
406
+ saveActions.append(scenarioButton('Save', saveTrafficScenario));
407
+ saveCard.append(saveTitle, saveDescription, saveActions);
408
+ els.scenarioList.appendChild(saveCard);
409
+ for (const scenario of scenarios) {
410
+ const card = document.createElement('div');
411
+ card.className = 'scenario-card';
412
+ const top = document.createElement('div');
413
+ top.className = 'scenario-card-top';
414
+ const title = document.createElement('strong');
415
+ title.textContent = scenario.title;
416
+ const provider = document.createElement('span');
417
+ provider.className = 'pill';
418
+ provider.textContent = scenario.provider;
419
+ top.append(title, provider);
420
+
421
+ const description = document.createElement('p');
422
+ description.textContent = scenario.description;
423
+
424
+ const actions = document.createElement('div');
425
+ actions.className = 'scenario-actions';
426
+ actions.append(
427
+ scenarioButton('Arm', () => armScenario(scenario.id), 'Armed'),
428
+ scenarioButton('Export', () => exportScenario(scenario.id), 'Copied'),
429
+ scenarioButton('Share', () => shareScenario(scenario.id), 'Copied')
430
+ );
431
+
432
+ card.append(top, description, actions);
433
+ els.scenarioList.appendChild(card);
434
+ }
435
+ els.scenarioOutput.textContent = JSON.stringify(scenarios[0] ?? {}, null, 2);
436
+ }
437
+
438
+ window.armScenario = async (id) => {
439
+ const payload = await fetchScenarioAction(`/api/scenarios/${encodeURIComponent(id)}/replay`, { method: 'POST' });
440
+ const text = JSON.stringify(payload, null, 2);
441
+ els.scenarioOutput.textContent = text;
442
+ await copyText(text);
443
+ };
444
+
445
+ window.exportScenario = async (id) => {
446
+ const payload = await fetchScenarioAction(`/api/scenarios/${encodeURIComponent(id)}/export`);
447
+ const text = JSON.stringify(payload, null, 2);
448
+ els.scenarioOutput.textContent = text;
449
+ await copyText(text);
450
+ };
451
+
452
+ window.shareScenario = async (id) => {
453
+ const payload = await fetchScenarioAction(`/api/scenarios/${encodeURIComponent(id)}/share`, { method: 'POST' });
454
+ els.scenarioOutput.textContent = JSON.stringify(payload, null, 2);
455
+ await copyText(payload.shareText || JSON.stringify(payload));
456
+ };
457
+
458
+ async function saveTrafficScenario() {
459
+ const payload = await fetchScenarioAction('/api/scenarios/save-from-traffic', {
460
+ method: 'POST',
461
+ headers: { 'content-type': 'application/json' },
462
+ body: JSON.stringify({ title: `Saved traffic ${new Date().toLocaleString()}` })
463
+ });
464
+ const text = JSON.stringify(payload, null, 2);
465
+ const copied = await copyText(text);
466
+ const res = await fetch('/api/scenarios');
467
+ if (!res.ok) throw new Error('Saved scenario, but could not refresh the scenario list.');
468
+ scenarios = await res.json();
469
+ renderScenarios();
470
+ els.scenarioOutput.textContent = text;
471
+ els.scenariosSubtitle.textContent = `Saved ${payload.title} · ${scenarios.length} scenarios ready${copied ? ' · copied' : ''}`;
472
+ }
473
+
474
+ function scenarioButton(label, handler, successLabel = label) {
475
+ const button = document.createElement('button');
476
+ button.type = 'button';
477
+ button.textContent = label;
478
+ button.onclick = async () => {
479
+ button.disabled = true;
480
+ try {
481
+ await handler();
482
+ flashButton(button, successLabel);
483
+ } catch (error) {
484
+ els.scenarioOutput.textContent = `Scenario action failed: ${error.message}`;
485
+ flashButton(button, 'Failed');
486
+ } finally {
487
+ button.disabled = false;
488
+ }
489
+ };
490
+ return button;
491
+ }
492
+
493
+ async function fetchScenarioAction(url, options) {
494
+ const res = await fetch(url, options);
495
+ const payload = await res.json();
496
+ if (!res.ok) throw new Error(payload?.error?.message || 'Scenario action failed');
497
+ return payload;
498
+ }
499
+
500
+ function closeScenariosModal() {
501
+ els.scenariosModal.classList.remove('open');
502
+ }
503
+
504
+ // Render Logic
505
+ function renderList() {
506
+ const query = els.searchInput.value.toLowerCase();
507
+ const list = events.filter(e => {
508
+ const path = String(e.path ?? '');
509
+ const provider = String(e.provider ?? '');
510
+ const requestText = safeStringify(e.request).toLowerCase();
511
+ const matchesSearch = path.toLowerCase().includes(query) ||
512
+ provider.toLowerCase().includes(query) ||
513
+ requestText.includes(query);
514
+ const matchesFilter = currentFilter === 'all' || provider === currentFilter || provider.startsWith(`${currentFilter}:`);
515
+ return matchesSearch && matchesFilter;
516
+ });
517
+
518
+ els.eventCount.textContent = list.length;
519
+ els.eventList.innerHTML = '';
520
+
521
+ const frag = document.createDocumentFragment();
522
+ list.forEach(ev => {
523
+ const div = document.createElement('div');
524
+ div.className = `event-card ${ev.id === activeId ? 'active' : ''}`;
525
+ div.onclick = () => selectEvent(ev.id);
526
+
527
+ const method = String(ev.method ?? 'GET');
528
+ const statusCode = Number(ev.statusCode ?? 0);
529
+ const top = document.createElement('div');
530
+ top.className = 'ec-top';
531
+ const methodEl = document.createElement('span');
532
+ methodEl.className = `ec-method m-${method.toLowerCase()}`;
533
+ methodEl.textContent = method;
534
+ const time = document.createElement('span');
535
+ time.className = 'ec-time';
536
+ time.textContent = new Date(ev.timestamp ?? Date.now()).toLocaleTimeString();
537
+ top.append(methodEl, time);
538
+
539
+ const path = document.createElement('div');
540
+ path.className = 'ec-path';
541
+ path.textContent = String(ev.path ?? '');
542
+
543
+ const bottom = document.createElement('div');
544
+ bottom.className = 'ec-bottom';
545
+ const status = document.createElement('span');
546
+ status.className = `pill pill-status-${strType(statusCode)}`;
547
+ status.textContent = String(statusCode || 'n/a');
548
+ const duration = document.createElement('span');
549
+ duration.className = 'pill';
550
+ duration.textContent = `${Number(ev.durationMs ?? 0)}ms`;
551
+ const provider = document.createElement('span');
552
+ provider.className = 'pill provider-pill';
553
+ provider.textContent = String(ev.provider ?? 'unknown');
554
+ bottom.append(status, duration, provider);
555
+
556
+ div.append(top, path, bottom);
557
+ frag.appendChild(div);
558
+ });
559
+ els.eventList.appendChild(frag);
560
+ }
561
+
562
+ function selectEvent(id) {
563
+ activeId = id;
564
+ renderList();
565
+ const ev = events.find(e => e.id === id);
566
+ if (!ev) return;
567
+
568
+ els.emptyDetails.style.display = 'none';
569
+ els.detailsContent.classList.add('visible');
570
+
571
+ els.dMethod.textContent = ev.method;
572
+ els.dMethod.className = `ec-method m-${ev.method.toLowerCase()}`;
573
+ els.dPath.textContent = ev.path;
574
+
575
+ els.dStatus.textContent = ev.statusCode;
576
+ els.dStatus.className = `pill pill-status-${strType(ev.statusCode)}`;
577
+
578
+ els.dTime.textContent = `${ev.durationMs}ms`;
579
+ els.dProvider.textContent = ev.provider;
580
+ els.dSource.textContent = ev.source;
581
+
582
+ els.dReq.innerHTML = highlight(ev.request);
583
+ els.dRes.innerHTML = highlight(ev.response);
584
+ }
585
+
586
+ window.generateSelectedTest = async () => {
587
+ if (!activeId) return;
588
+ try {
589
+ const res = await fetch(`/api/events/${encodeURIComponent(activeId)}/test`);
590
+ const payload = await res.json();
591
+ if (!res.ok) throw new Error(payload?.error?.message || 'Unable to generate test');
592
+ els.rulesModal.classList.add('open');
593
+ els.rulesTitle.textContent = 'Generated Vitest Test';
594
+ els.rulesOutput.textContent = payload.content;
595
+ const copied = await copyText(payload.content);
596
+ els.rulesSubtitle.textContent = `${payload.filename} · ${copied ? 'Copied to clipboard' : 'Copy failed'}`;
597
+ } catch (e) {
598
+ els.rulesModal.classList.add('open');
599
+ els.rulesTitle.textContent = 'Generated Vitest Test';
600
+ els.rulesSubtitle.textContent = 'Test generation failed';
601
+ els.rulesOutput.textContent = `Test generation failed: ${e.message}`;
602
+ }
603
+ };
604
+
605
+ // Utils
606
+ function strType(code) {
607
+ if (code >= 200 && code < 300) return '200';
608
+ if (code >= 300 && code < 400) return '300';
609
+ if (code >= 400 && code < 500) return '400';
610
+ return '500';
611
+ }
612
+
613
+ function highlight(obj) {
614
+ if (!obj) return '{}';
615
+ const str = JSON.stringify(obj, null, 2);
616
+ const scaped = str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
617
+ return scaped.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => {
618
+ let cls = 'j-num';
619
+ if (/^"/.test(match)) { cls = /:$/.test(match) ? 'j-key' : 'j-str'; }
620
+ else if (/true|false/.test(match)) cls = 'j-bool';
621
+ else if (/null/.test(match)) cls = 'j-null';
622
+ return `<span class="${cls}">${match}</span>`;
623
+ });
624
+ }
625
+
626
+ function copy(type) {
627
+ const text = (type === 'req') ? els.dReq.textContent : els.dRes.textContent;
628
+ copyText(text);
629
+ }
630
+
631
+ window.closeRulesModal = closeRulesModal;
632
+ window.copyRules = copyRules;
633
+ window.closeSetupModal = closeSetupModal;
634
+ window.copySetupOutput = copySetupOutput;
635
+ window.closeScenariosModal = closeScenariosModal;
636
+
637
+ function escapeHtml(value) {
638
+ return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
639
+ }
640
+
641
+ function formatSafetyReport(report) {
642
+ const findings = report.findings.length > 0
643
+ ? report.findings.map((finding) => `- ${finding.severity.toUpperCase()} ${finding.file ? `${finding.file}: ` : ''}${finding.message}`).join('\n')
644
+ : '- No high-risk provider usage found.';
645
+ return [
646
+ '# GhostAPI Safety Report',
647
+ '',
648
+ `Project: ${report.projectRoot}`,
649
+ `Detected SDKs: ${report.detected.length > 0 ? report.detected.join(', ') : 'none'}`,
650
+ '',
651
+ '## Findings',
652
+ findings,
653
+ '',
654
+ '## Recommendations',
655
+ ...report.recommendations.map((item) => `- ${item}`)
656
+ ].join('\n');
657
+ }
658
+
659
+ function safeStringify(value) {
660
+ try {
661
+ return JSON.stringify(value ?? '');
662
+ } catch {
663
+ return '';
664
+ }
665
+ }
666
+
667
+ function flashButton(button, message) {
668
+ if (!button) return;
669
+ const original = button.dataset.originalLabel ?? button.textContent;
670
+ button.dataset.originalLabel = original;
671
+ button.textContent = message;
672
+ setTimeout(() => { button.textContent = original; }, 1400);
673
+ }
674
+
675
+ async function copyText(text) {
676
+ try {
677
+ await navigator.clipboard.writeText(text ?? '');
678
+ return true;
679
+ } catch (error) {
680
+ console.warn('Clipboard copy failed', error);
681
+ return false;
682
+ }
683
+ }
684
+
685
+ init();