@stacksjs/ts-cloud 0.7.56 → 0.7.58

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 (467) hide show
  1. package/dist/alerts/alerts.test.d.ts +1 -0
  2. package/dist/alerts/delivery.d.ts +45 -0
  3. package/dist/alerts/evaluator.d.ts +12 -0
  4. package/dist/alerts/health.d.ts +34 -0
  5. package/dist/alerts/index.d.ts +6 -0
  6. package/dist/alerts/model.d.ts +170 -0
  7. package/dist/alerts/store.d.ts +90 -0
  8. package/dist/alerts/telemetry.d.ts +3 -0
  9. package/dist/api/client.d.ts +197 -0
  10. package/dist/api/handler.d.ts +18 -0
  11. package/dist/api/handler.test.d.ts +1 -0
  12. package/dist/api/index.d.ts +6 -0
  13. package/dist/api/openapi.d.ts +2 -0
  14. package/dist/api/service.d.ts +28 -0
  15. package/dist/api/types.d.ts +119 -0
  16. package/dist/auth/encryption.d.ts +3 -0
  17. package/dist/auth/index.d.ts +8 -0
  18. package/dist/auth/mailer.d.ts +9 -0
  19. package/dist/auth/oidc.d.ts +36 -0
  20. package/dist/auth/oidc.test.d.ts +1 -0
  21. package/dist/auth/store.d.ts +121 -0
  22. package/dist/auth/store.test.d.ts +1 -0
  23. package/dist/auth/totp.d.ts +11 -0
  24. package/dist/auth/totp.test.d.ts +1 -0
  25. package/dist/auth/types.d.ts +138 -0
  26. package/dist/automation/index.d.ts +2 -0
  27. package/dist/automation/store.d.ts +46 -0
  28. package/dist/automation/store.test.d.ts +1 -0
  29. package/dist/automation/types.d.ts +66 -0
  30. package/dist/aws/acm.d.ts +0 -4
  31. package/dist/aws/application-autoscaling.d.ts +0 -5
  32. package/dist/aws/backup.d.ts +42 -0
  33. package/dist/aws/bedrock.d.ts +0 -5
  34. package/dist/aws/client.d.ts +3 -6
  35. package/dist/aws/cloudformation.d.ts +0 -4
  36. package/dist/aws/cloudfront-media.test.d.ts +1 -0
  37. package/dist/aws/cloudfront-origin.test.d.ts +1 -0
  38. package/dist/aws/cloudfront.d.ts +87 -3
  39. package/dist/aws/cloudwatch-logs.d.ts +2 -4
  40. package/dist/aws/cloudwatch.d.ts +1 -1
  41. package/dist/aws/comprehend.d.ts +0 -5
  42. package/dist/aws/connect.d.ts +0 -4
  43. package/dist/aws/cost-explorer-cache.d.ts +1 -15
  44. package/dist/aws/cost-explorer.d.ts +18 -9
  45. package/dist/aws/cost-explorer.test.d.ts +1 -0
  46. package/dist/aws/dynamodb.d.ts +0 -4
  47. package/dist/aws/ec2.d.ts +0 -4
  48. package/dist/aws/ecr.d.ts +0 -4
  49. package/dist/aws/ecs.d.ts +17 -7
  50. package/dist/aws/efs.d.ts +0 -4
  51. package/dist/aws/elasticache.d.ts +0 -4
  52. package/dist/aws/elbv2.d.ts +0 -7
  53. package/dist/aws/email.d.ts +0 -10
  54. package/dist/aws/eventbridge.d.ts +0 -4
  55. package/dist/aws/iam.d.ts +1 -5
  56. package/dist/aws/imap-server.d.ts +0 -7
  57. package/dist/aws/index.d.ts +4 -3
  58. package/dist/aws/index.js +31 -31
  59. package/dist/aws/kendra.d.ts +0 -5
  60. package/dist/aws/lambda.d.ts +3 -5
  61. package/dist/aws/opensearch.d.ts +0 -4
  62. package/dist/aws/personalize.d.ts +0 -5
  63. package/dist/aws/polly.d.ts +0 -5
  64. package/dist/aws/rds.d.ts +59 -4
  65. package/dist/aws/rekognition.d.ts +0 -5
  66. package/dist/aws/route53-domains.d.ts +0 -4
  67. package/dist/aws/route53.d.ts +0 -4
  68. package/dist/aws/s3.d.ts +10 -6
  69. package/dist/aws/scheduler.d.ts +0 -4
  70. package/dist/aws/secrets-manager.d.ts +0 -4
  71. package/dist/aws/setup-sms.d.ts +0 -14
  72. package/dist/aws/sms.d.ts +0 -10
  73. package/dist/aws/smtp-server.d.ts +0 -11
  74. package/dist/aws/sns.d.ts +0 -4
  75. package/dist/aws/sqs.d.ts +0 -4
  76. package/dist/aws/ssm.d.ts +0 -4
  77. package/dist/aws/sts.d.ts +0 -4
  78. package/dist/aws/support.d.ts +0 -4
  79. package/dist/aws/textract.d.ts +0 -5
  80. package/dist/aws/transcribe.d.ts +0 -4
  81. package/dist/aws/translate.d.ts +0 -5
  82. package/dist/aws/voice.d.ts +0 -11
  83. package/dist/aws/xray.d.ts +1 -0
  84. package/dist/backups/aws-database-source.d.ts +19 -0
  85. package/dist/backups/aws-database-source.test.d.ts +1 -0
  86. package/dist/backups/aws-infrastructure-source.d.ts +43 -0
  87. package/dist/backups/aws-infrastructure-source.test.d.ts +1 -0
  88. package/dist/backups/control-plane-source.d.ts +16 -0
  89. package/dist/backups/control-plane-source.test.d.ts +1 -0
  90. package/dist/backups/docker-volume-source.d.ts +31 -0
  91. package/dist/backups/docker-volume-source.test.d.ts +1 -0
  92. package/dist/backups/filesystem-source.d.ts +34 -0
  93. package/dist/backups/filesystem-source.test.d.ts +1 -0
  94. package/dist/backups/index.d.ts +10 -0
  95. package/dist/backups/logical-database-source.d.ts +16 -0
  96. package/dist/backups/logical-database-source.test.d.ts +1 -0
  97. package/dist/backups/model.d.ts +121 -0
  98. package/dist/backups/s3-destination.d.ts +85 -0
  99. package/dist/backups/s3-destination.test.d.ts +1 -0
  100. package/dist/backups/service.d.ts +80 -0
  101. package/dist/backups/service.test.d.ts +1 -0
  102. package/dist/backups/store.d.ts +30 -0
  103. package/dist/backups/store.test.d.ts +1 -0
  104. package/dist/bin/cli.js +3360 -1802
  105. package/dist/{chunk-tjjgajbh.js → chunk-01d86gt1.js} +301 -9
  106. package/dist/{chunk-73vcqg6z.js → chunk-0z43x8re.js} +7 -7
  107. package/dist/{chunk-tnztxpcb.js → chunk-32e7ya18.js} +61 -49
  108. package/dist/{chunk-qpj3edwz.js → chunk-4cjrg98a.js} +3 -7
  109. package/dist/{chunk-hsk6fe6x.js → chunk-50jpda9q.js} +2 -2
  110. package/dist/{chunk-93hjhs78.js → chunk-703nkybg.js} +1 -1
  111. package/dist/{chunk-d5vqwj8s.js → chunk-9m6b4m74.js} +755 -720
  112. package/dist/chunk-e05phx5f.js +10 -0
  113. package/dist/{chunk-f29w04w5.js → chunk-exbcsab2.js} +10 -4
  114. package/dist/{chunk-eq08r166.js → chunk-f55nkpvb.js} +2 -2
  115. package/dist/{chunk-3bfv1z4v.js → chunk-fv1bj3yt.js} +63 -56
  116. package/dist/chunk-fw3q88wk.js +48641 -0
  117. package/dist/{chunk-3knnr7wh.js → chunk-he9a874b.js} +170 -2
  118. package/dist/{chunk-zn0nxxa8.js → chunk-hpv68b4a.js} +1070 -1070
  119. package/dist/chunk-k114qwg9.js +12 -0
  120. package/dist/{chunk-r16e02bc.js → chunk-vdk53yyq.js} +3900 -4101
  121. package/dist/{chunk-jp2a8yad.js → chunk-vvkd46k1.js} +15 -13
  122. package/dist/{chunk-vd87cpvn.js → chunk-wj3s95p9.js} +62 -46
  123. package/dist/{chunk-mj1tmhte.js → chunk-x9s0cqfx.js} +1 -1
  124. package/dist/{chunk-3rsfns7x.js → chunk-ybcz6sxc.js} +2048 -387
  125. package/dist/{chunk-arsh1g5h.js → chunk-zqtpg06c.js} +14 -11
  126. package/dist/compose/index.d.ts +6 -0
  127. package/dist/compose/parser.d.ts +13 -0
  128. package/dist/compose/parser.test.d.ts +1 -0
  129. package/dist/compose/runtime.d.ts +10 -0
  130. package/dist/compose/service.d.ts +18 -0
  131. package/dist/compose/store.d.ts +59 -0
  132. package/dist/compose/store.test.d.ts +1 -0
  133. package/dist/compose/templates.d.ts +17 -0
  134. package/dist/compose/types.d.ts +164 -0
  135. package/dist/configuration/backends.d.ts +66 -0
  136. package/dist/configuration/backends.test.d.ts +1 -0
  137. package/dist/configuration/dotenv.d.ts +14 -0
  138. package/dist/configuration/dotenv.test.d.ts +1 -0
  139. package/dist/configuration/index.d.ts +6 -0
  140. package/dist/configuration/migrate.d.ts +10 -0
  141. package/dist/configuration/migrate.test.d.ts +1 -0
  142. package/dist/configuration/model.d.ts +50 -0
  143. package/dist/configuration/service.d.ts +144 -0
  144. package/dist/configuration/service.test.d.ts +1 -0
  145. package/dist/configuration/store.d.ts +33 -0
  146. package/dist/configuration/store.test.d.ts +1 -0
  147. package/dist/control-plane/authorization.d.ts +24 -0
  148. package/dist/control-plane/authorization.test.d.ts +1 -0
  149. package/dist/control-plane/index.d.ts +8 -0
  150. package/dist/control-plane/migrations.d.ts +7 -0
  151. package/dist/control-plane/search.d.ts +25 -0
  152. package/dist/control-plane/store.d.ts +117 -0
  153. package/dist/control-plane/store.test.d.ts +1 -0
  154. package/dist/control-plane/types.d.ts +381 -0
  155. package/dist/cost/reporting.d.ts +24 -0
  156. package/dist/cost/reporting.test.d.ts +1 -0
  157. package/dist/cost/resource-inventory.d.ts +32 -0
  158. package/dist/cost/resource-inventory.test.d.ts +1 -0
  159. package/dist/cost/resource-optimization.d.ts +59 -0
  160. package/dist/cost/resource-optimization.test.d.ts +1 -0
  161. package/dist/data-services/adapters.d.ts +39 -0
  162. package/dist/data-services/aws-transports.d.ts +29 -0
  163. package/dist/data-services/aws-transports.test.d.ts +1 -0
  164. package/dist/data-services/capabilities.d.ts +2 -0
  165. package/dist/data-services/data-services.test.d.ts +1 -0
  166. package/dist/data-services/docker-transport.d.ts +55 -0
  167. package/dist/data-services/docker-transport.test.d.ts +1 -0
  168. package/dist/data-services/index.d.ts +8 -0
  169. package/dist/data-services/model.d.ts +79 -0
  170. package/dist/data-services/secrets.d.ts +13 -0
  171. package/dist/data-services/secrets.test.d.ts +1 -0
  172. package/dist/data-services/service.d.ts +51 -0
  173. package/dist/data-services/store.d.ts +17 -0
  174. package/dist/deploy/container-image.d.ts +28 -0
  175. package/dist/deploy/container-image.test.d.ts +1 -0
  176. package/dist/deploy/dashboard-audit.test.d.ts +1 -0
  177. package/dist/deploy/dashboard-auth.d.ts +4 -23
  178. package/dist/deploy/dashboard-backups.integration.test.d.ts +1 -0
  179. package/dist/deploy/dashboard-configuration.integration.test.d.ts +1 -0
  180. package/dist/deploy/dashboard-control-plane.d.ts +37 -0
  181. package/dist/deploy/dashboard-control-plane.test.d.ts +1 -0
  182. package/dist/deploy/dashboard-data-services.integration.test.d.ts +1 -0
  183. package/dist/deploy/dashboard-environment-scope.test.d.ts +1 -0
  184. package/dist/deploy/dashboard-guard.d.ts +12 -0
  185. package/dist/deploy/dashboard-guard.test.d.ts +1 -0
  186. package/dist/deploy/dashboard-identities.d.ts +15 -0
  187. package/dist/deploy/dashboard-identities.test.d.ts +1 -0
  188. package/dist/deploy/dashboard-jobs.integration.test.d.ts +1 -0
  189. package/dist/deploy/dashboard-login-page.d.ts +5 -5
  190. package/dist/deploy/dashboard-login-page.test.d.ts +1 -0
  191. package/dist/deploy/dashboard-oidc.integration.test.d.ts +1 -0
  192. package/dist/deploy/dashboard-onboarding.integration.test.d.ts +1 -0
  193. package/dist/deploy/dashboard-policy.d.ts +4 -2
  194. package/dist/deploy/dashboard-queue-worker.integration.test.d.ts +1 -0
  195. package/dist/deploy/dashboard-release.integration.test.d.ts +1 -0
  196. package/dist/deploy/dashboard-route-manifest.d.ts +14 -0
  197. package/dist/deploy/dashboard-route-manifest.test.d.ts +1 -0
  198. package/dist/deploy/dashboard-security.integration.test.d.ts +1 -0
  199. package/dist/deploy/dashboard-session.d.ts +3 -14
  200. package/dist/deploy/dashboard-source.integration.test.d.ts +1 -0
  201. package/dist/deploy/dashboard-users.d.ts +3 -0
  202. package/dist/deploy/fullstack-container.d.ts +87 -0
  203. package/dist/deploy/fullstack-container.test.d.ts +1 -0
  204. package/dist/deploy/index.d.ts +7 -2
  205. package/dist/deploy/index.js +65 -25
  206. package/dist/deploy/local-dashboard-server.d.ts +30 -0
  207. package/dist/deploy/quick-deploy.d.ts +1 -1
  208. package/dist/deploy/serverless-operations.d.ts +1 -0
  209. package/dist/deploy/site-target.d.ts +16 -0
  210. package/dist/deploy/static-api-origin.d.ts +107 -0
  211. package/dist/deploy/static-api-origin.test.d.ts +1 -0
  212. package/dist/deploy/static-site-helper.d.ts +0 -16
  213. package/dist/deploy/telemetry-access.d.ts +5 -0
  214. package/dist/deploy/telemetry-access.test.d.ts +1 -0
  215. package/dist/deploy/telemetry-collection.d.ts +44 -0
  216. package/dist/deploy/telemetry-collection.test.d.ts +1 -0
  217. package/dist/dns/index.js +3 -3
  218. package/dist/dns/route53-adapter.d.ts +1 -1
  219. package/dist/drivers/hetzner/cloud-init.d.ts +1 -1
  220. package/dist/drivers/index.d.ts +2 -2
  221. package/dist/drivers/index.js +6 -6
  222. package/dist/drivers/shared/notifications.d.ts +1 -1
  223. package/dist/drivers/shared/rpx-gateway.d.ts +1 -0
  224. package/dist/fleet/fleet.test.d.ts +1 -0
  225. package/dist/fleet/index.d.ts +144 -0
  226. package/dist/fleet/queue.d.ts +3 -0
  227. package/dist/fleet/ssh-driver.d.ts +34 -0
  228. package/dist/index.d.ts +28 -4
  229. package/dist/index.js +1132 -424
  230. package/dist/jobs/adapters.d.ts +23 -0
  231. package/dist/jobs/index.d.ts +7 -0
  232. package/dist/jobs/jobs.test.d.ts +1 -0
  233. package/dist/jobs/model.d.ts +100 -0
  234. package/dist/jobs/provider-adapters.d.ts +49 -0
  235. package/dist/jobs/reconcile.d.ts +19 -0
  236. package/dist/jobs/schedule.d.ts +9 -0
  237. package/dist/jobs/service.d.ts +30 -0
  238. package/dist/jobs/store.d.ts +48 -0
  239. package/dist/maintenance/drivers.d.ts +117 -0
  240. package/dist/maintenance/index.d.ts +6 -0
  241. package/dist/maintenance/maintenance.test.d.ts +1 -0
  242. package/dist/maintenance/manifest.d.ts +15 -0
  243. package/dist/maintenance/queue.d.ts +9 -0
  244. package/dist/maintenance/service.d.ts +31 -0
  245. package/dist/maintenance/store.d.ts +84 -0
  246. package/dist/maintenance/types.d.ts +192 -0
  247. package/dist/media.d.ts +62 -0
  248. package/dist/media.test.d.ts +1 -0
  249. package/dist/object-storage/index.d.ts +0 -24
  250. package/dist/onboarding/archive.d.ts +12 -0
  251. package/dist/onboarding/artifact-store.d.ts +22 -0
  252. package/dist/onboarding/detection.d.ts +8 -0
  253. package/dist/onboarding/index.d.ts +9 -0
  254. package/dist/onboarding/migrations.d.ts +3 -0
  255. package/dist/onboarding/onboarding.test.d.ts +1 -0
  256. package/dist/onboarding/plan.d.ts +3 -0
  257. package/dist/onboarding/registry.d.ts +41 -0
  258. package/dist/onboarding/service.d.ts +16 -0
  259. package/dist/onboarding/store.d.ts +30 -0
  260. package/dist/onboarding/types.d.ts +204 -0
  261. package/dist/placement/builds.d.ts +20 -0
  262. package/dist/placement/drivers.d.ts +54 -0
  263. package/dist/placement/index.d.ts +6 -0
  264. package/dist/placement/placement.test.d.ts +1 -0
  265. package/dist/placement/queue.d.ts +4 -0
  266. package/dist/placement/service.d.ts +34 -0
  267. package/dist/placement/store.d.ts +49 -0
  268. package/dist/placement/types.d.ts +109 -0
  269. package/dist/preview/index.d.ts +3 -0
  270. package/dist/preview/service.d.ts +45 -0
  271. package/dist/preview/service.test.d.ts +1 -0
  272. package/dist/preview/store.d.ts +47 -0
  273. package/dist/preview/store.test.d.ts +1 -0
  274. package/dist/preview/types.d.ts +99 -0
  275. package/dist/push/apns.d.ts +0 -26
  276. package/dist/push/fcm.d.ts +0 -23
  277. package/dist/push/index.d.ts +2 -2
  278. package/dist/push/index.js +1 -1
  279. package/dist/queue/deployment-handlers.d.ts +27 -0
  280. package/dist/queue/deployment-handlers.test.d.ts +1 -0
  281. package/dist/queue/index.d.ts +4 -0
  282. package/dist/queue/queue.d.ts +57 -0
  283. package/dist/queue/queue.test.d.ts +1 -0
  284. package/dist/queue/types.d.ts +93 -0
  285. package/dist/queue/worker.d.ts +32 -0
  286. package/dist/queue/worker.test.d.ts +1 -0
  287. package/dist/regions/drivers.d.ts +139 -0
  288. package/dist/regions/index.d.ts +5 -0
  289. package/dist/regions/queue.d.ts +4 -0
  290. package/dist/regions/regions.test.d.ts +1 -0
  291. package/dist/regions/service.d.ts +23 -0
  292. package/dist/regions/store.d.ts +41 -0
  293. package/dist/regions/types.d.ts +132 -0
  294. package/dist/release/index.d.ts +6 -0
  295. package/dist/release/queue.d.ts +15 -0
  296. package/dist/release/queue.test.d.ts +1 -0
  297. package/dist/release/runtime.d.ts +35 -0
  298. package/dist/release/runtime.test.d.ts +1 -0
  299. package/dist/release/service.d.ts +28 -0
  300. package/dist/release/store.d.ts +88 -0
  301. package/dist/release/store.test.d.ts +1 -0
  302. package/dist/release/strategy.d.ts +8 -0
  303. package/dist/release/types.d.ts +103 -0
  304. package/dist/runtime/adapters/aws.d.ts +5 -0
  305. package/dist/runtime/adapters/docker.d.ts +53 -0
  306. package/dist/runtime/adapters/systemd.d.ts +26 -0
  307. package/dist/runtime/index.d.ts +9 -0
  308. package/dist/runtime/inventory.d.ts +6 -0
  309. package/dist/runtime/model.d.ts +130 -0
  310. package/dist/runtime/normalize.d.ts +5 -0
  311. package/dist/runtime/operations.d.ts +113 -0
  312. package/dist/runtime/service.d.ts +32 -0
  313. package/dist/runtime/streams.d.ts +49 -0
  314. package/dist/security/artifacts.d.ts +105 -0
  315. package/dist/security/artifacts.test.d.ts +1 -0
  316. package/dist/security/container-release.d.ts +35 -0
  317. package/dist/security/container-release.test.d.ts +1 -0
  318. package/dist/security/index.d.ts +8 -0
  319. package/dist/security/integration.d.ts +41 -0
  320. package/dist/security/integration.test.d.ts +1 -0
  321. package/dist/security/posture-store.d.ts +80 -0
  322. package/dist/security/posture-store.test.d.ts +1 -0
  323. package/dist/security/pre-deploy-scanner.d.ts +0 -4
  324. package/dist/security/scanners.d.ts +81 -0
  325. package/dist/security/scanners.test.d.ts +1 -0
  326. package/dist/security/types.d.ts +150 -0
  327. package/dist/source/git-workspace.d.ts +26 -0
  328. package/dist/source/git-workspace.test.d.ts +1 -0
  329. package/dist/source/index.d.ts +6 -0
  330. package/dist/source/providers.d.ts +199 -0
  331. package/dist/source/providers.test.d.ts +1 -0
  332. package/dist/source/service.d.ts +18 -0
  333. package/dist/source/store.d.ts +149 -0
  334. package/dist/source/store.test.d.ts +1 -0
  335. package/dist/source/types.d.ts +191 -0
  336. package/dist/source/webhooks.d.ts +33 -0
  337. package/dist/source/webhooks.test.d.ts +1 -0
  338. package/dist/ssl/acme-client.d.ts +0 -6
  339. package/dist/storage/compose.d.ts +4 -0
  340. package/dist/storage/compose.test.d.ts +1 -0
  341. package/dist/storage/drivers.d.ts +107 -0
  342. package/dist/storage/drivers.test.d.ts +1 -0
  343. package/dist/storage/index.d.ts +5 -0
  344. package/dist/storage/model.d.ts +112 -0
  345. package/dist/storage/service.d.ts +95 -0
  346. package/dist/storage/storage.test.d.ts +1 -0
  347. package/dist/storage/store.d.ts +40 -0
  348. package/dist/telemetry/correlation.test.d.ts +1 -0
  349. package/dist/telemetry/index.d.ts +4 -0
  350. package/dist/telemetry/model.d.ts +145 -0
  351. package/dist/telemetry/policy.d.ts +8 -0
  352. package/dist/telemetry/policy.test.d.ts +1 -0
  353. package/dist/telemetry/redaction.d.ts +9 -0
  354. package/dist/telemetry/store.d.ts +34 -0
  355. package/dist/telemetry/store.test.d.ts +1 -0
  356. package/dist/ui/404.html +1506 -17
  357. package/dist/ui/accept-invitation.html +1163 -0
  358. package/dist/ui/{server/backups.html → access-denied.html} +344 -55
  359. package/dist/ui/account/automation.html +1669 -0
  360. package/dist/ui/account/security.html +1646 -0
  361. package/dist/ui/applications/compose.html +1590 -0
  362. package/dist/ui/applications/new.html +1868 -0
  363. package/dist/ui/data/backups.html +1757 -0
  364. package/dist/ui/data/services.html +1723 -0
  365. package/dist/ui/data/volumes.html +1639 -0
  366. package/dist/ui/index.html +392 -29
  367. package/dist/ui/integrations.html +1865 -0
  368. package/dist/ui/operations/alerts.html +1610 -0
  369. package/dist/ui/operations/configuration.html +1746 -0
  370. package/dist/ui/operations/jobs.html +1709 -0
  371. package/dist/ui/operations/maintenance.html +1540 -0
  372. package/dist/ui/operations/observability.html +1742 -0
  373. package/dist/ui/{serverless/scheduler.html → operations/previews.html} +447 -46
  374. package/dist/ui/operations/queue.html +1615 -0
  375. package/dist/ui/{server/workers.html → operations/regions.html} +413 -49
  376. package/dist/ui/operations/releases.html +1596 -0
  377. package/dist/ui/operations/workloads.html +1775 -0
  378. package/dist/ui/security.html +1767 -0
  379. package/dist/ui/server/actions.html +446 -57
  380. package/dist/ui/server/activity.html +346 -1
  381. package/dist/ui/server/capacity.html +1542 -0
  382. package/dist/ui/server/database.html +443 -36
  383. package/dist/ui/server/deployments.html +439 -53
  384. package/dist/ui/server/diagnostics.html +346 -1
  385. package/dist/ui/server/firewall.html +476 -50
  386. package/dist/ui/server/fleet.html +1577 -0
  387. package/dist/ui/server/logs.html +389 -25
  388. package/dist/ui/server/metrics.html +346 -1
  389. package/dist/ui/server/security.html +346 -1
  390. package/dist/ui/server/services.html +421 -46
  391. package/dist/ui/server/sites.html +476 -95
  392. package/dist/ui/server/ssh-keys.html +476 -46
  393. package/dist/ui/server/team.html +700 -77
  394. package/dist/ui/server/terminal.html +444 -54
  395. package/dist/ui/serverless/alarms.html +430 -42
  396. package/dist/ui/serverless/assets.html +421 -46
  397. package/dist/ui/serverless/cost.html +346 -1
  398. package/dist/ui/serverless/data.html +422 -47
  399. package/dist/ui/serverless/deployments.html +421 -46
  400. package/dist/ui/serverless/firewall.html +346 -1
  401. package/dist/ui/serverless/functions.html +423 -48
  402. package/dist/ui/serverless/logs.html +389 -25
  403. package/dist/ui/serverless/metrics.html +346 -1
  404. package/dist/ui/serverless/queues.html +448 -62
  405. package/dist/ui/serverless/secrets.html +448 -42
  406. package/dist/ui/serverless/traces.html +350 -5
  407. package/dist/ui/serverless.html +434 -58
  408. package/dist/ui-src/pages/404.stx +18 -0
  409. package/dist/ui-src/pages/accept-invitation.stx +50 -0
  410. package/dist/ui-src/pages/access-denied.stx +18 -0
  411. package/dist/ui-src/pages/account/automation.stx +187 -0
  412. package/dist/ui-src/pages/account/security.stx +249 -0
  413. package/dist/ui-src/pages/applications/compose.stx +38 -0
  414. package/dist/ui-src/pages/applications/new.stx +76 -0
  415. package/dist/ui-src/pages/data/backups.stx +214 -0
  416. package/dist/ui-src/pages/data/services.stx +257 -0
  417. package/dist/ui-src/pages/data/volumes.stx +40 -0
  418. package/dist/ui-src/pages/index.stx +24 -10
  419. package/dist/ui-src/pages/integrations.stx +186 -0
  420. package/dist/ui-src/pages/operations/alerts.stx +40 -0
  421. package/dist/ui-src/pages/operations/configuration.stx +164 -0
  422. package/dist/ui-src/pages/operations/jobs.stx +238 -0
  423. package/dist/ui-src/pages/operations/maintenance.stx +15 -0
  424. package/dist/ui-src/pages/operations/observability.stx +76 -0
  425. package/dist/ui-src/pages/operations/previews.stx +62 -0
  426. package/dist/ui-src/pages/operations/queue.stx +158 -0
  427. package/dist/ui-src/pages/operations/regions.stx +12 -0
  428. package/dist/ui-src/pages/operations/releases.stx +39 -0
  429. package/dist/ui-src/pages/operations/workloads.stx +151 -0
  430. package/dist/ui-src/pages/partials/head.stx +148 -15
  431. package/dist/ui-src/pages/partials/nav.stx +491 -74
  432. package/dist/ui-src/pages/partials/op-confirm.stx +6 -6
  433. package/dist/ui-src/pages/security.stx +264 -0
  434. package/dist/ui-src/pages/server/actions.stx +17 -26
  435. package/dist/ui-src/pages/server/capacity.stx +16 -0
  436. package/dist/ui-src/pages/server/database.stx +31 -16
  437. package/dist/ui-src/pages/server/deployments.stx +28 -30
  438. package/dist/ui-src/pages/server/firewall.stx +25 -30
  439. package/dist/ui-src/pages/server/fleet.stx +10 -0
  440. package/dist/ui-src/pages/server/logs.stx +11 -9
  441. package/dist/ui-src/pages/server/metrics.stx +8 -16
  442. package/dist/ui-src/pages/server/services.stx +19 -28
  443. package/dist/ui-src/pages/server/sites.stx +75 -72
  444. package/dist/ui-src/pages/server/ssh-keys.stx +24 -30
  445. package/dist/ui-src/pages/server/team.stx +286 -149
  446. package/dist/ui-src/pages/server/terminal.stx +6 -28
  447. package/dist/ui-src/pages/serverless/alarms.stx +17 -31
  448. package/dist/ui-src/pages/serverless/assets.stx +13 -22
  449. package/dist/ui-src/pages/serverless/cost.stx +15 -3
  450. package/dist/ui-src/pages/serverless/data.stx +12 -25
  451. package/dist/ui-src/pages/serverless/deployments.stx +14 -23
  452. package/dist/ui-src/pages/serverless/firewall.stx +5 -1
  453. package/dist/ui-src/pages/serverless/functions.stx +20 -26
  454. package/dist/ui-src/pages/serverless/logs.stx +11 -10
  455. package/dist/ui-src/pages/serverless/metrics.stx +18 -17
  456. package/dist/ui-src/pages/serverless/queues.stx +34 -42
  457. package/dist/ui-src/pages/serverless/secrets.stx +31 -34
  458. package/dist/ui-src/pages/serverless/traces.stx +2 -6
  459. package/dist/ui-src/pages/serverless.stx +40 -74
  460. package/package.json +11 -11
  461. package/dist/chunk-b17vf1g1.js +0 -13787
  462. package/dist/chunk-b82pbxyp.js +0 -1572
  463. package/dist/chunk-pqfzdg68.js +0 -12
  464. package/dist/chunk-z58fqnjm.js +0 -10
  465. package/dist/ui-src/pages/server/backups.stx +0 -91
  466. package/dist/ui-src/pages/server/workers.stx +0 -99
  467. package/dist/ui-src/pages/serverless/scheduler.stx +0 -89
@@ -0,0 +1,264 @@
1
+ <script server>
2
+ let __d = null
3
+ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
4
+ const mode = __d?.mode ?? 'server'
5
+ const project = __d?.project ?? { name: 'Project' }
6
+ const environment = __d?.environment ?? 'production'
7
+ </script>
8
+ <!DOCTYPE html>
9
+ <html lang="en" data-theme="dark">
10
+ <head>
11
+ @include('./partials/head', { title: 'ts-cloud - Security posture' })
12
+ <style>
13
+ .security-hero { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:24px; align-items:start }
14
+ .security-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:8px }
15
+ .posture-score { font-size:2.5rem; line-height:1; font-weight:760; letter-spacing:-.05em }
16
+ .severity-grid { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:10px }
17
+ .severity-card { border-left:3px solid var(--line); min-height:110px }
18
+ .severity-card.critical { border-color:#ff5f68 }.severity-card.high { border-color:#f39b52 }.severity-card.medium { border-color:#e4c35e }.severity-card.low { border-color:#65a2ee }.severity-card.info { border-color:#8290a4 }
19
+ .severity-value { display:block; font-size:1.8rem; font-weight:720; margin-top:12px }
20
+ .security-layout { display:grid; grid-template-columns:minmax(0,2fr) minmax(290px,1fr); gap:18px; align-items:start }
21
+ .security-sidebar { display:grid; gap:18px; position:sticky; top:20px }
22
+ .filter-bar { display:grid; grid-template-columns:minmax(180px,1fr) repeat(3,minmax(130px,.4fr)); gap:8px; margin-bottom:12px }
23
+ .filter-bar input,.filter-bar select,.finding-actions select,#policy-form input,#policy-form select,.security-dialog input,.security-dialog textarea { width:100%; min-width:0; background:rgba(255,255,255,.045); border:1px solid var(--panel-br); border-radius:8px; padding:9px 10px; color:var(--txt); font:inherit }
24
+ .filter-bar input:focus,.filter-bar select:focus,.finding-actions select:focus,#policy-form input:focus,#policy-form select:focus,.security-dialog input:focus,.security-dialog textarea:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px rgba(90,139,224,.12) }
25
+ .finding-list,.scanner-list,.decision-list { display:grid; gap:10px }
26
+ .finding { border:1px solid var(--line); border-radius:10px; padding:16px; background:var(--panel); display:grid; gap:12px }
27
+ .finding-head { display:grid; grid-template-columns:auto minmax(0,1fr) auto; gap:10px; align-items:start }
28
+ .severity-dot { width:10px; height:10px; border-radius:50%; margin-top:6px; background:#8290a4 }
29
+ .severity-dot.critical { background:#ff5f68 }.severity-dot.high { background:#f39b52 }.severity-dot.medium { background:#e4c35e }.severity-dot.low { background:#65a2ee }
30
+ .finding h3 { margin:0; font-size:1rem }.finding p { margin:3px 0 0 }
31
+ .finding-meta { display:flex; gap:7px; flex-wrap:wrap; color:var(--muted); font-size:.78rem }
32
+ .finding-actions { display:flex; flex-wrap:wrap; align-items:center; gap:7px }
33
+ .finding-actions select { width:auto; min-width:150px }
34
+ .remediation { border-left:2px solid var(--accent); padding-left:10px; color:var(--muted) }
35
+ .evidence { white-space:pre-wrap; overflow-wrap:anywhere; max-height:150px; overflow:auto; font-size:.75rem; color:var(--muted) }
36
+ .scanner-row,.decision-row { border:1px solid var(--line); border-radius:8px; padding:12px; display:grid; gap:5px }
37
+ .scanner-top,.decision-top { display:flex; justify-content:space-between; gap:10px; align-items:center }
38
+ .policy-rules { display:grid; gap:7px; margin:12px 0 }
39
+ .policy-rule { display:grid; grid-template-columns:1fr 1fr; gap:7px }
40
+ #policy-form { display:grid; gap:10px } #policy-form label,.policy-rule label,.security-dialog label { display:grid; gap:6px; color:var(--txt3); font-size:.72rem; font-weight:750; letter-spacing:.04em }
41
+ #policy-form label input,#policy-form label select,.security-dialog label input,.security-dialog label textarea { color:var(--txt); font-weight:450; letter-spacing:0 }
42
+ .status-line { min-height:1.3em; color:var(--muted); margin-top:8px }
43
+ dialog.security-dialog { width:min(560px,calc(100vw - 32px)); border:1px solid var(--line); border-radius:12px; background:var(--panel); color:inherit; padding:22px }
44
+ dialog.security-dialog::backdrop { background:rgba(0,0,0,.68) }
45
+ dialog.security-dialog form { display:grid; gap:12px }.dialog-actions { display:flex; justify-content:flex-end; gap:8px }
46
+ .empty-security { padding:28px; text-align:center; border:1px dashed var(--line); border-radius:10px; color:var(--muted) }
47
+ @media (max-width:900px) { .security-layout,.security-hero { grid-template-columns:1fr }.security-sidebar { position:static }.security-actions { justify-content:flex-start }.severity-grid { grid-template-columns:repeat(2,1fr) }.filter-bar { grid-template-columns:1fr 1fr } }
48
+ @media (max-width:560px) { .severity-grid,.filter-bar { grid-template-columns:1fr }.finding-head { grid-template-columns:auto minmax(0,1fr) }.finding-head > :last-child { grid-column:2 } }
49
+ </style>
50
+ </head>
51
+ <body>
52
+ @include('./partials/nav', { active: 'security-posture', name: project.name, env: environment })
53
+ <div class="wrap" id="main-content">
54
+ <div class="crumbs"><a href="{{ mode === 'serverless' ? '/serverless' : '/' }}">{{ project.name }}</a> <span class="sep">/</span> Security posture</div>
55
+ <header class="page security-hero">
56
+ <div><h1>Security posture</h1><p>Findings, supply-chain checks, policy gates, and time-limited exceptions for <strong>{{ environment }}</strong>.</p></div>
57
+ <div class="security-actions">
58
+ <button class="btn ghost" id="export-posture" type="button">Export report</button>
59
+ <button class="btn ghost" id="review-change" type="button">Review production change</button>
60
+ <button class="btn" id="run-scans" type="button">Run checks</button>
61
+ </div>
62
+ </header>
63
+
64
+ <div class="section severity-grid" aria-label="Open findings by severity">
65
+ <div class="critical panel severity-card"><span class="note">Critical</span><strong class="severity-value" data-severity="critical">—</strong><small>Immediate action</small></div>
66
+ <div class="high panel severity-card"><span class="note">High</span><strong class="severity-value" data-severity="high">—</strong><small>Prioritize remediation</small></div>
67
+ <div class="medium panel severity-card"><span class="note">Medium</span><strong class="severity-value" data-severity="medium">—</strong><small>Review and schedule</small></div>
68
+ <div class="low panel severity-card"><span class="note">Low</span><strong class="severity-value" data-severity="low">—</strong><small>Defense in depth</small></div>
69
+ <div class="info panel severity-card"><span class="note">Waived</span><strong class="severity-value" id="waived-count">—</strong><small>Expires automatically</small></div>
70
+ </div>
71
+
72
+ <div class="section security-layout">
73
+ <main>
74
+ <div class="section-heading"><div><h2>Findings</h2><p class="note">Repeated observations update one lifecycle and preserve recurrence history.</p></div></div>
75
+ <div class="filter-bar" aria-label="Finding filters">
76
+ <input id="finding-search" type="search" placeholder="Search title, resource, scanner…" aria-label="Search findings">
77
+ <select id="severity-filter" aria-label="Severity"><option value="">All severities</option><option>critical</option><option>high</option><option>medium</option><option>low</option><option>info</option></select>
78
+ <select id="status-filter" aria-label="Status"><option value="">All statuses</option><option>open</option><option>acknowledged</option><option>waived</option><option>resolved</option></select>
79
+ <select id="scanner-filter" aria-label="Scanner"><option value="">All scanners</option></select>
80
+ </div>
81
+ <div class="finding-list" id="finding-list"><div class="empty-security">Loading security findings…</div></div>
82
+ </main>
83
+
84
+ <aside class="security-sidebar">
85
+ <section class="panel">
86
+ <div class="scanner-top"><h2>Deploy policy</h2><span class="pill" id="policy-version">—</span></div>
87
+ <form id="policy-form">
88
+ <label>Name<input name="name" maxlength="100" required></label>
89
+ <label>Scanner outage behavior<select name="scannerFailMode"><option value="closed">Fail closed</option><option value="open">Fail open with warning</option></select></label>
90
+ <label>Required scanners<input name="requiredScanners" placeholder="source-secrets, trivy-image"></label>
91
+ <div class="policy-rules">
92
+ <div class="policy-rule"><label>Critical<select name="critical"><option value="block">Block</option><option value="warn">Warn</option><option value="record">Record</option></select></label><label>High<select name="high"><option value="block">Block</option><option value="warn">Warn</option><option value="record">Record</option></select></label></div>
93
+ <div class="policy-rule"><label>Medium<select name="medium"><option value="warn">Warn</option><option value="block">Block</option><option value="record">Record</option></select></label><label>Low<select name="low"><option value="record">Record</option><option value="warn">Warn</option><option value="block">Block</option></select></label></div>
94
+ </div>
95
+ <button class="btn sm" type="submit">Save policy</button>
96
+ <p class="status-line" id="policy-status" role="status"></p>
97
+ </form>
98
+ </section>
99
+
100
+ <section class="panel">
101
+ <div class="scanner-top"><h2>Scanner health</h2><span class="pill" id="last-scan">—</span></div>
102
+ <div class="scanner-list" id="scanner-list"><div class="note">Loading checks…</div></div>
103
+ </section>
104
+
105
+ <section class="panel">
106
+ <h2>Recent decisions</h2>
107
+ <div class="decision-list" id="decision-list"><div class="note">No policy decisions yet.</div></div>
108
+ </section>
109
+ </aside>
110
+ </div>
111
+ <p class="status-line" id="page-status" role="status"></p>
112
+ </div>
113
+
114
+ <dialog class="security-dialog" id="waiver-dialog">
115
+ <form id="waiver-form">
116
+ <div><h2>Time-limited waiver</h2><p class="note" id="waiver-finding-title"></p></div>
117
+ <input type="hidden" name="findingId">
118
+ <label>Reason<textarea name="reason" minlength="8" maxlength="1000" rows="4" required placeholder="Why is accepting this risk necessary, and what is the remediation plan?"></textarea></label>
119
+ <label>Tracking link<input name="referenceUrl" type="url" placeholder="https://tickets.example/SEC-123"></label>
120
+ <label>Expires<input name="expiresAt" type="datetime-local" required></label>
121
+ <p class="note">Creating a waiver requires recent authentication and is recorded in the organization audit log.</p>
122
+ <div class="dialog-actions"><button class="btn ghost" type="button" data-close-dialog>Cancel</button><button class="btn" type="submit">Create waiver</button></div>
123
+ <p class="status-line" id="waiver-status" role="status"></p>
124
+ </form>
125
+ </dialog>
126
+
127
+ <dialog class="security-dialog" id="comment-dialog">
128
+ <form id="comment-form">
129
+ <div><h2>Add finding note</h2><p class="note" id="comment-finding-title"></p></div>
130
+ <input type="hidden" name="findingId">
131
+ <label>Note<textarea name="body" minlength="2" maxlength="4000" rows="4" required></textarea></label>
132
+ <label>Reference link<input name="referenceUrl" type="url" placeholder="https://tickets.example/SEC-123"></label>
133
+ <div class="dialog-actions"><button class="btn ghost" type="button" data-close-dialog>Cancel</button><button class="btn" type="submit">Save note</button></div>
134
+ <p class="status-line" id="comment-status" role="status"></p>
135
+ </form>
136
+ </dialog>
137
+
138
+ <script>
139
+ const state = { data: null }
140
+ const pageStatus = document.querySelector('#page-status')
141
+ const findingList = document.querySelector('#finding-list')
142
+ const scannerList = document.querySelector('#scanner-list')
143
+ const decisionList = document.querySelector('#decision-list')
144
+ const policyForm = document.querySelector('#policy-form')
145
+
146
+ async function request(path, options = {}) {
147
+ const response = await fetch(path, { credentials: 'same-origin', ...options })
148
+ const body = await response.json().catch(() => ({}))
149
+ if (!response.ok) {
150
+ const error = new Error(body.error || `Request failed (${response.status})`)
151
+ error.stepUpRequired = body.stepUpRequired
152
+ throw error
153
+ }
154
+ return body
155
+ }
156
+ function element(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text != null) node.textContent = text; return node }
157
+ function formatDate(value) { if (!value) return 'never'; const date = new Date(value); return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString() }
158
+ function statusTone(value) { return value === 'passed' || value === 'allow' || value === 'resolved' ? 'ok' : value === 'failed' || value === 'block' || value === 'critical' ? 'bad' : 'warn' }
159
+ function activePolicy() { return state.data?.policies?.find(policy => policy.environmentId === state.data.scope.environmentId) || state.data?.policies?.[0] }
160
+
161
+ function renderSummary() {
162
+ for (const severity of ['critical', 'high', 'medium', 'low']) document.querySelector(`[data-severity="${severity}"]`).textContent = state.data.summary.open[severity] || 0
163
+ document.querySelector('#waived-count').textContent = state.data.summary.waived || 0
164
+ document.querySelector('#last-scan').textContent = state.data.summary.lastScannedAt ? formatDate(state.data.summary.lastScannedAt) : 'not run'
165
+ }
166
+
167
+ function renderPolicy() {
168
+ const policy = activePolicy()
169
+ if (!policy) return
170
+ policyForm.elements.name.value = policy.name
171
+ policyForm.elements.scannerFailMode.value = policy.scannerFailMode
172
+ policyForm.elements.requiredScanners.value = policy.requiredScanners.join(', ')
173
+ for (const severity of ['critical', 'high', 'medium', 'low']) {
174
+ const rule = policy.rules.filter(rule => !rule.scannerId && ['info','low','medium','high','critical'].indexOf(severity) >= ['info','low','medium','high','critical'].indexOf(rule.minimumSeverity)).sort((a,b) => ['record','warn','block'].indexOf(b.action) - ['record','warn','block'].indexOf(a.action))[0]
175
+ if (rule) policyForm.elements[severity].value = rule.action
176
+ }
177
+ document.querySelector('#policy-version').textContent = `v${policy.version} · fail ${policy.scannerFailMode}`
178
+ }
179
+
180
+ function renderScanners() {
181
+ scannerList.replaceChildren()
182
+ const latest = new Map()
183
+ for (const scan of state.data.scans) if (!latest.has(scan.scannerId)) latest.set(scan.scannerId, scan)
184
+ const policy = activePolicy()
185
+ for (const required of policy?.requiredScanners || []) if (!latest.has(required)) latest.set(required, { scannerId: required, scannerVersion: 'unavailable', status: 'unavailable', error: 'Required check has not run.' })
186
+ if (!latest.size) { scannerList.append(element('div', 'note', 'No scanner results yet.')); return }
187
+ for (const scan of latest.values()) {
188
+ const row = element('div', 'scanner-row')
189
+ const top = element('div', 'scanner-top'); top.append(element('strong', '', scan.scannerId), element('span', `pill ${statusTone(scan.status)}`, scan.status)); row.append(top)
190
+ row.append(element('div', 'finding-meta', `version ${scan.scannerVersion} · ${formatDate(scan.completedAt)} · ${scan.findingsCount || 0} findings`))
191
+ if (scan.error) row.append(element('p', 'note', scan.error))
192
+ scannerList.append(row)
193
+ }
194
+ }
195
+
196
+ function findingMatches(finding) {
197
+ const query = document.querySelector('#finding-search').value.trim().toLowerCase()
198
+ const severity = document.querySelector('#severity-filter').value
199
+ const status = document.querySelector('#status-filter').value
200
+ const scanner = document.querySelector('#scanner-filter').value
201
+ return (!query || [finding.title, finding.subject, finding.scannerId, finding.ruleId, finding.description].join(' ').toLowerCase().includes(query)) && (!severity || finding.severity === severity) && (!status || finding.status === status) && (!scanner || finding.scannerId === scanner)
202
+ }
203
+
204
+ function renderFindings() {
205
+ findingList.replaceChildren()
206
+ const findings = state.data.findings.filter(findingMatches)
207
+ if (!findings.length) { findingList.append(element('div', 'empty-security', 'No findings match these filters.')); return }
208
+ for (const finding of findings) {
209
+ const card = element('article', 'finding')
210
+ const head = element('div', 'finding-head'); head.append(element('span', `severity-dot ${finding.severity}`))
211
+ const title = element('div'); title.append(element('h3', '', finding.title), element('p', 'note', finding.description)); head.append(title)
212
+ head.append(element('span', `pill ${statusTone(finding.status)}`, `${finding.severity} · ${finding.status}`)); card.append(head)
213
+ card.append(element('div', 'finding-meta', `${finding.scannerId} ${finding.scannerVersion} · ${finding.subject} · first ${formatDate(finding.firstSeenAt)} · last ${formatDate(finding.lastSeenAt)} · recurred ${finding.recurrenceCount}×`))
214
+ if (finding.remediation) card.append(element('div', 'remediation', finding.remediation))
215
+ const details = element('details'); details.append(element('summary', '', 'Sanitized evidence')); details.append(element('pre', 'evidence', JSON.stringify(finding.evidence, null, 2))); card.append(details)
216
+ const actions = element('div', 'finding-actions')
217
+ if (finding.status !== 'resolved' && finding.status !== 'acknowledged') {
218
+ const acknowledge = element('button', 'btn ghost sm', 'Acknowledge'); acknowledge.type = 'button'; acknowledge.onclick = () => updateFinding(finding.id, { action: 'acknowledge' }); actions.append(acknowledge)
219
+ }
220
+ const owners = element('select'); owners.setAttribute('aria-label', 'Finding owner'); owners.append(new Option('Unassigned', ''))
221
+ for (const actor of state.data.actors || []) owners.append(new Option(actor.displayName, actor.id, false, actor.id === finding.ownerActorId))
222
+ const assign = element('button', 'btn ghost sm', 'Assign'); assign.type = 'button'; assign.onclick = () => updateFinding(finding.id, { action: 'assign', ownerActorId: owners.value }); actions.append(owners, assign)
223
+ const note = element('button', 'btn ghost sm', 'Add note'); note.type = 'button'; note.onclick = () => openComment(finding); actions.append(note)
224
+ if (finding.status !== 'resolved') { const waive = element('button', 'btn ghost sm', 'Waive…'); waive.type = 'button'; waive.onclick = () => openWaiver(finding); actions.append(waive) }
225
+ card.append(actions); findingList.append(card)
226
+ }
227
+ }
228
+
229
+ function renderDecisions() {
230
+ decisionList.replaceChildren()
231
+ if (!state.data.decisions.length) { decisionList.append(element('div', 'note', 'No policy decisions yet.')); return }
232
+ for (const decision of state.data.decisions.slice(0, 8)) {
233
+ const row = element('div', 'decision-row'); const top = element('div', 'decision-top'); top.append(element('strong', '', formatDate(decision.createdAt)), element('span', `pill ${statusTone(decision.outcome)}`, decision.outcome)); row.append(top)
234
+ row.append(element('p', 'note', decision.explanation), element('div', 'finding-meta', `policy v${decision.policyVersion} · ${Object.entries(decision.scannerVersions).map(([key,value]) => `${key} ${value}`).join(' · ')}`)); decisionList.append(row)
235
+ }
236
+ }
237
+
238
+ function populateScannerFilter() {
239
+ const select = document.querySelector('#scanner-filter'); const current = select.value
240
+ select.replaceChildren(new Option('All scanners', ''))
241
+ for (const scanner of [...new Set(state.data.findings.map(finding => finding.scannerId))].sort()) select.append(new Option(scanner, scanner))
242
+ select.value = current
243
+ }
244
+
245
+ async function load() {
246
+ state.data = await request(`/api/security/posture${location.search}`)
247
+ renderSummary(); renderPolicy(); renderScanners(); populateScannerFilter(); renderFindings(); renderDecisions()
248
+ }
249
+ async function updateFinding(id, body) { try { await request('/api/security/findings', { method:'PATCH', headers:{'content-type':'application/json'}, body:JSON.stringify({ id, ...body }) }); await load() } catch (error) { pageStatus.textContent = error.message } }
250
+ function openWaiver(finding) { const dialog = document.querySelector('#waiver-dialog'); const form = document.querySelector('#waiver-form'); form.reset(); form.elements.findingId.value = finding.id; document.querySelector('#waiver-finding-title').textContent = finding.title; const expiry = new Date(Date.now() + 24*60*60*1000); form.elements.expiresAt.value = expiry.toISOString().slice(0,16); dialog.showModal() }
251
+ function openComment(finding) { const dialog = document.querySelector('#comment-dialog'); const form = document.querySelector('#comment-form'); form.reset(); form.elements.findingId.value = finding.id; document.querySelector('#comment-finding-title').textContent = finding.title; dialog.showModal() }
252
+
253
+ for (const selector of ['#finding-search','#severity-filter','#status-filter','#scanner-filter']) document.querySelector(selector).addEventListener('input', renderFindings)
254
+ for (const button of document.querySelectorAll('[data-close-dialog]')) button.addEventListener('click', () => button.closest('dialog').close())
255
+ document.querySelector('#run-scans').addEventListener('click', async event => { const button = event.currentTarget; button.disabled = true; pageStatus.textContent = 'Running local checks…'; try { await request(`/api/security/scan${location.search}`, { method:'POST' }); pageStatus.textContent = 'Checks complete.'; await load() } catch (error) { pageStatus.textContent = error.message } finally { button.disabled = false } })
256
+ document.querySelector('#review-change').addEventListener('click', async () => { pageStatus.textContent = 'Evaluating the current production-change policy…'; try { const result = await request(`/api/security/review${location.search}`, { method:'POST' }); pageStatus.textContent = `${result.decision.outcome.toUpperCase()}: ${result.decision.explanation}`; await load() } catch (error) { pageStatus.textContent = error.message } })
257
+ document.querySelector('#export-posture').addEventListener('click', async () => { try { const report = await request('/api/security/export'); const url = URL.createObjectURL(new Blob([JSON.stringify(report,null,2)], {type:'application/json'})); const link = document.createElement('a'); link.href=url; link.download=`ts-cloud-security-${new Date().toISOString().slice(0,10)}.json`; link.click(); URL.revokeObjectURL(url) } catch (error) { pageStatus.textContent=error.message } })
258
+ policyForm.addEventListener('submit', async event => { event.preventDefault(); const policy = activePolicy(); const data = new FormData(policyForm); const body = { id:policy.id, version:policy.version, name:data.get('name'), scannerFailMode:data.get('scannerFailMode'), requiredScanners:String(data.get('requiredScanners')||'').split(',').map(value=>value.trim()).filter(Boolean), rules:['critical','high','medium','low'].map(minimumSeverity=>({minimumSeverity,action:data.get(minimumSeverity)})) }; const status=document.querySelector('#policy-status'); try { await request('/api/security/policies',{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify(body)}); status.textContent='Policy saved and audited.'; await load() } catch(error) { status.textContent=error.stepUpRequired ? `${error.message} Confirm your identity under Sign-in & security, then retry.` : error.message } })
259
+ document.querySelector('#waiver-form').addEventListener('submit', async event => { event.preventDefault(); const form=event.currentTarget; const data=new FormData(form); const policy=activePolicy(); const status=document.querySelector('#waiver-status'); try { await request('/api/security/waivers',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({findingId:data.get('findingId'),policyId:policy?.id,reason:data.get('reason'),referenceUrl:data.get('referenceUrl')||undefined,expiresAt:new Date(String(data.get('expiresAt'))).toISOString()})}); form.closest('dialog').close(); await load() } catch(error) { status.textContent=error.stepUpRequired ? `${error.message} Confirm your identity under Sign-in & security, then retry.` : error.message } })
260
+ document.querySelector('#comment-form').addEventListener('submit', async event => { event.preventDefault(); const form=event.currentTarget; const data=new FormData(form); const status=document.querySelector('#comment-status'); try { await request('/api/security/comments',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({findingId:data.get('findingId'),body:data.get('body'),referenceUrl:data.get('referenceUrl')||undefined})}); form.closest('dialog').close(); await load() } catch(error) { status.textContent=error.message } })
261
+ load().catch(error => { pageStatus.textContent=error.message; findingList.replaceChildren(element('div','empty-security','Security posture could not be loaded.')) })
262
+ </script>
263
+ </body>
264
+ </html>
@@ -4,33 +4,23 @@ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d =
4
4
  const server = __d?.server ?? { name: 'stacks-production-app' }
5
5
  </script>
6
6
  <script client>
7
+ import { useOpConfirm } from '../../functions/useOpConfirm.ts'
8
+
7
9
  // Reactive action runner: non-mutating actions run immediately; mutating ones
8
10
  // (deploy) stage a typed-confirmation via the shared op-confirm bar.
9
- const opOutput = state('')
10
- const opShown = state(false)
11
- const pending = state(null)
12
- const typed = state('')
13
- const confirmTok = derived(() => { const p = pending(); return p ? p.confirm : '' })
14
- const confirmVerb = derived(() => { const p = pending(); return p ? p.verb : '' })
15
- const canRun = derived(() => { const p = pending(); return !!p && typed() === p.confirm })
16
- async function execAction(action, confirm) {
17
- opShown.set(true); opOutput.set('Running ' + action + '...')
18
- try {
19
- const res = await fetch('/api/actions/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action, confirm }) })
20
- const body = await res.json()
21
- opOutput.set((body.ok ? 'OK ' : 'FAILED ') + (body.command || action) + '\nexit ' + (body.exitCode == null ? '-' : body.exitCode) + '\n\n' + (body.stdout || '') + (body.stderr ? '\n' + body.stderr : '') + (body.error ? '\n' + body.error : ''))
22
- } catch (e) { opOutput.set('FAILED ' + action + '\n\n' + ((e && e.message) || e)) }
23
- }
24
- function askAction(action, confirm, verb) {
25
- if (!confirm) { execAction(action, ''); return }
26
- pending.set({ operation: action, confirm, verb: verb || ('run ' + action) }); typed.set('')
11
+ async function executeAction(action, confirm) {
12
+ const res = await fetch('/api/actions/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action, confirm }) })
13
+ const body = await res.json()
14
+ return (body.ok ? 'OK ' : 'FAILED ') + (body.command || action) + '\nexit ' + (body.exitCode == null ? '-' : body.exitCode) + '\n\n' + (body.stdout || '') + (body.stderr ? '\n' + body.stderr : '') + (body.error ? '\n' + body.error : '')
27
15
  }
28
- function cancelOp() { pending.set(null) }
29
- async function runOp() {
30
- const p = pending()
31
- if (!p || typed() !== p.confirm) return
32
- pending.set(null); typed.set('')
33
- await execAction(p.operation, p.confirm)
16
+ const { opOutput, opShown, pending, typed, busy, confirmTok, confirmVerb, confirmDanger, confirmLabel, canRun, askOp, cancelOp, runOp } = useOpConfirm(p => executeAction(p.operation, p.apiConfirm))
17
+ async function askAction(action, confirm, verb) {
18
+ if (confirm) { askOp(action, confirm, verb || ('run ' + action)); return }
19
+ if (busy()) return
20
+ busy.set(true); opShown.set(true); opOutput.set('Running ' + action + '...')
21
+ try { opOutput.set(await executeAction(action, '')) }
22
+ catch (error) { opOutput.set('FAILED ' + action + '\n\n' + ((error && error.message) || error)) }
23
+ finally { busy.set(false) }
34
24
  }
35
25
 
36
26
  // Free-form server command runner (Forge-style). Runs on the app box over the
@@ -98,7 +88,8 @@ async function runServerCmd() {
98
88
  <h2>Run a command on the server</h2>
99
89
  <div class="panel">
100
90
  <div class="field">
101
- <input :value="serverCmd()" @input="serverCmd.set($event.target.value)" @keydown.enter="askServerCmd()" placeholder="e.g. systemctl status rpx-gateway, or df -h" autocomplete="off" aria-label="Command to run on the server">
91
+ <label class="sr-only" for="server-command">Command to run on the server</label>
92
+ <input id="server-command" :value="serverCmd()" @input="serverCmd.set($event.target.value)" @keydown.enter="askServerCmd()" placeholder="e.g. systemctl status rpx-gateway, or df -h" autocomplete="off">
102
93
  <button class="btn" type="button" :disabled="cmdBusy()" @click="askServerCmd()">Run</button>
103
94
  </div>
104
95
  <div class="op-confirm" @show="cmdPending() !== null" style="margin-top:14px">
@@ -107,7 +98,7 @@ async function runServerCmd() {
107
98
  <button type="button" class="btn danger sm" :disabled="!cmdCanRun()" @click="runServerCmd()">Run command</button>
108
99
  <button type="button" class="btn ghost sm" @click="cancelServerCmd()">Cancel</button>
109
100
  </div>
110
- <pre class="action-output" @show="cmdShown()">{{ cmdOut() }}</pre>
101
+ <pre class="action-output" @show="cmdShown()" aria-live="polite">{{ cmdOut() }}</pre>
111
102
  <p class="note">Runs on the app box over the active driver (SSH/SSM). Use it for one-off recipes, service inspection, or installing software. Prefer declarative config for anything you want reproduced on the next deploy.</p>
112
103
  </div>
113
104
  </div>
@@ -0,0 +1,16 @@
1
+ <script server>import { usePolling } from '../../functions/usePolling.ts'</script><script>
2
+ const pools=state([]),placements=state([]),servers=state([]),decisions=state([]),selected=state(null),error=state(''),notice=state(''),adding=state(false)
3
+ async function api(url,options){const response=await fetch(url,options),body=await response.json();if(!response.ok||body.ok===false)throw new Error(body.error||'Request failed.');return body}
4
+ async function load(){const body=await api('/api/capacity');pools.set(body.pools||[]);placements.set(body.placements||[]);servers.set(body.servers||[]);if(selected())selected.set((body.pools||[]).find(item=>item.id===selected().id)||null)}const{pollingState,pollingError,lastUpdatedAt,refresh}=usePolling(load,15000)
5
+ const object=value=>Object.fromEntries(String(value||'').split(',').map(item=>item.split('=')).filter(item=>item[0]&&item[1])),numbers=value=>Object.fromEntries(Object.entries(object(value)).map(([key,item])=>[key,Number(item)]))
6
+ async function post(url,body){error.set('');try{const value=await api(url,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});await refresh();return value}catch(cause){error.set(cause.message)}}
7
+ async function create(event){event.preventDefault();const value=Object.fromEntries(new FormData(event.currentTarget));await post('/api/capacity',{...value,labels:object(value.labels),requiredServerLabels:object(value.selector),capacity:numbers(value.capacity),maxWorkloads:Number(value.maxWorkloads),concurrency:Number(value.concurrency),costWeight:Number(value.costWeight)});notice.set('Capacity pool created. Add a validated server or use its provider-managed backend.');adding.set(false)}
8
+ async function attach(serverId){if(selected()&&serverId){await post('/api/capacity/member',{poolId:selected().id,serverId});notice.set('Server attached to pool.')}}async function action(name){if(selected()){const value=await post('/api/capacity/action',{poolId:selected().id,action:name});notice.set(name==='drain'?`Drain moved ${value?.moved?.length||0} and blocked ${value?.blocked?.length||0} stateful placement(s).`:`Pool ${name}d.`)}}
9
+ async function explain(event){event.preventDefault();const value=Object.fromEntries(new FormData(event.currentTarget)),body=await post('/api/capacity/explain',{purpose:value.purpose,resources:numbers(value.resources),region:value.region||undefined,architecture:value.architecture||undefined,labels:object(value.labels)});decisions.set(body?.decisions||[])}
10
+ function active(pool){return placements().filter(item=>item.poolId===pool.id&&!['released','failed'].includes(item.status))}function tone(status){return status==='active'?'ok':status==='disabled'?'bad':'warn'}
11
+ </script><!DOCTYPE html><html lang="en" data-theme="dark"><head>@include('../partials/head',{title:'ts-cloud · Capacity & placement'})</head><body>@include('../partials/nav',{active:'capacity',env:'production'})
12
+ <main class="wrap" id="main-content"><div class="crumbs">Infrastructure <span class="sep">/</span> Capacity & placement</div><header class="page"><div><h1>Capacity & placement</h1><p>Separate production, build, worker, monitoring, and backup capacity with explainable scheduling and safe drains.</p></div><div class="actions"><span class="pill" :class="pollingState()==='live'?'ok':'warn'">{{ pollingState() }}</span><small>{{ lastUpdatedAt()?.toLocaleTimeString()||'connecting' }}</small><button class="btn sm" @click="adding.set(!adding())">{{ adding()?'Close':'New pool' }}</button></div></header><p class="notice" role="status" @show="!!notice()">{{ notice() }}</p><p class="bad notice" role="alert" @show="!!error()||!!pollingError()">{{ error()||pollingError() }}</p>
13
+ <form class="panel pool-form" @show="adding()" @submit="create($event)"><label>Name<input name="name" required pattern="[a-z0-9][a-z0-9-]{1,62}"></label><label>Purpose<select name="purpose"><option>application</option><option>build</option><option>worker</option><option>monitoring</option><option>backup</option></select></label><label>Backend<select name="backend"><option>server</option><option>ecs</option><option>asg</option></select></label><label>Region<input name="region" placeholder="us-west-2"></label><label>Architecture<input name="architecture" placeholder="x86_64"></label><label>Capacity<input name="capacity" required value="cpu=4,memoryBytes=8589934592,diskBytes=53687091200"></label><label>Pool labels<input name="labels" placeholder="stage=production"></label><label>Server selector<input name="selector" placeholder="tier=application"></label><label>Maximum workloads<input name="maxWorkloads" type="number" min="1" value="10"></label><label>Concurrency<input name="concurrency" type="number" min="1" value="1"></label><label>Cost weight<input name="costWeight" type="number" min="0" step=".1" value="1"></label><button class="btn" type="submit">Create isolated pool</button></form>
14
+ <section class="layout"><div class="cards"><template :for="pool in pools()"><article class="card panel"><div><span class="pill" :class="tone(pool.status)">{{ pool.status }}</span><span class="pill">{{ pool.purpose }}</span><h2>{{ pool.name }}</h2><p>{{ pool.backend }} · {{ pool.region||'any region' }} · {{ pool.architecture||'any architecture' }}</p></div><dl><div><dt>Capacity</dt><dd>{{ pool.capacity.cpu }} CPU · {{ (pool.capacity.memoryBytes/1073741824).toFixed(1) }} GB</dd></div><div><dt>Workloads</dt><dd>{{ active(pool).length }} / {{ pool.maxWorkloads }}</dd></div><div><dt>Build concurrency</dt><dd>{{ pool.concurrency }}</dd></div></dl><button class="btn ghost sm" @click="selected.set(pool)">Manage</button></article></template></div><aside class="detail panel" @show="!!selected()"><div class="actions"><h2>{{ selected()?.name }}</h2><button class="btn ghost sm" @click="selected.set(null)">Close</button></div><p>Production secrets are denied to builders. Workspaces are ephemeral and artifacts must be content-addressed.</p><label>Add validated server<select @change="attach($event.currentTarget.value)"><option value="">Choose server</option><template :for="server in servers().filter(item=>item.status==='ready')"><option value="{{ server.id }}">{{ server.name }} · {{ server.roles.join(', ') }}</option></template></select></label><div class="actions"><button class="btn sm warn" @click="action('drain')">Drain safely</button><button class="btn ghost sm" @click="action('enable')">Enable</button><button class="btn danger sm" @click="action('disable')">Disable</button></div><h3>Active placements</h3><template :for="item in active(selected())"><p class="placement"><b>{{ item.purpose }}</b><span>{{ item.status }} · {{ item.stateful?'stateful':'stateless' }}</span></p></template></aside></section>
15
+ <form class="explain panel" @submit="explain($event)"><div><h2>Explain a placement</h2><p>See every eligible and rejected target before scheduling.</p></div><label>Purpose<select name="purpose"><option>application</option><option>build</option><option>worker</option><option>monitoring</option><option>backup</option></select></label><label>Resources<input name="resources" value="cpu=1,memoryBytes=536870912"></label><label>Region<input name="region"></label><label>Architecture<input name="architecture"></label><label>Labels<input name="labels"></label><button class="btn" type="submit">Explain placement</button></form><section class="decisions"><template :for="item in decisions()"><article class="panel"><span class="pill" :class="item.eligible?'ok':'bad'">{{ item.eligible?'eligible':'rejected' }}</span><h3>{{ item.poolName }}</h3><p>{{ item.serverId||'provider-managed' }}</p><p>{{ item.reasons.join('; ')||'All requirements fit.' }}</p><small>fit {{ item.score.fit.toFixed(2) }} · spread {{ item.score.spread }} · cost {{ item.score.cost }}</small></article></template></section>
16
+ </main><style>.actions{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.pool-form{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin-bottom:1rem}.pool-form label,.detail label,.explain label{display:grid;gap:.3rem}.layout{display:grid;grid-template-columns:1fr 400px;gap:1rem}.cards{display:grid;gap:1rem}.card{display:grid;grid-template-columns:1fr 1fr auto;gap:1rem;align-items:center}.card dl{display:flex;gap:1rem}.card dl div{display:grid}.card dt{font-size:.75rem;color:var(--muted)}.detail{position:sticky;top:1rem;align-self:start}.detail .actions h2{margin-right:auto}.placement{display:flex;justify-content:space-between;border-bottom:1px solid var(--border);padding:.5rem 0}.explain{display:grid;grid-template-columns:1.5fr repeat(5,1fr) auto;gap:.75rem;align-items:end;margin-top:1rem}.decisions{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin-top:1rem}@media(max-width:1000px){.layout{grid-template-columns:1fr}.detail{position:static}.pool-form,.explain,.decisions{grid-template-columns:1fr 1fr}.card{grid-template-columns:1fr}}@media(max-width:600px){.pool-form,.explain,.decisions{grid-template-columns:1fr}}</style></body></html>
@@ -4,6 +4,8 @@ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d =
4
4
  const server = __d?.server ?? { name: 'acme-production-app' }
5
5
  </script>
6
6
  <script client>
7
+ import { requestJson } from '../../functions/requestJson.ts'
8
+
7
9
  // Reactive database management - create databases/users, live lists.
8
10
  const engine = state('mysql')
9
11
  const databases = state([])
@@ -11,6 +13,7 @@ const users = state([])
11
13
  const dbError = state('')
12
14
  const output = state('')
13
15
  const outputShown = state(false)
16
+ const busy = state(false)
14
17
  const dbName = state('')
15
18
  const uName = state('')
16
19
  const uPass = state('')
@@ -27,20 +30,28 @@ async function load() {
27
30
  } catch (e) { show('Could not read databases: ' + ((e && e.message) || e)) }
28
31
  }
29
32
  async function createDb() {
33
+ if (busy()) return
34
+ busy.set(true)
30
35
  show('Creating database ' + dbName() + '...')
31
- const res = await fetch('/api/databases', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: dbName() }) })
32
- const b = await res.json()
33
- show((b.ok ? 'OK created ' + (b.name || dbName()) : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
34
- if (b.ok) { dbName.set(''); load() }
36
+ try {
37
+ const b = await requestJson('/api/databases', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: dbName() }) })
38
+ show((b.ok ? 'OK created ' + (b.name || dbName()) : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
39
+ if (b.ok) { dbName.set(''); load() }
40
+ } catch (e) { show('FAILED\n\n' + ((e && e.message) || e)) }
41
+ finally { busy.set(false) }
35
42
  }
36
43
  async function createUser() {
44
+ if (busy()) return
45
+ busy.set(true)
37
46
  show('Creating user ' + uName() + '...')
38
47
  const payload = { username: uName(), password: uPass(), access: uAccess() }
39
48
  if (uDatabase()) payload.database = uDatabase()
40
- const res = await fetch('/api/databases/users', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
41
- const b = await res.json()
42
- show((b.ok ? 'OK created user ' + (b.username || uName()) : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
43
- if (b.ok) { uName.set(''); uPass.set(''); load() }
49
+ try {
50
+ const b = await requestJson('/api/databases/users', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
51
+ show((b.ok ? 'OK created user ' + (b.username || uName()) : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
52
+ if (b.ok) { uName.set(''); uPass.set(''); load() }
53
+ } catch (e) { show('FAILED\n\n' + ((e && e.message) || e)) }
54
+ finally { busy.set(false) }
44
55
  }
45
56
 
46
57
  // Per-database backups: a gzipped dump written on the box, plus a live list.
@@ -49,11 +60,15 @@ async function loadBackups() {
49
60
  try { const res = await fetch('/api/databases/backups'); const d = await res.json(); if (d.ok) backups.set(d.backups || []) } catch {}
50
61
  }
51
62
  async function backupDb(name) {
63
+ if (busy()) return
64
+ busy.set(true)
52
65
  show('Backing up ' + name + '...')
53
- const res = await fetch('/api/databases/backup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ database: name, confirm: name }) })
54
- const b = await res.json()
55
- show((b.ok ? 'OK backed up ' + (b.database || name) : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
56
- if (b.ok) loadBackups()
66
+ try {
67
+ const b = await requestJson('/api/databases/backup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ database: name, confirm: name }) })
68
+ show((b.ok ? 'OK backed up ' + (b.database || name) : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
69
+ if (b.ok) loadBackups()
70
+ } catch (e) { show('FAILED\n\n' + ((e && e.message) || e)) }
71
+ finally { busy.set(false) }
57
72
  }
58
73
  onMount(() => { load(); loadBackups() })
59
74
  </script>
@@ -74,7 +89,7 @@ onMount(() => { load(); loadBackups() })
74
89
  <div class="panel">
75
90
  <form class="form-grid" @submit.prevent="createDb()">
76
91
  <div class="field-block wide"><label for="db-name">Name</label><input id="db-name" placeholder="acme_production" autocomplete="off" :value="dbName()" @input="dbName.set($event.target.value)"></div>
77
- <div class="form-actions"><button class="btn" type="submit">Create database</button></div>
92
+ <div class="form-actions"><button class="btn" type="submit" :disabled="busy()">Create database</button></div>
78
93
  </form>
79
94
  </div>
80
95
  </div>
@@ -96,20 +111,20 @@ onMount(() => { load(); loadBackups() })
96
111
  <option value="readonly">read only</option>
97
112
  </select>
98
113
  </div>
99
- <div class="form-actions"><button class="btn" type="submit">Create user</button></div>
114
+ <div class="form-actions"><button class="btn" type="submit" :disabled="busy()">Create user</button></div>
100
115
  </form>
101
116
  </div>
102
117
  </div>
103
118
  </div>
104
119
 
105
- <pre class="action-output" @show="outputShown()">{{ output() }}</pre>
120
+ <pre class="action-output" @show="outputShown()" aria-live="polite">{{ output() }}</pre>
106
121
 
107
122
  <div class="cols section">
108
123
  <div>
109
124
  <h2>Databases</h2>
110
125
  <div class="panel">
111
126
  <template :for="n in databases()">
112
- <div class="kv"><span class="mono">{{ n }}</span> <button class="btn ghost sm" type="button" @click="backupDb(n)">Back up</button></div>
127
+ <div class="kv"><span class="mono">{{ n }}</span> <button class="btn ghost sm" type="button" :disabled="busy()" @click="backupDb(n)" :aria-label="'Back up ' + n">Back up</button></div>
113
128
  </template>
114
129
  <span class="item" @show="databases().length === 0 && dbError() !== ''">{{ dbError() }}</span>
115
130
  <span class="item" @show="databases().length === 0 && dbError() === ''">no databases</span>