@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,238 @@
1
+ <script client>
2
+ import { usePolling } from '../../functions/usePolling.ts'
3
+
4
+ const jobs = state([])
5
+ const workers = state([])
6
+ const history = state([])
7
+ const operations = state([])
8
+ const preview = state(null)
9
+ const selectedJob = state(null)
10
+ const mode = state('server')
11
+ const tab = state('schedules')
12
+ const notice = state('')
13
+ const actionError = state('')
14
+ const search = state('')
15
+ const providerFilter = state('all')
16
+ const statusFilter = state('all')
17
+ const busy = state(false)
18
+ let previewTimer
19
+
20
+ async function api(url, options) {
21
+ const response = await fetch(url, options)
22
+ const body = await response.json()
23
+ if (!response.ok || body.ok === false)
24
+ throw new Error(body.error || 'Request failed.')
25
+ return body
26
+ }
27
+
28
+ async function load() {
29
+ const [jobResult, workerResult, dashboard] = await Promise.all([
30
+ api('/api/jobs'),
31
+ api('/api/workers'),
32
+ fetch('/api/dashboard-data').then(response => response.json()).catch(() => ({})),
33
+ ])
34
+ jobs.set(jobResult.jobs || [])
35
+ workers.set(workerResult.workers || [])
36
+ mode.set(dashboard.mode || (jobResult.jobs?.some(item => item.provider === 'eventbridge') ? 'serverless' : 'server'))
37
+ if ((dashboard.mode || mode()) === 'server') {
38
+ const all = await fetch('/api/server/operations').then(response => response.json()).catch(() => [])
39
+ const values = Array.isArray(all) ? all : []
40
+ operations.set(values.filter(item => !item.danger && ['scheduler', 'backup', 'worker'].includes(item.group)))
41
+ }
42
+ else operations.set([])
43
+ if (selectedJob()) {
44
+ const current = jobResult.jobs?.find(item => item.id === selectedJob().id)
45
+ if (current) selectedJob.set(current)
46
+ }
47
+ }
48
+
49
+ const { pollingState, pollingError, lastUpdatedAt, refresh } = usePolling(load, 15000)
50
+
51
+ function formBody(form) { return Object.fromEntries(new FormData(form)) }
52
+ function tone(value) { return ['succeeded', 'enabled', 'in_sync', 'running'].includes(value) ? 'ok' : ['failed', 'dead', 'drifted', 'unavailable'].includes(value) ? 'bad' : 'warn' }
53
+ function when(value) { return value ? new Date(value).toLocaleString() : '—' }
54
+ function duration(item) { if (!item?.startedAt || !item?.finishedAt) return '—'; const ms = new Date(item.finishedAt) - new Date(item.startedAt); return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s` }
55
+ function filteredJobs() {
56
+ const query = search().trim().toLowerCase()
57
+ return jobs().filter(item => (!query || `${item.name} ${item.expression} ${item.timezone}`.toLowerCase().includes(query)) && (providerFilter() === 'all' || item.provider === providerFilter()) && (statusFilter() === 'all' || (statusFilter() === 'enabled' ? item.enabled : !item.enabled)))
58
+ }
59
+ function setPreset(event) {
60
+ const value = event.currentTarget.value
61
+ if (!value) return
62
+ const input = event.currentTarget.form?.querySelector('[name="expression"]')
63
+ if (input) { input.value = value; queuePreview(event.currentTarget.form) }
64
+ }
65
+ function queuePreview(form) { clearTimeout(previewTimer); previewTimer = setTimeout(() => void previewForm(form), 300) }
66
+ async function previewForm(form) {
67
+ if (!form) return
68
+ const value = formBody(form)
69
+ try {
70
+ const result = await api('/api/jobs/preview', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ expression: value.expression, timezone: value.timezone, count: 5 }) })
71
+ preview.set(result.preview)
72
+ actionError.set('')
73
+ }
74
+ catch (error) { preview.set(null); actionError.set(error.message) }
75
+ }
76
+ async function createJob(event) {
77
+ event.preventDefault()
78
+ busy.set(true)
79
+ try {
80
+ const value = formBody(event.currentTarget)
81
+ const payloadRefs = value.payloadRef ? { input: value.payloadRef } : {}
82
+ await api('/api/jobs', {
83
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({
84
+ name: value.name,
85
+ provider: mode() === 'serverless' ? 'eventbridge' : 'server',
86
+ operationId: value.operationId || undefined,
87
+ expression: value.expression,
88
+ timezone: value.timezone,
89
+ startsAt: value.startsAt || undefined,
90
+ endsAt: value.endsAt || undefined,
91
+ flexibleMinutes: Number(value.flexibleMinutes) || 0,
92
+ missedRunPolicy: value.missedRunPolicy,
93
+ overlapPolicy: value.overlapPolicy,
94
+ maxAttempts: Number(value.maxAttempts),
95
+ backoffSeconds: Number(value.backoffSeconds),
96
+ timeoutSeconds: Number(value.timeoutSeconds),
97
+ deadLetterRef: value.deadLetterRef || undefined,
98
+ payloadRefs,
99
+ }),
100
+ })
101
+ event.currentTarget.reset()
102
+ preview.set(null)
103
+ notice.set('Schedule created. Provider reconciliation is pending.')
104
+ actionError.set('')
105
+ await refresh()
106
+ }
107
+ catch (error) { actionError.set(error.message) }
108
+ finally { busy.set(false) }
109
+ }
110
+ async function jobAction(item, action) {
111
+ let confirm = ''
112
+ if (['disable', 'delete'].includes(action)) {
113
+ confirm = window.prompt(`Type ${action} to confirm this change.`) || ''
114
+ if (confirm !== action) return
115
+ }
116
+ busy.set(true)
117
+ try {
118
+ const result = await api('/api/jobs/action', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: item.id, action, confirm }) })
119
+ notice.set(action === 'run' ? `Queued ${item.name} as execution ${result.execution.id}.` : `${item.name} ${action}d.`)
120
+ actionError.set('')
121
+ await refresh()
122
+ if (action === 'run') await showHistory(item)
123
+ }
124
+ catch (error) { actionError.set(error.message) }
125
+ finally { busy.set(false) }
126
+ }
127
+ async function editJob(item) {
128
+ if (item.origin === 'config') { actionError.set('Config-defined schedules must be edited in cloud.config.ts, then reconciled.'); return }
129
+ const expression = window.prompt('Schedule expression', item.expression)
130
+ if (!expression) return
131
+ const timezone = window.prompt('IANA timezone', item.timezone)
132
+ if (!timezone) return
133
+ try {
134
+ await api('/api/jobs', { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: item.id, expression, timezone }) })
135
+ notice.set(`${item.name} updated.`)
136
+ actionError.set('')
137
+ await refresh()
138
+ }
139
+ catch (error) { actionError.set(error.message) }
140
+ }
141
+ async function showHistory(item) {
142
+ selectedJob.set(item)
143
+ tab.set('history')
144
+ try { history.set((await api(`/api/jobs/history?jobId=${encodeURIComponent(item.id)}&limit=100`)).executions || []) }
145
+ catch (error) { actionError.set(error.message) }
146
+ }
147
+ async function reconcile() {
148
+ try {
149
+ const result = await api('/api/jobs/reconcile', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
150
+ notice.set(`Reconciled ${result.reconciliation.jobs} schedules and ${result.reconciliation.workers} workers; ${result.reconciliation.drifted} retained as drifted.`)
151
+ await refresh()
152
+ }
153
+ catch (error) { actionError.set(error.message) }
154
+ }
155
+ async function restartWorker(item) {
156
+ const confirm = window.prompt(`Type ${item.name} to restart this worker.`) || ''
157
+ if (confirm !== item.name) return
158
+ try {
159
+ await api('/api/workers/action', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: item.id, action: 'restart', confirm }) })
160
+ notice.set(`${item.name} restarted.`)
161
+ await refresh()
162
+ }
163
+ catch (error) { actionError.set(error.message) }
164
+ }
165
+ </script>
166
+ <!DOCTYPE html>
167
+ <html lang="en" data-theme="dark">
168
+ <head>@include('../partials/head', { title: 'ts-cloud · Workers & schedules' })</head>
169
+ <body>
170
+ @include('../partials/nav', { active: 'jobs', env: 'production' })
171
+ <main class="jobs-wrap wrap" id="main-content">
172
+ <div class="crumbs">Operations <span class="sep">/</span> Workers &amp; schedules</div>
173
+ <header class="page">
174
+ <div><h1>Workers &amp; schedules</h1><p>One control plane for cron, EventBridge schedules, recurring operations, queue workers, retries, and execution evidence.</p></div>
175
+ <div class="header-actions"><span class="pill" :class="pollingState()==='live'?'ok':'warn'">{{ pollingState() }}</span><small>{{ lastUpdatedAt()?.toLocaleTimeString() || 'connecting' }}</small><button class="btn ghost sm" @click="refresh()">Refresh</button><button class="btn ghost sm" @click="reconcile()">Reconcile config</button></div>
176
+ </header>
177
+ <p class="notice" role="status" @show="!!notice()">{{ notice() }}</p>
178
+ <p class="bad notice" role="alert" @show="!!actionError() || !!pollingError()">{{ actionError() || pollingError() }}</p>
179
+
180
+ <div class="summary">
181
+ <article class="panel"><b>Schedules</b><strong>{{ jobs().length }}</strong><small>{{ jobs().filter(item=>item.enabled).length }} enabled</small></article>
182
+ <article class="panel"><b>Workers</b><strong>{{ workers().length }}</strong><small>{{ workers().reduce((sum,item)=>sum+item.processes,0) }} processes</small></article>
183
+ <article class="panel"><b>Needs review</b><strong>{{ jobs().filter(item=>['drifted','unsupported','unavailable'].includes(item.reconciliationStatus)).length + workers().filter(item=>['drifted','unsupported','unavailable'].includes(item.reconciliationStatus)).length }}</strong><small>drift or provider gaps</small></article>
184
+ <article class="panel"><b>Failed last run</b><strong>{{ jobs().filter(item=>['failed','dead'].includes(item.lastExecution?.status)).length }}</strong><small>retry from history</small></article>
185
+ </div>
186
+
187
+ <nav class="tabs" aria-label="Jobs workspace"><button :class="{active:tab()==='schedules'}" @click="tab.set('schedules')">Schedules</button><button :class="{active:tab()==='workers'}" @click="tab.set('workers')">Workers</button><button :class="{active:tab()==='history'}" @click="tab.set('history')">Execution history</button></nav>
188
+
189
+ <section @show="tab()==='schedules'" class="schedule-layout">
190
+ <div>
191
+ <div class="filters panel"><input aria-label="Search schedules" type="search" placeholder="Search name, expression, timezone" @input="search.set($event.currentTarget.value)"><select aria-label="Filter provider" @change="providerFilter.set($event.currentTarget.value)"><option value="all">All providers</option><option value="server">Server cron</option><option value="eventbridge">EventBridge</option><option value="platform">Platform</option></select><select aria-label="Filter state" @change="statusFilter.set($event.currentTarget.value)"><option value="all">All states</option><option value="enabled">Enabled</option><option value="disabled">Disabled</option></select></div>
192
+ <div class="cards-list">
193
+ <template :for="item in filteredJobs()"><article class="job-card panel">
194
+ <div class="job-title"><div><span class="pill" :class="item.enabled?'ok':'warn'">{{ item.enabled?'enabled':'disabled' }}</span><span class="pill">{{ item.provider }}</span><span class="pill" :class="tone(item.reconciliationStatus)">{{ item.reconciliationStatus }}</span><h2>{{ item.name }}</h2></div><div class="actions"><button class="btn ghost sm" :disabled="busy()" @click="jobAction(item,'run')">Run now</button><button class="btn ghost sm" @click="showHistory(item)">History</button><button class="btn ghost sm" @click="editJob(item)">Edit</button><button class="btn ghost sm" @click="jobAction(item,item.enabled?'disable':'enable')">{{ item.enabled?'Disable':'Enable' }}</button><button class="btn danger sm" @show="item.origin!=='config'" @click="jobAction(item,'delete')">Delete</button></div></div>
195
+ <div class="job-grid"><div><small>Schedule</small><code>{{ item.expression }}</code><span>{{ item.timezone }}</span></div><div><small>Next run</small><b>{{ when(item.nextRunAt) }}</b><span>Last {{ when(item.lastExecution?.finishedAt || item.lastScheduledFor) }}</span></div><div><small>Target</small><b>{{ item.target.operationId || item.target.action || item.target.functionName || item.target.kind }}</b><span>{{ item.origin }} · owner {{ item.ownerActorId || 'configuration' }}</span></div><div><small>Policies</small><b>{{ item.overlapPolicy }} overlap · {{ item.missedRunPolicy }}</b><span>{{ item.retryPolicy.maxAttempts }} attempts · {{ item.timeoutSeconds }}s timeout</span></div></div>
196
+ <div class="run-preview"><small>Next five</small><span>{{ item.preview?.nextRuns?.map(when).join(' · ') || 'No run in active window' }}</span></div>
197
+ <p class="bad note" @show="!!item.lastExecution?.error">{{ item.lastExecution.error }}</p>
198
+ </article></template>
199
+ <div class="empty panel" @show="!filteredJobs().length"><strong>No schedules match</strong><span>Create a recurring operation or adjust the filters. Empty means no provider execution will occur.</span></div>
200
+ </div>
201
+ </div>
202
+
203
+ <form class="editor panel" @submit="createJob($event)" @input="queuePreview($event.currentTarget)">
204
+ <h2>New schedule</h2><p class="note">Provider: <b>{{ mode()==='serverless'?'EventBridge Scheduler':'server cron' }}</b>. Targets are constrained to safe, configured operations.</p>
205
+ <label>Name<input name="name" required maxlength="120" placeholder="Nightly backup verification"></label>
206
+ <label>Preset<select @change="setPreset($event)"><option value="">Custom</option><option value="every_minute">Every minute</option><option value="hourly">Hourly</option><option value="daily">Daily</option><option value="weekly">Weekly</option><option value="monthly">Monthly</option><option value="rate(5 minutes)">Every 5 minutes</option></select></label>
207
+ <label class="wide">Expression<input name="expression" class="mono" required value="0 * * * *" placeholder="cron or rate(5 minutes)"></label>
208
+ <label>Timezone<input name="timezone" required value="UTC" placeholder="America/Los_Angeles"></label>
209
+ <label @show="mode()==='server'"><span>Allowlisted target</span><select name="operationId" :required="mode()==='server'"><option value="">Choose operation</option><template :for="operation in operations()"><option :value="operation.id">{{ operation.label }}</option></template></select></label>
210
+ <label>Starts at<input name="startsAt" type="datetime-local"></label><label>Ends at<input name="endsAt" type="datetime-local"></label>
211
+ <label>Flexible window (minutes)<input name="flexibleMinutes" type="number" min="0" max="1440" value="0"></label>
212
+ <label>Missed runs<select name="missedRunPolicy"><option value="skip">Skip</option><option value="catch_up">Catch up (max 10)</option></select></label>
213
+ <label>Overlap<select name="overlapPolicy"><option value="forbid">Forbid</option><option value="allow">Allow</option><option value="replace">Replace active run</option></select></label>
214
+ <label>Max attempts<input name="maxAttempts" type="number" min="1" max="20" value="3"></label>
215
+ <label>Backoff seconds<input name="backoffSeconds" type="number" min="0" value="30"></label><label>Timeout seconds<input name="timeoutSeconds" type="number" min="1" max="86400" value="900"></label>
216
+ <label class="wide">Dead-letter reference<input name="deadLetterRef" placeholder="arn://aws/sqs/... or secret://jobs/dlq"></label>
217
+ <label class="wide">Payload reference (optional)<input name="payloadRef" placeholder="secret://jobs/nightly-input"><span>Inline payload values are rejected. Use secret://, ssm://, arn://, or env:// references.</span></label>
218
+ <div class="preview wide" @show="!!preview()"><b>{{ preview()?.description }}</b><code>{{ preview()?.normalized }}</code><ol><template :for="run in preview()?.nextRuns || []"><li>{{ when(run) }}</li></template></ol><p class="note">Preview only—no production execution or incident is created.</p></div>
219
+ <button class="btn wide" type="submit" :disabled="busy() || (mode()==='server' && !operations().length)">{{ busy()?'Creating…':'Create schedule' }}</button>
220
+ <p class="bad note wide" @show="mode()==='server' && !operations().length">No safe recurring target is configured. Enable a scheduler, worker, or backup operation first.</p>
221
+ </form>
222
+ </section>
223
+
224
+ <section @show="tab()==='workers'" class="cards-list">
225
+ <template :for="item in workers()"><article class="panel worker-card"><div><span class="pill">{{ item.provider }}</span><span class="pill" :class="tone(item.observedState?.status || item.reconciliationStatus)">{{ item.observedState?.status || item.reconciliationStatus }}</span><h2>{{ item.name }}</h2></div><div class="job-grid"><div><small>Queue</small><b>{{ item.queue }}</b><span>{{ item.processes }} processes</span></div><div><small>Current job</small><b>{{ item.observedState?.currentJob || 'idle / unavailable' }}</b><span>{{ item.observedState?.processed || 0 }} processed</span></div><div><small>Failures</small><b>{{ item.observedState?.failures || 0 }}</b><span>{{ item.timeoutSeconds }}s timeout</span></div><div><small>Lifecycle</small><b>{{ item.restartPolicy }}</b><span>{{ item.origin }} · {{ item.reconciliationStatus }}</span></div></div><div class="actions"><button class="btn ghost sm" @show="item.provider==='systemd'" @click="restartWorker(item)">Graceful restart</button><span class="note" @show="item.provider!=='systemd'">Scaling and restarts remain provider/config managed for this worker.</span></div></article></template>
226
+ <div class="empty panel" @show="!workers().length"><strong>No workers configured</strong><span>Add site queues or serverless queue definitions, then reconcile configuration.</span></div>
227
+ </section>
228
+
229
+ <section @show="tab()==='history'">
230
+ <div class="empty panel" @show="!selectedJob()"><strong>Select a schedule</strong><span>Open History from a schedule to inspect attempts, retry state, duration, output, and redacted logs.</span></div>
231
+ <div @show="!!selectedJob()"><header class="history-head"><div><h2>{{ selectedJob()?.name }}</h2><p class="note">At-least-once execution evidence; retries reuse the durable operation and preserve every log line.</p></div><button class="btn sm" @click="jobAction(selectedJob(),'run')">Run again</button></header><div class="cards-list"><template :for="item in history()"><article class="execution panel"><div class="execution-head"><span class="pill" :class="tone(item.status)">{{ item.status }}</span><b>{{ when(item.scheduledFor) }}</b><span>{{ item.trigger }} · attempt {{ item.attempt }} · {{ duration(item) }}</span></div><p class="bad note" @show="!!item.error">{{ item.error }}</p><pre @show="item.logs?.length">{{ item.logs?.map(log=>'['+log.stream+'] '+log.message).join('\n') }}</pre><pre @show="Object.keys(item.output||{}).length">{{ JSON.stringify(item.output,null,2) }}</pre></article></template><div class="empty panel" @show="!history().length"><strong>No executions</strong><span>Run the schedule manually or wait for its next due time.</span></div></div></div>
232
+ </section>
233
+ </main>
234
+ <style>
235
+ .jobs-wrap{max-width:1500px}.summary{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}.summary article{display:grid;gap:5px}.summary strong{font-size:28px}.summary small{color:var(--txt3)}.tabs{display:flex;gap:5px;overflow:auto;border-bottom:1px solid var(--line);margin-bottom:18px}.tabs button{border:0;background:transparent;color:var(--txt3);padding:10px 14px;white-space:nowrap}.tabs button.active{color:var(--txt);border-bottom:2px solid var(--accent)}.schedule-layout{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(340px,.8fr);gap:14px;align-items:start}.filters{display:grid;grid-template-columns:1fr 170px 150px;gap:8px;margin-bottom:10px}.filters input,.filters select,.editor input,.editor select{width:100%;background:var(--bg2);border:1px solid var(--line);border-radius:8px;color:var(--txt);padding:9px}.cards-list{display:grid;gap:10px}.job-title,.execution-head,.history-head{display:flex;justify-content:space-between;gap:10px;align-items:center;flex-wrap:wrap}.job-title>div:first-child,.worker-card>div:first-child{display:flex;gap:7px;align-items:center;flex-wrap:wrap}.job-title h2,.worker-card h2{font-size:17px;margin:0 0 0 4px}.actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap}.job-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:14px}.job-grid>div{display:grid;gap:4px;min-width:0}.job-grid small,.run-preview small{color:var(--txt3);text-transform:uppercase;font-size:9px;letter-spacing:.08em}.job-grid span,.run-preview span{color:var(--txt3);font-size:11px;overflow-wrap:anywhere}.job-grid code{overflow-wrap:anywhere}.run-preview{display:grid;gap:4px;border-top:1px solid var(--line);margin-top:12px;padding-top:10px}.editor{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;position:sticky;top:10px}.editor h2,.editor>p{grid-column:1/-1;margin:0}.editor label{display:grid;gap:5px;color:var(--txt3);font-size:11px}.editor label span{font-size:10px}.wide{grid-column:1/-1}.preview{background:var(--bg2);border:1px solid var(--line);border-radius:8px;padding:10px;display:grid;gap:6px}.preview ol{margin:0;padding-left:20px;color:var(--txt2);font-size:11px}.worker-card .actions{margin-top:12px}.execution pre{max-height:260px;overflow:auto;background:var(--bg2);padding:10px;border-radius:8px;font-size:11px}.history-head{margin-bottom:10px}.mono,code{font-family:var(--mono)}@media(max-width:1050px){.schedule-layout{grid-template-columns:1fr}.editor{position:static}.job-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:720px){.summary{grid-template-columns:repeat(2,1fr)}.filters{grid-template-columns:1fr}.job-grid{grid-template-columns:1fr}}@media(max-width:480px){.summary{grid-template-columns:1fr}.editor{grid-template-columns:1fr}.editor>*{grid-column:1!important}}
236
+ </style>
237
+ </body>
238
+ </html>
@@ -0,0 +1,15 @@
1
+ <script server>import { usePolling } from '../../functions/usePolling.ts'</script><script>
2
+ const data=state({manifests:[],windows:[],campaigns:[],cleanups:[],drills:[]}),tab=state('upgrades'),error=state(''),notice=state(''),busy=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}async function load(){data.set(await api('/api/maintenance'))}const{pollingState,pollingError,lastUpdatedAt,refresh}=usePolling(load,12000)
4
+ async function post(url,body){busy.set(true);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)}finally{busy.set(false)}}
5
+ async function preview(event){event.preventDefault();const value=Object.fromEntries(new FormData(event.currentTarget)),result=await post('/api/maintenance/cleanup-preview',{kind:value.kind,olderThanDays:Number(value.olderThanDays)});if(result)notice.set(`Preview found ${result.plan.candidates.length} immutable candidate(s), reclaiming up to ${result.plan.estimatedBytes} bytes.`)}
6
+ async function cleanup(plan){const expected=`DELETE ${plan.candidates.length} ${plan.kind}`,confirmation=prompt(`Type ${expected}. The candidate digest is fixed; changed resources will be rejected.`);if(confirmation===expected){const result=await post('/api/maintenance/cleanup-apply',{planId:plan.id,confirmation});if(result)notice.set(`Cleanup queued as ${result.operation.id}.`)}}
7
+ async function drill(event){event.preventDefault();const value=Object.fromEntries(new FormData(event.currentTarget)),result=await post('/api/maintenance/drill',{...value,expectedRpoMinutes:Number(value.expectedRpoMinutes),expectedRtoMinutes:Number(value.expectedRtoMinutes)});if(result)notice.set(`Isolated drill ${result.drill.id} planned. Review it, then run.`)}async function runDrill(id){const result=await post('/api/maintenance/drill-run',{drillId:id});if(result)notice.set(`DR drill queued as ${result.operation.id}.`)}
8
+ function tone(status){return ['verified','compatible','succeeded','passed','healthy'].includes(status)?'ok':['failed','partial','cleanup_required','rolled_back','blocked'].includes(status)?'bad':'warn'}function bytes(value){return value>1073741824?`${(value/1073741824).toFixed(1)} GB`:value>1048576?`${(value/1048576).toFixed(1)} MB`:`${value} B`}
9
+ </script><!DOCTYPE html><html lang="en" data-theme="dark"><head>@include('../partials/head',{title:'ts-cloud · Maintenance & disaster recovery'})</head><body>@include('../partials/nav',{active:'maintenance',env:'production'})
10
+ <main class="wrap" id="main-content"><div class="crumbs">Operations <span class="sep">/</span> Maintenance & DR</div><header class="page"><div><h1>Maintenance & disaster recovery</h1><p>Verified updates, approved windows, reversible rollout, dry-run cleanup, and isolated restore drills.</p></div><div class="actions"><span class="pill" :class="pollingState()==='live'?'ok':'warn'">{{ pollingState() }}</span><small>{{ lastUpdatedAt()?.toLocaleTimeString()||'connecting' }}</small></div></header><p class="notice" role="status" @show="!!notice()">{{ notice() }}</p><p class="bad notice" role="alert" @show="!!error()||!!pollingError()">{{ error()||pollingError() }}</p>
11
+ <nav class="tabs" aria-label="Maintenance areas"><button :class="tab()==='upgrades'?'active':''" @click="tab.set('upgrades')">Platform updates</button><button :class="tab()==='cleanup'?'active':''" @click="tab.set('cleanup')">Safe cleanup</button><button :class="tab()==='dr'?'active':''" @click="tab.set('dr')">Recovery drills</button></nav>
12
+ <section @show="tab()==='upgrades'"><div class="panel safety"><b>Update trust chain</b><span>Ed25519 signature → schema and platform compatibility → verified backup → approved window → canary → health gate → rolling cohorts.</span></div><div class="grid"><section class="panel"><h2>Verified manifests</h2><template :for="item in data().manifests"><article class="item"><span class="pill" :class="item.compatibility.compatible?'ok':'bad'">{{ item.compatibility.compatible?'compatible':'blocked' }}</span><b>{{ item.version }}</b><small>{{ item.channel }} · key {{ item.keyId }}</small><p>{{ item.compatibility.reasons.join('; ')||'Signature and compatibility verified.' }}</p></article></template><p class="empty" @show="!data().manifests.length">Import signed manifests with the CLI or library; raw unsigned URLs are never offered here.</p></section><section class="panel"><h2>Staged campaigns</h2><template :for="item in data().campaigns"><article class="item"><span class="pill" :class="tone(item.campaign.status)">{{ item.campaign.status }}</span><b>{{ item.campaign.strategy }} from {{ item.campaign.fromVersion }}</b><small>{{ item.campaign.currentStage||'not started' }} · backup {{ item.campaign.backupId }}</small><div class="target-row"><template :for="target in item.targets"><span>{{ target.target }} · {{ target.status }}</span></template></div></article></template><p class="empty" @show="!data().campaigns.length">No upgrade campaigns.</p></section></div></section>
13
+ <section @show="tab()==='cleanup'"><div class="panel safety"><b>Nothing is deleted during discovery.</b><span>Each preview expires, has a digest over the exact candidates, and requires a count-and-kind confirmation.</span></div><form class="form panel" @submit="preview($event)"><label>Resource type<select name="kind"><option>releases</option><option>artifacts</option><option>operations</option><option>logs</option><option>previews</option><option>snapshots</option><option>orphaned_resources</option></select></label><label>Older than days<input name="olderThanDays" type="number" min="1" value="30"></label><button class="btn" type="submit" :disabled="busy()">Generate dry-run preview</button></form><div class="cards"><template :for="plan in data().cleanups"><article class="panel"><span class="pill" :class="tone(plan.status)">{{ plan.status }}</span><h2>{{ plan.kind }}</h2><p>{{ plan.candidates.length }} candidates · {{ bytes(plan.estimatedBytes) }}</p><small>expires {{ plan.expiresAt }} · digest {{ plan.candidateDigest.slice(0,12) }}</small><details><summary>Inspect candidates</summary><template :for="candidate in plan.candidates"><p class="row"><span>{{ candidate.label }}</span><b>{{ bytes(candidate.bytes) }}</b></p></template></details><button class="btn danger sm" @show="plan.status==='preview'" @click="cleanup(plan)">Confirm exact candidate set</button></article></template></div></section>
14
+ <section @show="tab()==='dr'"><div class="panel safety"><b>Drills never target production.</b><span>Only <code>isolated://</code> targets are accepted. RPO/RTO and health evidence are recorded, and cleanup runs even after failure.</span></div><form class="drill form panel" @submit="drill($event)"><label>Scenario<select name="scenario"><option>control_plane</option><option>database</option><option>volume</option><option>regional</option></select></label><label>Isolated target<input name="isolatedTarget" required value="isolated://drills/latest"></label><label>Recovery point<input name="recoveryPointId" placeholder="optional"></label><label>RPO minutes<input name="expectedRpoMinutes" type="number" min="1" value="60"></label><label>RTO minutes<input name="expectedRtoMinutes" type="number" min="1" value="30"></label><button class="btn" type="submit">Plan isolated drill</button></form><div class="cards"><template :for="item in data().drills"><article class="panel"><span class="pill" :class="tone(item.status)">{{ item.status }}</span><h2>{{ item.scenario }}</h2><p>{{ item.isolatedTarget }}</p><small>objective {{ item.expectedRpoMinutes }}m RPO / {{ item.expectedRtoMinutes }}m RTO</small><p @show="item.measuredRpoMinutes!=null">measured {{ item.measuredRpoMinutes.toFixed(1) }}m RPO / {{ item.measuredRtoMinutes?.toFixed(1) }}m RTO · cleanup {{ item.cleanupVerified?'verified':'pending' }}</p><button class="btn sm" @show="item.status==='planned'" @click="runDrill(item.id)">Run drill</button></article></template></div></section>
15
+ </main><style>.actions,.target-row{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.tabs{display:flex;gap:.5rem;margin:1rem 0;border-bottom:1px solid var(--border)}.tabs button{background:none;border:0;color:var(--muted);padding:.75rem}.tabs .active{color:var(--text);border-bottom:2px solid var(--accent)}.safety{display:grid;grid-template-columns:auto 1fr;gap:1rem;margin-bottom:1rem}.grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.item{display:grid;grid-template-columns:auto 1fr auto;gap:.5rem;align-items:center;border-bottom:1px solid var(--border);padding:.75rem 0}.item p,.target-row{grid-column:1/-1}.form{display:flex;gap:1rem;align-items:end;margin-bottom:1rem}.form label{display:grid;gap:.3rem}.drill{display:grid;grid-template-columns:repeat(5,1fr) auto}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}.row{display:flex;justify-content:space-between;border-bottom:1px solid var(--border);padding:.4rem 0}.empty{color:var(--muted)}@media(max-width:900px){.grid,.cards{grid-template-columns:1fr}.drill{grid-template-columns:1fr 1fr}}@media(max-width:560px){.form,.drill{display:grid;grid-template-columns:1fr}.safety{grid-template-columns:1fr}}</style></body></html>
@@ -0,0 +1,76 @@
1
+ <script client>
2
+ import { usePolling } from '../../functions/usePolling.ts'
3
+
4
+ const ranges = { '1h': 3600000, '6h': 21600000, '24h': 86400000, '7d': 604800000, '30d': 2592000000 }
5
+ const range = state(new URLSearchParams(location.search).get('range') || '1h')
6
+ const timezone = state(Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC')
7
+ const compare = state(false), paused = state(false), textQuery = state(''), level = state(''), source = state('')
8
+ const status = state({ sources: [], errors: [], retention: {} })
9
+ const traffic = state([]), errors = state([]), latency = state([]), saturation = state([]), records = state([])
10
+ const nextCursor = state(''), savedQueries = state([]), correlation = state(null), message = state(''), savedName = state(''), tailState = state('connecting')
11
+ let tailSource = null
12
+
13
+ function windowParams() {
14
+ const to = new Date(), duration = ranges[range()] || ranges['1h'], from = new Date(to.getTime() - duration)
15
+ const params = new URLSearchParams({ from: from.toISOString(), to: to.toISOString(), timezone: timezone() })
16
+ if (compare()) { params.set('compareFrom', new Date(from.getTime() - duration).toISOString()); params.set('compareTo', from.toISOString()) }
17
+ return params
18
+ }
19
+ async function json(url, options) { const response = await fetch(url, options), body = await response.json(); if (!response.ok || body.ok === false) throw new Error(body.error || 'Telemetry request failed.'); return body }
20
+ function seriesUrl(names, aggregation, bucketMs) { const params = windowParams(); params.set('names', names.join(',')); params.set('aggregation', aggregation); params.set('bucketMs', String(bucketMs)); return '/api/telemetry/series?' + params }
21
+ async function load() {
22
+ if (paused()) return
23
+ const duration = ranges[range()] || ranges['1h'], bucket = Math.max(60000, Math.ceil(duration / 120 / 60000) * 60000), params = windowParams()
24
+ params.set('kinds', 'log,request,event,trace'); params.set('limit', '300'); if (textQuery()) params.set('text', textQuery()); if (level()) params.set('levels', level()); if (source()) params.set('sources', source())
25
+ const [statusBody, trafficBody, errorBody, latencyBody, saturationBody, recordBody, savedBody] = await Promise.all([
26
+ json('/api/telemetry/status'), json(seriesUrl(['request.count','traffic.requests'], 'sum', bucket)), json(seriesUrl(['request.error','errors.count'], 'sum', bucket)),
27
+ json(seriesUrl(['request.duration','latency.p95'], 'p95', bucket)), json(seriesUrl(['queue.depth','saturation.restarts','saturation.throttles','saturation.concurrency','host.disk.used_percent','runtime.memory.used'], 'max', bucket)),
28
+ json('/api/telemetry/query?' + params), json('/api/telemetry/saved-queries'),
29
+ ])
30
+ status.set(statusBody); traffic.set(trafficBody.series || []); errors.set(errorBody.series || []); latency.set(latencyBody.series || []); saturation.set(saturationBody.series || []); records.set(recordBody.records || []); nextCursor.set(recordBody.nextCursor || ''); savedQueries.set(savedBody.queries || [])
31
+ if (!tailSource && !paused()) connectTail()
32
+ }
33
+ const { pollingState, pollingError, pollingLabel, lastUpdatedAt, refresh } = usePolling(load, 15000)
34
+ function allPoints(signal) { return signal().flatMap(item => item.points || []).filter(point => !point.gap && point.value != null) }
35
+ function total(signal) { return allPoints(signal).reduce((sum, point) => sum + Number(point.value || 0), 0) }
36
+ function latest(signal) { const points = allPoints(signal); return points.length ? Number(points.at(-1).value) : null }
37
+ function p95() { const values = latency().map(item => [...(item.points || [])].reverse().find(point => !point.gap && point.value != null)?.value).filter(value => value != null).map(Number); return values.length ? Math.max(...values) : null }
38
+ function fmt(value, suffix = '') { return value == null ? 'Unavailable' : `${value >= 1000 ? (value / 1000).toFixed(1) + 'k' : Number(value).toFixed(value % 1 ? 1 : 0)}${suffix}` }
39
+ const requestTotal = derived(() => total(traffic)), errorTotal = derived(() => total(errors)), errorRate = derived(() => requestTotal() ? errorTotal() / requestTotal() * 100 : null)
40
+ function maxPoint(series) { return Math.max(1, ...(series.points || []).map(point => Number(point.value || 0))) }
41
+ function barHeight(series, point) { return point.gap ? 2 : Math.max(3, Math.round(Number(point.value || 0) / maxPoint(series) * 100)) }
42
+ function comparisonDelta(series) { const current = (series.points || []).reduce((sum, point) => sum + Number(point.value || 0), 0), previous = (series.comparison || []).reduce((sum, point) => sum + Number(point.value || 0), 0); return !series.comparison || !previous ? null : (current - previous) / previous * 100 }
43
+ function saturationSignal() { const candidates = saturation().flatMap(item => { const point = [...(item.points || [])].reverse().find(entry => !entry.gap && entry.value != null); return point ? [{ value: Number(point.value), name: item.name, unit: item.unit || '' }] : [] }); return candidates.sort((a,b) => b.value - a.value)[0] || null }
44
+ function freshnessTone(value) { return value === 'live' ? 'ok' : value === 'stale' ? 'warn' : 'bad' }
45
+ function closeTail() { if (tailSource) tailSource.close(); tailSource = null }
46
+ function connectTail() { closeTail(); if (paused()) { tailState.set('paused'); return } const params = windowParams(); params.set('kinds','log,request,event,trace'); params.set('limit','200'); if (textQuery()) params.set('text', textQuery()); if (level()) params.set('levels', level()); if (source()) params.set('sources', source()); tailState.set('connecting'); tailSource = new EventSource('/api/telemetry/tail?' + params); tailSource.addEventListener('record', event => { const record = JSON.parse(event.data); if (!records().some(item => item.id === record.id)) records.set([record, ...records()].slice(0, 500)); tailState.set('live') }); tailSource.addEventListener('heartbeat', () => tailState.set('live')); tailSource.onerror = () => tailState.set('reconnecting') }
47
+ function togglePause() { paused.set(!paused()); if (paused()) closeTail(); else { connectTail(); refresh() } }
48
+ function applyFilters() { const url = new URL(location.href); url.searchParams.set('range', range()); history.replaceState(null, '', url); connectTail(); refresh() }
49
+ async function loadMore() { if (!nextCursor()) return; const params = windowParams(); params.set('kinds','log,request,event,trace'); params.set('limit','300'); params.set('cursor', nextCursor()); const body = await json('/api/telemetry/query?' + params); records.set([...records(), ...(body.records || [])]); nextCursor.set(body.nextCursor || '') }
50
+ async function openCorrelation(record) { const key = record.traceId ? ['traceId', record.traceId] : record.requestId ? ['requestId', record.requestId] : record.releaseId ? ['releaseId', record.releaseId] : record.deploymentId ? ['deploymentId', record.deploymentId] : null; if (!key) { correlation.set({ key: 'No correlation ID', records: [record] }); return } const params = windowParams(); params.set(key[0], key[1]); params.set('limit','500'); const body = await json('/api/telemetry/query?' + params); correlation.set({ key: `${key[0]} ${key[1]}`, records: body.records || [] }) }
51
+ async function saveQuery() { const name = savedName().trim(); if (!name) { message.set('Enter a name before saving this query.'); return } const params = windowParams(), query = { from: params.get('from'), to: params.get('to'), range: range(), kinds: ['log','request','event','trace'], text: textQuery(), levels: level() ? [level()] : undefined, sources: source() ? [source()] : undefined }; const body = await json('/api/telemetry/saved-queries', { method: 'POST', headers: {'content-type':'application/json'}, body: JSON.stringify({ name, query }) }); savedQueries.set([body.query, ...savedQueries().filter(item => item.id !== body.query.id)]); savedName.set(''); message.set(`Saved ${name}.`) }
52
+ function useSaved(item) { const query = item.query || {}; textQuery.set(query.text || ''); level.set(query.levels?.[0] || ''); source.set(query.sources?.[0] || ''); if (query.range) range.set(query.range); applyFilters() }
53
+ async function exportData(format) { const params = windowParams(), response = await fetch('/api/telemetry/export', { method: 'POST', headers: {'content-type':'application/json'}, body: JSON.stringify({ from: params.get('from'), to: params.get('to'), kinds: ['log','request','event','trace','metric'], text: textQuery(), levels: level() ? [level()] : undefined, sources: source() ? [source()] : undefined, format }) }); if (!response.ok) { const body = await response.json(); throw new Error(body.error || 'Export failed.') } const blob = await response.blob(), url = URL.createObjectURL(blob), link = document.createElement('a'); link.href = url; link.download = `ts-cloud-telemetry.${format}`; link.click(); URL.revokeObjectURL(url) }
54
+ async function savePolicy(event) { event.preventDefault(); const values = Object.fromEntries(new FormData(event.currentTarget)); for (const key of ['rawDays','downsampleAfterDays','maxRecords','samplingRate','estimatedStorageUsdPerGbMonth']) values[key] = Number(values[key]); for (const key of ['collectLogs','collectTraces','collectRequestAnalytics']) values[key] = event.currentTarget.elements[key].checked; const body = await json('/api/telemetry/settings', { method:'PATCH', headers:{'content-type':'application/json'}, body:JSON.stringify(values) }); status.set({ ...status(), policy: body.policy, retention: body.policy }); message.set('Telemetry policy saved. New collection runs use these limits.'); refresh() }
55
+ addEventListener('pagehide', closeTail)
56
+ </script>
57
+ <!DOCTYPE html>
58
+ <html lang="en" data-theme="dark"><head>@include('../partials/head', { title: 'ts-cloud - Observability' })</head><body>
59
+ @include('../partials/nav', { active: 'observability', env: 'production' })
60
+ <main class="telemetry-wrap wrap" id="main-content">
61
+ <div class="crumbs">Operations <span class="sep">/</span> Observability</div>
62
+ <header class="page"><div><h1>Observability</h1><p>Correlated metrics, logs, requests, traces, releases, and runtime context—with explicit source freshness and retention.</p></div><div class="header-actions"><span class="pill" :class="pollingState() === 'live' ? 'ok' : 'warn'">{{ paused() ? 'paused' : pollingLabel() }}</span><span class="pill" :class="tailState() === 'live' ? 'ok' : 'warn'">tail {{ tailState() }}</span><small>{{ lastUpdatedAt() ? lastUpdatedAt().toLocaleTimeString() : 'connecting' }}</small><button class="btn ghost sm" type="button" @click="togglePause()">{{ paused() ? 'Resume' : 'Pause' }}</button><button class="btn ghost sm" type="button" @click="refresh()">Refresh</button></div></header>
63
+ <p class="bad notice" role="status" @show="!!pollingError()">{{ pollingError() }}</p><p class="notice" role="status" @show="!!message()">{{ message() }}</p>
64
+ <section class="panel timebar"><label>Time range<select :value="range()" @change="range.set($event.target.value); applyFilters()"><option value="1h">Last hour</option><option value="6h">Last 6 hours</option><option value="24h">Last 24 hours</option><option value="7d">Last 7 days</option><option value="30d">Last 30 days</option></select></label><label>Timezone<input :value="timezone()" @change="timezone.set($event.target.value); applyFilters()"></label><label class="check"><input type="checkbox" :checked="compare()" @change="compare.set($event.target.checked); applyFilters()"> Compare previous period</label><span class="note">Auto-refresh 15s · provider refresh is cached separately · {{ status().policy?.samplingRate != null ? Math.round(status().policy.samplingRate * 100) + '% sampled' : 'sampling unavailable' }}</span></section>
65
+ <details class="panel policy"><summary>Collection, retention, and estimated cost</summary><div class="cost-summary"><span>~{{ fmt(status().estimatedMonthlyBytes, ' B/month') }}</span><span>{{ status().costEstimateConfigured ? '$' + status().estimatedMonthlyCostUsd + '/month at configured storage rate' : 'Cost unavailable until a storage $/GB-month rate is configured' }}</span></div><form @submit="savePolicy($event)"><label>Raw retention days<input name="rawDays" type="number" min="1" max="3650" :value="status().policy?.rawDays || 30"></label><label>Downsample after days<input name="downsampleAfterDays" type="number" min="1" max="3650" :value="status().policy?.downsampleAfterDays || 7"></label><label>Maximum local records<input name="maxRecords" type="number" min="1000" :value="status().policy?.maxRecords || 1000000"></label><label>Sample rate (0.01–1)<input name="samplingRate" type="number" min="0.01" max="1" step="0.01" :value="status().policy?.samplingRate || 1"></label><label>Storage $/GB-month<input name="estimatedStorageUsdPerGbMonth" type="number" min="0" step="0.001" :value="status().policy?.estimatedStorageUsdPerGbMonth || 0"></label><label class="check"><input name="collectLogs" type="checkbox" :checked="status().policy?.collectLogs !== false"> Logs</label><label class="check"><input name="collectTraces" type="checkbox" :checked="status().policy?.collectTraces !== false"> Traces</label><label class="check"><input name="collectRequestAnalytics" type="checkbox" :checked="status().policy?.collectRequestAnalytics !== false"> Request analytics</label><button class="btn sm" type="submit">Save policy</button></form><p class="note">Lower sampling and retention reduce local ingestion/storage, but can hide rare events. Cost is an estimate using your configured storage rate; provider allocation is never invented.</p></details>
66
+ <section class="source-grid" aria-label="Telemetry collection sources"><template :for="item in status().sources || []"><article class="panel source-card"><div><b>{{ item.source }}</b><span class="pill" :class="freshnessTone(item.freshness)">{{ item.freshness }}</span></div><small>last {{ item.lastObservedAt ? new Date(item.lastObservedAt).toLocaleString() : 'never' }} · lag {{ item.lagSeconds ?? '—' }}s</small><small>{{ item.retentionDays }}d retention · {{ Math.round(item.samplingRate * 100) }}% sampled · ~{{ fmt(item.estimatedDailyBytes, ' B/day') }}</small><p class="note" @show="!!item.message">{{ item.message }}</p></article></template><article class="panel source-card" @show="!(status().sources || []).length"><b>No collection sources</b><small>Refresh to collect live provider data. Empty data is never replaced with samples.</small></article></section>
67
+ <section class="golden"><article class="panel"><h3>Traffic</h3><strong>{{ fmt(requestTotal()) }}</strong><small>requests · summed buckets</small></article><article class="panel"><h3>Errors</h3><strong>{{ fmt(errorRate(), '%') }}</strong><small>{{ fmt(errorTotal()) }} errors</small></article><article class="panel"><h3>Latency</h3><strong>{{ fmt(p95(), ' ms') }}</strong><small>latest p95 · worst available source</small></article><article class="panel"><h3>Saturation</h3><strong>{{ fmt(saturationSignal()?.value, saturationSignal()?.unit ? ' ' + saturationSignal().unit : '') }}</strong><small>{{ saturationSignal()?.name || 'unsupported in this window' }}</small></article></section>
68
+ <section class="chart-grid"><template :for="series in [...traffic(), ...errors(), ...latency(), ...saturation()].slice(0, 8)"><article class="chart-card panel"><div class="chart-head"><b>{{ series.name }}</b><span>{{ series.source }} · {{ series.aggregation }} / {{ Math.round(series.bucketMs / 60000) }}m</span></div><div class="telemetry-bars" :aria-label="series.name + ' time series'"><template :for="point in series.points"><i :class="{ gap: point.gap }" :style="'height:' + barHeight(series, point) + '%'" :title="point.label + ': ' + (point.gap ? 'gap' : point.value)"></i></template></div><small>{{ timezone() }} · gaps shown as dim baseline bars <span @show="comparisonDelta(series) != null">· {{ comparisonDelta(series) > 0 ? '+' : '' }}{{ comparisonDelta(series)?.toFixed(1) }}% vs previous</span></small></article></template><article class="chart-card panel" @show="![...traffic(), ...errors(), ...latency(), ...saturation()].length"><b>No metric series</b><p class="note">The selected sources have not emitted these golden signals in this window, or the provider reports them as unsupported.</p></article></section>
69
+ <section class="section"><div class="section-title"><div><h2>Logs, requests, traces, and events</h2><p class="note">Bodies, authorization, cookies, query values, and configured PII are removed before persistence.</p></div><div class="header-actions"><input class="save-name" :value="savedName()" @input="savedName.set($event.target.value)" placeholder="Query name" aria-label="Saved query name"><button class="btn ghost sm" type="button" @click="saveQuery()">Save query</button><button class="btn ghost sm" type="button" @click="exportData('json')">JSON</button><button class="btn ghost sm" type="button" @click="exportData('csv')">CSV</button></div></div><div class="log-filters panel"><label>Search<input type="search" :value="textQuery()" @input="textQuery.set($event.target.value)" @change="applyFilters()" placeholder="Message, request, release…"></label><label>Level<select :value="level()" @change="level.set($event.target.value); applyFilters()"><option value="">All</option><option value="debug">debug</option><option value="info">info</option><option value="warning">warning</option><option value="error">error</option></select></label><label>Source<input :value="source()" @change="source.set($event.target.value); applyFilters()" placeholder="cloudwatch:http"></label></div><div class="saved" @show="savedQueries().length"><span>Saved:</span><template :for="item in savedQueries()"><button type="button" class="btn ghost sm" @click="useSaved(item)">{{ item.name }}</button></template></div><div class="panel telemetry-table-wrap"><table class="telemetry-table"><thead><tr><th scope="col">Time</th><th scope="col">Kind</th><th scope="col">Source</th><th scope="col">Signal</th><th scope="col">Message / value</th><th scope="col">Correlation</th></tr></thead><tbody><template :for="record in records()"><tr tabindex="0" @click="openCorrelation(record)" @keydown.enter="openCorrelation(record)"><td>{{ new Date(record.timestamp).toLocaleString([], {timeZone: timezone()}) }}</td><td><span class="tag">{{ record.kind }}</span></td><td>{{ record.source }}</td><td><span class="pill" :class="record.level === 'error' ? 'bad' : record.level === 'warning' ? 'warn' : ''">{{ record.level || record.name }}</span></td><td class="mono">{{ record.message || (record.value != null ? record.value + ' ' + (record.unit || '') : record.durationMs != null ? record.durationMs + 'ms' : record.name) }}</td><td class="mono">{{ record.requestId || record.traceId || record.releaseId || record.deploymentId || '—' }}</td></tr></template></tbody></table><div class="empty" @show="!records().length"><strong>No telemetry in this window</strong><span>Empty, unavailable, and filtered results remain explicit; no demo rows are shown.</span></div><button class="btn ghost load-more sm" type="button" @show="!!nextCursor()" @click="loadMore()">Load older records</button></div></section>
70
+ <aside class="correlation panel" @show="!!correlation()"><button class="detail-close" type="button" aria-label="Close correlation detail" @click="correlation.set(null)">×</button><h2>Correlation</h2><p class="mono">{{ correlation()?.key }}</p><div class="timeline"><template :for="item in correlation()?.records || []"><article><span>{{ new Date(item.timestamp).toLocaleString() }}</span><b>{{ item.name }}</b><small>{{ item.source }} · {{ item.kind }} · {{ item.message || item.value || '' }}</small><nav><a @show="!!item.releaseId" :href="'/operations/releases?release=' + encodeURIComponent(item.releaseId)">Release</a><a @show="!!item.workloadId" :href="'/operations/workloads?search=' + encodeURIComponent(item.workloadId)">Workload</a></nav></article></template></div></aside>
71
+ </main>
72
+ <style>
73
+ .telemetry-wrap{max-width:1540px}.timebar,.log-filters{display:flex;gap:12px;align-items:end;flex-wrap:wrap;padding:14px}.timebar label,.log-filters label,.policy label{display:grid;gap:5px;color:var(--txt3);font-size:11px}.timebar select,.timebar input,.log-filters select,.log-filters input,.policy input,.save-name{min-width:160px;background:var(--bg2);border:1px solid var(--line);border-radius:8px;color:var(--txt);padding:8px}.timebar .check,.policy .check{display:flex;align-items:center;gap:7px;padding:8px}.timebar .check input,.policy .check input{min-width:0}.policy{margin-top:10px}.policy summary{cursor:pointer;font-weight:600}.policy form,.cost-summary{display:flex;gap:12px;align-items:end;flex-wrap:wrap;margin-top:14px}.cost-summary{justify-content:space-between;color:var(--txt2)}.source-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:10px;margin:12px 0}.source-card{display:grid;gap:5px;padding:13px}.source-card>div{display:flex;justify-content:space-between;gap:8px}.source-card small{color:var(--txt3)}.golden{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:16px 0}.golden article{display:grid;gap:5px}.golden strong{font-size:28px}.golden small{color:var(--txt3)}.chart-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.chart-card{min-width:0}.chart-head{display:flex;justify-content:space-between;gap:10px}.chart-head span,.chart-card small{color:var(--txt3);font-size:10px}.telemetry-bars{display:flex;align-items:end;gap:2px;height:110px;margin:14px 0 7px;border-bottom:1px solid var(--line)}.telemetry-bars i{flex:1;min-width:2px;background:var(--accent);border-radius:2px 2px 0 0}.telemetry-bars i.gap{opacity:.18;background:var(--txt3)}.section-title{display:flex;align-items:end;justify-content:space-between;gap:12px}.saved{display:flex;gap:7px;align-items:center;flex-wrap:wrap;margin:8px 0}.telemetry-table-wrap{overflow:auto;padding:0}.telemetry-table{min-width:1050px}.telemetry-table tbody tr{cursor:pointer}.telemetry-table td{max-width:420px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.load-more{margin:12px}.correlation{position:fixed;z-index:20;right:20px;top:20px;bottom:20px;width:min(480px,calc(100vw - 40px));overflow:auto;padding:20px;box-shadow:0 20px 70px #000b}.detail-close{float:right;border:0;background:transparent;color:var(--txt3);font-size:24px}.timeline{display:grid;gap:14px;margin-top:20px;border-left:1px solid var(--line);padding-left:16px}.timeline article{display:grid;gap:4px}.timeline span,.timeline small{color:var(--txt3);font-size:11px}.timeline nav{display:flex;gap:10px}
74
+ @media(max-width:900px){.golden{grid-template-columns:repeat(2,1fr)}.chart-grid{grid-template-columns:1fr}}@media(max-width:600px){.golden{grid-template-columns:1fr}.timebar>*{width:100%}.section-title{align-items:start;flex-direction:column}.source-grid{grid-template-columns:1fr}.telemetry-table{min-width:0}.telemetry-table thead{display:none}.telemetry-table tbody,.telemetry-table tr,.telemetry-table td{display:block}.telemetry-table tr{padding:12px;border-bottom:1px solid var(--line)}.telemetry-table td{padding:4px 0;white-space:normal}.telemetry-table td:before{display:inline-block;width:90px;color:var(--txt3);font-size:10px}.telemetry-table td:nth-child(1):before{content:'Time'}.telemetry-table td:nth-child(2):before{content:'Kind'}.telemetry-table td:nth-child(3):before{content:'Source'}.telemetry-table td:nth-child(4):before{content:'Signal'}.telemetry-table td:nth-child(5):before{content:'Value'}.telemetry-table td:nth-child(6):before{content:'Correlation'}}
75
+ </style>
76
+ </body></html>
@@ -0,0 +1,62 @@
1
+ <script server>
2
+ let __p = null
3
+ try { __p = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __p = null }
4
+ const capabilities = __p?.viewer?.capabilities ?? []
5
+ const canDeploy = capabilities.includes('deployments:create')
6
+ const canDestroy = capabilities.includes('deployments:cancel')
7
+ const canConfigure = capabilities.includes('config:write')
8
+ const canCleanup = capabilities.includes('automation:manage')
9
+ </script>
10
+ <script client>
11
+ const data = state({ previews: [], definitions: [], resources: [], environments: [] })
12
+ const message = state('')
13
+ const loading = state(true)
14
+ const destroyId = state('')
15
+ const destroyConfirm = state('')
16
+ const cleanup = state({ dryRun: true, confirm: '', keepCount: 10 })
17
+ async function request(path, body) { const response = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); const result = await response.json(); if (!response.ok || result.ok === false) throw new Error(result.error || 'Request failed.'); return result }
18
+ async function refresh() { try { const response = await fetch('/api/previews'); const result = await response.json(); if (!response.ok) throw new Error(result.error || 'Previews could not be loaded.'); data.set(result) } catch (error) { message.set(error.message) } finally { loading.set(false) } }
19
+ function tone(status) { return ['active','destroyed'].includes(status) ? 'ok' : ['failed','cleanup_failed'].includes(status) ? 'bad' : 'warn' }
20
+ function age(value) { const hours = Math.max(0, Math.round((Date.now() - new Date(value).getTime()) / 3600000)); return hours < 24 ? hours + 'h' : Math.round(hours / 24) + 'd' }
21
+ async function configure(event) { event.preventDefault(); const form = event.currentTarget; try { await request('/api/previews/definitions', { resourceId: form.resourceId.value, baseEnvironmentId: form.baseEnvironmentId.value, domainPattern: form.domainPattern.value, ttlHours: Number(form.ttlHours.value), keepCount: Number(form.keepCount.value), databaseStrategy: form.databaseStrategy.value, publicAccess: form.publicAccess.checked, authenticationRequired: true, allowForks: form.allowForks.checked, inheritedSecrets: form.inheritedSecrets.value.split(/[,\n]/).map(v => v.trim()).filter(Boolean), maxMonthlyCost: Number(form.maxMonthlyCost.value), maxCpu: Number(form.maxCpu.value), maxMemoryMb: Number(form.maxMemoryMb.value) }); form.reset(); message.set('Preview policy configured.'); await refresh() } catch (error) { message.set(error.message) } }
22
+ async function deploy(event) { event.preventDefault(); const form = event.currentTarget; try { await request('/api/previews/deploy', { definitionId: form.definitionId.value, branch: form.branch.value, pullRequestNumber: Number(form.pullRequestNumber.value) || undefined, commitSha: form.commitSha.value }); message.set('Immutable preview deployment queued.'); await refresh() } catch (error) { message.set(error.message) } }
23
+ async function extend(id) { try { await request('/api/previews/extend', { id, hours: 24 }); message.set('Preview extended by 24 hours.'); await refresh() } catch (error) { message.set(error.message) } }
24
+ async function rebuild(id) { try { await request('/api/previews/rebuild', { id }); message.set('Exact recorded commit queued for rebuild.'); await refresh() } catch (error) { message.set(error.message) } }
25
+ async function destroy() { const item = data().previews.find(value => value.id === destroyId()); if (!item) return; try { await request('/api/previews/destroy', { id: item.id, confirm: destroyConfirm() }); destroyId.set(''); destroyConfirm.set(''); message.set('Tagged teardown queued.'); await refresh() } catch (error) { message.set(error.message) } }
26
+ async function runCleanup() { try { const result = await request('/api/previews/cleanup', cleanup()); message.set(cleanup().dryRun ? result.candidates.length + ' cleanup candidate(s).' : result.operations.length + ' teardown job(s) queued.'); await refresh() } catch (error) { message.set(error.message) } }
27
+ onMount(() => { refresh(); const timer = setInterval(refresh, 5000); return () => clearInterval(timer) })
28
+ </script>
29
+ <!DOCTYPE html>
30
+ <html lang="en" data-theme="dark">
31
+ <head>@include('../partials/head', { title: 'ts-cloud - Preview environments' })</head>
32
+ <body>
33
+ @include('../partials/nav', { active: 'previews', env: 'production' })
34
+ <div class="preview-wrap wrap" id="main-content">
35
+ <div class="crumbs">Operations <span class="sep">/</span> Preview environments</div>
36
+ <header class="page preview-hero"><div><h1>Preview environments</h1><p>Isolated, exact-commit deployments with stable URLs, bounded resources, and automatic teardown.</p></div><button class="btn ghost" type="button" @click="refresh()">Refresh</button></header>
37
+ <p class="notice" @show="!!message()">{{ message() }}</p>
38
+ <section class="section stats"><div class="panel"><small>Total</small><strong>{{ data().previews.length }}</strong></div><div class="panel"><small>Active</small><strong>{{ data().previews.filter(v=>v.status==='active').length }}</strong></div><div class="panel"><small>Expiring</small><strong>{{ data().previews.filter(v=>!['destroyed','destroying'].includes(v.status)&&new Date(v.expiresAt)-Date.now()<86400000).length }}</strong></div><div class="panel"><small>Cleanup errors</small><strong>{{ data().previews.filter(v=>v.status==='cleanup_failed').length }}</strong></div></section>
39
+ <section class="panel preview-list section">
40
+ <div class="section-heading"><div><h2>Environments</h2><p class="note">A URL always corresponds to the immutable commit shown here.</p></div></div>
41
+ <div class="empty" @show="!loading() && !data().previews.length"><strong>No previews yet</strong><span>Configure an application policy or enable PR previews on a Git binding.</span></div>
42
+ <div class="preview-grid">
43
+ <article class="preview-card" @each="item in data().previews" :key="item.id">
44
+ <div class="card-head"><div><h3>{{ item.name }}</h3><p class="note">{{ item.pullRequestNumber ? 'PR #' + item.pullRequestNumber : item.branch }} · {{ item.commitSha.slice(0,12) }}</p></div><span class="pill" :class="tone(item.status)">{{ item.status }}</span></div>
45
+ <a class="preview-url" :href="item.url" target="_blank" rel="noopener">{{ item.url || 'URL pending' }}</a>
46
+ <dl><div><dt>Age / expiry</dt><dd>{{ age(item.createdAt) }} · {{ new Date(item.expiresAt).toLocaleString() }}</dd></div><div><dt>Latest deployment</dt><dd class="mono">{{ item.latestOperationId || '—' }}</dd></div><div><dt>Estimated cost</dt><dd>{{ item.costEstimate == null ? 'Not available' : '$' + item.costEstimate.toFixed(2) + '/mo' }}</dd></div><div @show="!!item.teardownError"><dt>Teardown error</dt><dd class="bad-text">{{ item.teardownError }}</dd></div></dl>
47
+ <div class="actions">@if (canDeploy)<button class="btn ghost sm" @click="extend(item.id)">Extend 24h</button><button class="btn ghost sm" @click="rebuild(item.id)">Rebuild SHA</button>@endif @if (canDestroy)<button class="btn danger sm" @click="destroyId.set(item.id);destroyConfirm.set('')">Delete…</button>@endif <a class="btn ghost sm" :href="'/operations/queue?state=all'">Operation</a></div>
48
+ <div class="confirm" @show="destroyId()===item.id"><p>Type <b>{{ item.name }}</b>. Only resources with matching preview tags are eligible.</p><input :value="destroyConfirm()" @input="destroyConfirm.set($event.target.value)"><button class="btn danger sm" :disabled="destroyConfirm()!==item.name" @click="destroy()">Queue teardown</button></div>
49
+ </article>
50
+ </div>
51
+ </section>
52
+ <div class="forms section">
53
+ @if (canDeploy)<form class="panel" @submit="deploy($event)"><h2>Deploy exact commit</h2><label>Policy<select name="definitionId" required><option value="">Select application…</option><option @each="item in data().definitions" :value="item.id">{{ data().resources.find(v=>v.id===item.resourceId)?.name || item.resourceId }}</option></select></label><label>Branch<input name="branch" required placeholder="feature/search"></label><label>Commit SHA<input name="commitSha" required minlength="40" maxlength="64" class="mono"></label><label>Pull request (optional)<input name="pullRequestNumber" type="number" min="1"></label><button class="btn" type="submit">Queue preview</button></form>@endif
54
+ @if (canConfigure)<form class="panel" @submit="configure($event)"><h2>Configure application</h2><label>Application<select name="resourceId" required><option @each="item in data().resources" :value="item.id">{{ item.name }}</option></select></label><label>Base environment<select name="baseEnvironmentId" required><option @each="item in data().environments" :value="item.id">{{ item.name }}</option></select></label><label>Domain pattern<input name="domainPattern" required value="https://{name}.preview.example.com"></label><div class="pair"><label>TTL hours<input name="ttlHours" type="number" value="24" min="1" max="720"></label><label>Keep newest<input name="keepCount" type="number" value="10" min="1" max="100"></label></div><div class="pair"><label>Max CPU<input name="maxCpu" type="number" value="1" step="0.1"></label><label>Max memory MB<input name="maxMemoryMb" type="number" value="1024"></label></div><label>Monthly budget cap<input name="maxMonthlyCost" type="number" value="25" step="0.01"></label><label>Database<select name="databaseStrategy"><option value="disabled">Disabled</option><option value="isolated">Isolated</option><option value="snapshot">Snapshot clone</option><option value="shared_read_only">Shared read-only</option></select></label><label>Explicit secret names<textarea name="inheritedSecrets" placeholder="PREVIEW_API_KEY"></textarea></label><label class="check"><input name="publicAccess" type="checkbox"> Public (authentication remains required)</label><label class="check"><input name="allowForks" type="checkbox"> Allow untrusted forks (secrets are still stripped)</label><button class="btn" type="submit">Save policy</button></form>@endif
55
+ @if (canCleanup)<section class="panel"><h2>Cleanup</h2><p class="note">Dry-run first. Teardown never touches untagged or shared resources.</p><label class="check"><input type="checkbox" :checked="cleanup().dryRun" @change="cleanup.set({...cleanup(),dryRun:$event.target.checked})"> Dry run</label><label>Keep newest<input type="number" :value="cleanup().keepCount" @input="cleanup.set({...cleanup(),keepCount:Number($event.target.value)})"></label><label @show="!cleanup().dryRun">Type cleanup previews<input :value="cleanup().confirm" @input="cleanup.set({...cleanup(),confirm:$event.target.value})"></label><button class="btn" :disabled="!cleanup().dryRun&&cleanup().confirm!=='cleanup previews'" @click="runCleanup()">{{ cleanup().dryRun ? 'Preview cleanup' : 'Queue cleanup' }}</button></section>@endif
56
+ </div>
57
+ </div>
58
+ <style>
59
+ .preview-wrap{max-width:1450px}.preview-hero,.card-head,.actions{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.stats strong{display:block;font-size:1.7rem;margin-top:8px}.preview-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.preview-card{border:1px solid var(--line);border-radius:11px;padding:16px;background:var(--bg2);display:grid;gap:13px}.preview-card h3{margin:0}.preview-url{overflow:hidden;text-overflow:ellipsis}.preview-card dl{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:0}.preview-card dt{color:var(--txt3);font-size:11px}.preview-card dd{margin:3px 0 0;font-size:12px;overflow-wrap:anywhere}.actions{justify-content:flex-start;flex-wrap:wrap}.confirm{border:1px solid rgba(235,100,100,.35);padding:12px;border-radius:8px;display:grid;gap:8px}.confirm p{margin:0;font-size:12px}.forms{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;align-items:start}.forms .panel{display:grid;gap:10px}.forms h2{margin:0 0 5px}.forms label{display:grid;gap:5px;color:var(--txt3);font-size:12px}.forms :is(input,select,textarea){width:100%}.forms .check{display:flex;align-items:center}.forms .check input{width:auto}.pair{display:grid;grid-template-columns:1fr 1fr;gap:8px}.bad-text{color:#ff9b9b}.notice{padding:10px}.empty{padding:25px}.empty span{display:block;color:var(--txt3);margin-top:5px}
60
+ @media(max-width:1000px){.forms{grid-template-columns:1fr 1fr}.preview-grid{grid-template-columns:1fr}}@media(max-width:650px){.stats,.forms{grid-template-columns:1fr 1fr}.preview-card dl{grid-template-columns:1fr}.preview-hero{display:grid}}@media(max-width:440px){.stats,.forms{grid-template-columns:1fr}.pair{grid-template-columns:1fr}}
61
+ </style>
62
+ </body></html>