@stacksjs/ts-cloud 0.7.56 → 0.7.57

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,18 @@
1
+ <script server>
2
+ const meta = { title: 'Page not found · ts-cloud' }
3
+ </script>
4
+ <!DOCTYPE html>
5
+ <html lang="en" data-theme="dark">
6
+ <head>@include('./partials/head', { title: meta.title })</head>
7
+ <body>
8
+ @include('./partials/nav', { active: '', name: 'ts-cloud', env: 'production' })
9
+ <div class="wrap">
10
+ <div class="state-page">
11
+ <span class="state-code">404</span>
12
+ <h1>This resource is not here</h1>
13
+ <p>The link may be stale, the resource may have been deleted, or it may belong to another environment.</p>
14
+ <div class="state-actions"><a class="btn" href="/">Open dashboard</a><a class="btn ghost" href="/server/sites">Browse services</a></div>
15
+ </div>
16
+ </div>
17
+ </body>
18
+ </html>
@@ -0,0 +1,50 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>@include('./partials/head', { title: 'ts-cloud - Accept invitation' })</head>
4
+ <body class="auth-page">
5
+ <main class="auth-card" id="main-content">
6
+ <a class="brand" href="/"><span class="dot"></span><span>ts-cloud</span></a>
7
+ <div>
8
+ <span class="context-label">Organization invitation</span>
9
+ <h1>Join your team</h1>
10
+ <p>Choose the dashboard username and password you will use after this invitation is accepted.</p>
11
+ </div>
12
+ <form id="accept-invitation-form" class="auth-form">
13
+ <div class="field-block"><label for="invite-name">Name</label><input id="invite-name" name="name" autocomplete="name" placeholder="Chris Breuer"></div>
14
+ <div class="field-block"><label for="invite-username">Username</label><input id="invite-username" name="username" autocomplete="username" minlength="2" maxlength="32" required></div>
15
+ <div class="field-block"><label for="invite-password">Password</label><input id="invite-password" name="password" type="password" autocomplete="new-password" minlength="12" required><small>At least 12 characters.</small></div>
16
+ <button class="btn" type="submit">Accept invitation</button>
17
+ <div id="invitation-status" class="auth-status" role="status"></div>
18
+ </form>
19
+ <p class="note">If this link expired or was already used, ask an organization owner to resend the invitation.</p>
20
+ </main>
21
+ <script>
22
+ (() => {
23
+ const form = document.querySelector('#accept-invitation-form')
24
+ const status = document.querySelector('#invitation-status')
25
+ const token = new URLSearchParams(location.search).get('token') || ''
26
+ if (!token) {
27
+ status.textContent = 'This invitation link is missing its token. Ask an owner to resend it.'
28
+ form.querySelector('button').disabled = true
29
+ }
30
+ form.addEventListener('submit', async (event) => {
31
+ event.preventDefault()
32
+ const data = new FormData(form)
33
+ status.textContent = 'Accepting invitation…'
34
+ const response = await fetch('/api/invitations/accept', {
35
+ method: 'POST',
36
+ headers: { 'content-type': 'application/json' },
37
+ body: JSON.stringify({ token, name: data.get('name'), username: data.get('username'), password: data.get('password') }),
38
+ })
39
+ const result = await response.json().catch(() => ({}))
40
+ if (!response.ok) {
41
+ status.textContent = result.error || 'This invitation could not be accepted.'
42
+ return
43
+ }
44
+ status.textContent = 'Invitation accepted. Opening your dashboard…'
45
+ location.assign('/')
46
+ })
47
+ })()
48
+ </script>
49
+ </body>
50
+ </html>
@@ -0,0 +1,18 @@
1
+ <script server>
2
+ const meta = { title: 'Access unavailable · ts-cloud' }
3
+ </script>
4
+ <!DOCTYPE html>
5
+ <html lang="en" data-theme="dark">
6
+ <head>@include('./partials/head', { title: meta.title })</head>
7
+ <body>
8
+ @include('./partials/nav', { active: '', name: 'ts-cloud', env: 'production' })
9
+ <div class="wrap">
10
+ <div class="state-page">
11
+ <span class="state-code">Access</span>
12
+ <h1>This area is outside your current grant</h1>
13
+ <p>Your account is signed in, but this page requires box-level access. You can return to the services explicitly shared with you.</p>
14
+ <div class="state-actions"><a class="btn" href="/server/sites">Open my services</a><a class="btn ghost" href="/server/deployments">View deployments</a></div>
15
+ </div>
16
+ </div>
17
+ </body>
18
+ </html>
@@ -0,0 +1,187 @@
1
+ <script server>
2
+ let __d = null
3
+ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
4
+ const server = __d?.server ?? { name: 'ts-cloud' }
5
+ </script>
6
+ <script client>
7
+ const accounts = state([])
8
+ const project = state(null)
9
+ const environments = state([])
10
+ const resources = state([])
11
+ const message = state('')
12
+ const messageKind = state('')
13
+ const reveal = state(null)
14
+ const accountSlug = state('')
15
+ const accountName = state('')
16
+ const accountRole = state('deployer')
17
+ const accountScopeType = state('environment')
18
+ const accountScopeId = state('')
19
+ const selectedAccount = state('')
20
+ const tokenName = state('CI token')
21
+ const tokenCapabilities = state(['project:read', 'deployments:read', 'deployments:create'])
22
+ const tokenScopeType = state('environment')
23
+ const tokenScopeId = state('')
24
+ const tokenExpiry = state('')
25
+ const password = state('')
26
+ const mfaCode = state('')
27
+
28
+ const capabilities = [
29
+ ['project:read', 'Read project and safe service metadata'],
30
+ ['deployments:read', 'Read deployment and operation history'],
31
+ ['deployments:create', 'Create or roll back deployments'],
32
+ ['runtime:read', 'Read runtime health'],
33
+ ['runtime:logs', 'Read scoped logs'],
34
+ ['audit:read', 'Read scoped audit events'],
35
+ ['backups:read', 'Read backup metadata'],
36
+ ['backups:create', 'Create backups'],
37
+ ]
38
+
39
+ function setMessage(text, kind) { message.set(text); messageKind.set(kind || '') }
40
+ function setScope(type, signal) {
41
+ if (type === 'organization') signal.set('')
42
+ else if (type === 'project') signal.set(project()?.id || '')
43
+ else if (type === 'environment') signal.set(environments()[0]?.id || '')
44
+ else signal.set(resources()[0]?.id || '')
45
+ }
46
+ function scopeLabel(scope) {
47
+ if (!scope || scope.type === 'organization') return 'Entire organization'
48
+ if (scope.type === 'project') return 'Project · ' + (project()?.name || scope.id)
49
+ if (scope.type === 'environment') return 'Environment · ' + (environments().find(item => item.id === scope.id)?.name || scope.id)
50
+ return 'Service · ' + (resources().find(item => item.id === scope.id)?.name || scope.id)
51
+ }
52
+ function tokenState(token) {
53
+ if (token.state === 'revoked') return 'Revoked'
54
+ if (token.state === 'expired') return 'Expired'
55
+ return 'Active'
56
+ }
57
+ function toggleCapability(value) {
58
+ tokenCapabilities.set(tokenCapabilities().includes(value) ? tokenCapabilities().filter(item => item !== value) : [...tokenCapabilities(), value])
59
+ }
60
+ async function load() {
61
+ try {
62
+ const [automationResponse, organizationResponse] = await Promise.all([fetch('/api/automation'), fetch('/api/organization')])
63
+ const automation = await automationResponse.json(); const organization = await organizationResponse.json()
64
+ if (!automationResponse.ok) throw new Error(automation.error || 'Could not load automation settings.')
65
+ if (!organizationResponse.ok) throw new Error(organization.error || 'Could not load organization scopes.')
66
+ accounts.set(automation.accounts || []); project.set(organization.project); environments.set(organization.environments || []); resources.set(organization.resources || [])
67
+ if (!accountScopeId()) setScope(accountScopeType(), accountScopeId)
68
+ if (!tokenScopeId()) setScope(tokenScopeType(), tokenScopeId)
69
+ if (!selectedAccount() && accounts().length) selectAccount(accounts()[0])
70
+ } catch (error) { setMessage(error?.message || 'Could not load automation settings.', 'bad') }
71
+ }
72
+ function selectAccount(item) {
73
+ selectedAccount.set(item.account.id)
74
+ tokenScopeType.set(item.membership?.scope?.type || 'organization')
75
+ tokenScopeId.set(item.membership?.scope?.id || '')
76
+ }
77
+ async function stepUp() {
78
+ const response = await fetch('/api/auth/step-up', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ password: password(), code: mfaCode() }) })
79
+ const data = await response.json().catch(() => ({}))
80
+ if (!response.ok) throw new Error(data.error || 'Recent authentication is required.')
81
+ }
82
+ async function createAccount() {
83
+ setMessage('Creating service account…', '')
84
+ try {
85
+ await stepUp()
86
+ const response = await fetch('/api/automation/service-accounts', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slug: accountSlug(), name: accountName(), roleTemplate: accountRole(), scopeType: accountScopeType(), scopeId: accountScopeId() }) })
87
+ const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || 'Service account could not be created.')
88
+ accountSlug.set(''); accountName.set(''); password.set(''); mfaCode.set(''); setMessage('Service account created. Add a narrowly scoped token next.', 'ok'); await load(); selectedAccount.set(data.serviceAccount.id)
89
+ } catch (error) { setMessage(error?.message || 'Service account could not be created.', 'bad') }
90
+ }
91
+ async function createToken() {
92
+ setMessage('Creating one-time token…', '')
93
+ try {
94
+ await stepUp()
95
+ const expiresAt = tokenExpiry() ? new Date(tokenExpiry() + 'T23:59:59Z').toISOString() : undefined
96
+ const response = await fetch('/api/automation/tokens', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ serviceAccountId: selectedAccount(), name: tokenName(), capabilities: tokenCapabilities(), scopeType: tokenScopeType(), scopeId: tokenScopeId(), expiresAt }) })
97
+ const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || 'API token could not be created.')
98
+ reveal.set(data); password.set(''); mfaCode.set(''); setMessage('Token created. Copy it now; only the hash remains after this page changes.', 'ok'); load()
99
+ } catch (error) { setMessage(error?.message || 'API token could not be created.', 'bad') }
100
+ }
101
+ async function tokenAction(token, action) {
102
+ setMessage(action === 'rotate' ? 'Creating overlapping replacement…' : 'Revoking token…', '')
103
+ try {
104
+ await stepUp()
105
+ const response = await fetch(action === 'rotate' ? '/api/automation/tokens/rotate' : '/api/automation/tokens', { method: action === 'rotate' ? 'POST' : 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: token.id }) })
106
+ const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || 'Token could not be updated.')
107
+ if (action === 'rotate') reveal.set(data)
108
+ password.set(''); mfaCode.set(''); setMessage(action === 'rotate' ? 'Replacement created. Update clients, then revoke the old token.' : 'Token revoked immediately.', 'ok'); load()
109
+ } catch (error) { setMessage(error?.message || 'Token could not be updated.', 'bad') }
110
+ }
111
+ async function disableAccount(item) {
112
+ try {
113
+ await stepUp()
114
+ const response = await fetch('/api/automation/service-accounts', { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: item.account.id }) })
115
+ const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || 'Service account could not be disabled.')
116
+ password.set(''); mfaCode.set(''); setMessage('Service account, membership, and every token were disabled immediately.', 'ok'); load()
117
+ } catch (error) { setMessage(error?.message || 'Service account could not be disabled.', 'bad') }
118
+ }
119
+ function curlExample() {
120
+ if (!reveal()) return ''
121
+ return `curl -H "Authorization: Bearer ${reveal().secret}" ${location.origin}/api/v1/projects`
122
+ }
123
+ function cliExample() {
124
+ if (!reveal()) return ''
125
+ return `TS_CLOUD_API_URL=${location.origin} TS_CLOUD_API_TOKEN=${reveal().secret} cloud api:projects`
126
+ }
127
+ async function copy(value) { await navigator.clipboard.writeText(value); setMessage('Copied to clipboard.', 'ok') }
128
+
129
+ onMount(() => { load() })
130
+ </script>
131
+ <!DOCTYPE html>
132
+ <html lang="en" data-theme="dark">
133
+ <head>@include('../partials/head', { title: 'ts-cloud - API & automation' })</head>
134
+ <body>
135
+ @include('../partials/nav', { active: 'automation', name: server.name, env: 'production' })
136
+ <div class="wrap">
137
+ <div class="crumbs"><a href="/">Dashboard</a> <span class="sep">/</span> Organization <span class="sep">/</span> API &amp; automation</div>
138
+ <header class="page"><div><h1>API &amp; automation</h1><p>Create least-privilege machine identities for CI. Tokens never create browser sessions and cannot access outside their membership and token scope.</p></div><a class="btn ghost" href="/api/v1/openapi.json">OpenAPI 3.1</a></header>
139
+ <div class="automation-message msg" :class="messageKind()" role="status" aria-live="polite">{{ message() }}</div>
140
+
141
+ <div class="panel reveal" @show="!!reveal()">
142
+ <div><strong>Copy this token now</strong><span>The raw secret is shown once. Rotation keeps the old token active until you explicitly revoke it.</span></div>
143
+ <code>{{ reveal()?.secret }}</code><button class="btn sm" type="button" @click="copy(reveal()?.secret)">Copy token</button>
144
+ <label>curl</label><code>{{ curlExample() }}</code><button class="btn ghost sm" type="button" @click="copy(curlExample())">Copy curl</button>
145
+ <label>CLI</label><code>{{ cliExample() }}</code><button class="btn ghost sm" type="button" @click="copy(cliExample())">Copy CLI</button>
146
+ <button class="btn danger sm" type="button" @click="reveal.set(null)">I saved it; hide secret</button>
147
+ </div>
148
+
149
+ <div class="section"><h2>Service accounts</h2><div class="accounts"><template :for="item in accounts()"><article class="account panel" :class="selectedAccount() === item.account.id ? 'selected' : ''">
150
+ <button class="account-main" type="button" @click="selectAccount(item)"><span><strong>{{ item.account.name }}</strong><small>{{ item.account.slug }} · {{ item.membership?.roleTemplate }} · {{ scopeLabel(item.membership?.scope) }}</small><small>Created {{ new Date(item.account.createdAt).toLocaleDateString() }} by {{ item.createdBy?.displayName || 'offline administrator' }}</small></span><span class="tag">{{ item.account.state }}</span></button>
151
+ <div class="token-list"><template :for="token in item.tokens"><div class="token"><div><strong>{{ token.name }}</strong><code>{{ token.prefix }}</code><small>{{ tokenState(token) }} · created {{ new Date(token.createdAt).toLocaleDateString() }} by {{ token.createdBy?.displayName || 'offline administrator' }} · expires {{ new Date(token.expiresAt).toLocaleDateString() }} · last used {{ token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleString() : 'never' }}</small><small>{{ token.capabilities.join(', ') }} · {{ scopeLabel(token.scope) }}</small></div><div class="table-actions"><button class="btn ghost sm" type="button" @show="token.state === 'active'" @click="tokenAction(token, 'rotate')">Rotate</button><button class="btn danger sm" type="button" @show="token.state === 'active'" @click="tokenAction(token, 'revoke')">Revoke</button></div></div></template><div class="compact empty" @show="item.tokens.length === 0"><span>No tokens yet.</span></div></div>
152
+ <button class="btn danger sm" type="button" @show="item.account.state === 'active'" @click="disableAccount(item)">Disable account and tokens</button>
153
+ </article></template><div class="compact empty panel" @show="accounts().length === 0"><strong>No service accounts</strong><span>Create one for a CI system or external automation.</span></div></div></div>
154
+
155
+ <div class="section"><h2>Create service account</h2><div class="panel"><form class="form-grid" @submit.prevent="createAccount()">
156
+ <div class="field-block"><label for="account-name">Name</label><input id="account-name" :value="accountName()" @input="accountName.set($event.target.value)" placeholder="Production CI" required></div>
157
+ <div class="field-block"><label for="account-slug">Slug</label><input id="account-slug" :value="accountSlug()" @input="accountSlug.set($event.target.value)" pattern="[a-z0-9][a-z0-9-]{1,47}" placeholder="production-ci" required></div>
158
+ <div class="field-block"><label for="account-role">Maximum role</label><select id="account-role" :value="accountRole()" @change="accountRole.set($event.target.value)"><option value="viewer">Viewer</option><option value="auditor">Auditor</option><option value="deployer">Deployer</option><option value="operator">Operator</option><option value="admin">Admin</option></select><small>Service accounts can never be owners.</small></div>
159
+ <div class="field-block"><label for="account-scope">Maximum scope</label><select id="account-scope" :value="accountScopeType()" @change="accountScopeType.set($event.target.value); setScope($event.target.value, accountScopeId)"><option value="organization">Organization</option><option value="project">Current project</option><option value="environment">One environment</option><option value="resource">One service</option></select></div>
160
+ <div class="field-block" @show="accountScopeType() === 'environment'"><label for="account-environment">Environment</label><select id="account-environment" :value="accountScopeId()" @change="accountScopeId.set($event.target.value)"><template :for="environment in environments()"><option :value="environment.id">{{ environment.name }}</option></template></select></div>
161
+ <div class="field-block" @show="accountScopeType() === 'resource'"><label for="account-resource">Service</label><select id="account-resource" :value="accountScopeId()" @change="accountScopeId.set($event.target.value)"><template :for="resource in resources()"><option :value="resource.id">{{ resource.name }}</option></template></select></div>
162
+ <div class="form-actions"><button class="btn" type="submit">Create service account</button></div>
163
+ </form></div></div>
164
+
165
+ <div class="section"><h2>Create API token</h2><div class="panel"><form class="form-grid" @submit.prevent="createToken()">
166
+ <div class="field-block"><label for="token-account">Service account</label><select id="token-account" :value="selectedAccount()" @change="selectAccount(accounts().find(item => item.account.id === $event.target.value))" required><template :for="item in accounts()"><option :value="item.account.id" :disabled="item.account.state !== 'active'">{{ item.account.name }}</option></template></select></div>
167
+ <div class="field-block"><label for="token-name">Token name</label><input id="token-name" :value="tokenName()" @input="tokenName.set($event.target.value)" required></div>
168
+ <div class="field-block"><label for="token-expiry">Expiry</label><input id="token-expiry" type="date" :value="tokenExpiry()" @input="tokenExpiry.set($event.target.value)"><small>Defaults to 90 days; maximum one year.</small></div>
169
+ <div class="field-block"><label for="token-scope">Token scope</label><select id="token-scope" :value="tokenScopeType()" @change="tokenScopeType.set($event.target.value); setScope($event.target.value, tokenScopeId)"><option value="organization">Organization</option><option value="project">Current project</option><option value="environment">One environment</option><option value="resource">One service</option></select><small>Must be equal to or narrower than the service-account scope.</small></div>
170
+ <div class="field-block" @show="tokenScopeType() === 'environment'"><label>Environment</label><select :value="tokenScopeId()" @change="tokenScopeId.set($event.target.value)"><template :for="environment in environments()"><option :value="environment.id">{{ environment.name }}</option></template></select></div>
171
+ <div class="field-block" @show="tokenScopeType() === 'resource'"><label>Service</label><select :value="tokenScopeId()" @change="tokenScopeId.set($event.target.value)"><template :for="resource in resources()"><option :value="resource.id">{{ resource.name }}</option></template></select></div>
172
+ <fieldset class="capabilities"><legend>Explicit capabilities</legend><template :for="capability in capabilities"><label class="capability"><input type="checkbox" :checked="tokenCapabilities().includes(capability[0])" @change="toggleCapability(capability[0])"><span><b>{{ capability[0] }}</b><small>{{ capability[1] }}</small></span></label></template></fieldset>
173
+ <div class="form-actions"><button class="btn" type="submit" :disabled="!selectedAccount() || tokenCapabilities().length === 0">Create and reveal token</button></div>
174
+ </form></div></div>
175
+
176
+ <div class="section"><h2>Confirm sensitive changes</h2><div class="auth-grid panel"><div class="field-block"><label for="automation-password">Your password</label><input id="automation-password" type="password" autocomplete="current-password" :value="password()" @input="password.set($event.target.value)" required></div><div class="field-block"><label for="automation-code">Authenticator or recovery code</label><input id="automation-code" autocomplete="one-time-code" :value="mfaCode()" @input="mfaCode.set($event.target.value)"><small>Required when MFA is enabled. Used only for recent-auth confirmation.</small></div></div></div>
177
+ </div>
178
+ <style>
179
+ .automation-message{min-height:20px;margin-top:12px}.accounts{display:grid;gap:12px}.account{display:grid;gap:14px}.account.selected{border-color:var(--accent)}
180
+ .account-main{display:flex;justify-content:space-between;gap:12px;width:100%;text-align:left;background:none;border:0;color:inherit;cursor:pointer}.account-main span:first-child{display:grid;gap:4px}.account-main small,.token small{color:var(--txt3);font-size:12px;line-height:1.5}
181
+ .token-list{display:grid;gap:10px}.token{display:flex;justify-content:space-between;gap:14px;padding:12px;border:1px solid var(--panel-br);border-radius:10px}.token>div:first-child{display:grid;gap:4px;min-width:0}.token code{color:var(--txt2)}
182
+ .reveal{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;margin-top:18px;border-color:rgba(53,209,155,.35)}.reveal>div,.reveal>code,.reveal>label{grid-column:1/-1}.reveal>div{display:grid;gap:4px}.reveal span{color:var(--txt2);font-size:12px}.reveal code{padding:10px;background:rgba(0,0,0,.25);border-radius:8px;overflow-wrap:anywhere;user-select:all}
183
+ .capabilities{grid-column:1/-1;border:1px solid var(--panel-br);border-radius:10px;padding:12px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.capabilities legend{padding:0 6px;color:var(--txt3);font-size:11px;font-weight:800;text-transform:uppercase}.capability{display:flex;gap:9px;align-items:flex-start;padding:9px}.capability input{width:auto;margin-top:3px}.capability span{display:grid;gap:2px}.capability small{color:var(--txt3);font-size:11px}.auth-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
184
+ @media(max-width:700px){.token{display:grid}.reveal,.capabilities,.auth-grid{grid-template-columns:1fr}.reveal>*{grid-column:1}}
185
+ </style>
186
+ </body>
187
+ </html>
@@ -0,0 +1,249 @@
1
+ <script server>
2
+ let __d = null
3
+ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
4
+ const project = __d?.project ?? { name: 'Project' }
5
+ </script>
6
+ <!DOCTYPE html>
7
+ <html lang="en" data-theme="dark">
8
+ <head>
9
+ @include('../partials/head', { title: 'ts-cloud - Sign-in & security' })
10
+ <style>
11
+ .security-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, .7fr); gap: 18px; align-items: start; }
12
+ .security-stack { display: grid; gap: 18px; }
13
+ .security-summary { display: grid; gap: 10px; }
14
+ .security-summary .kv { padding: 8px 0; }
15
+ .session-list { display: grid; gap: 10px; }
16
+ .session-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 13px; border: 1px solid var(--panel-br); border-radius: 11px; }
17
+ .session-copy { display: grid; gap: 4px; min-width: 0; }
18
+ .session-copy strong, .session-copy span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
19
+ .session-copy span { color: var(--txt3); font-size: 11px; }
20
+ .security-form { display: grid; gap: 13px; }
21
+ .security-form label { display: grid; gap: 6px; color: var(--txt3); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; }
22
+ .security-form input { width: 100%; }
23
+ .security-status { min-height: 20px; color: var(--txt2); font-size: 12px; }
24
+ .mfa-secret { display: block; margin: 9px 0; padding: 10px; overflow-wrap: anywhere; border: 1px solid var(--panel-br); border-radius: 9px; background: rgba(255,255,255,.025); color: var(--accent); font: 700 12px var(--mono); }
25
+ .recovery-codes { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 10px; }
26
+ .recovery-codes code { padding: 7px; border: 1px solid var(--panel-br); border-radius: 7px; text-align: center; }
27
+ .hidden { display: none !important; }
28
+ @media (max-width: 900px) { .security-layout { grid-template-columns: 1fr; } }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ @include('../partials/nav', { active: 'account-security', name: project.name, env: 'production' })
33
+ <main class="wrap" id="main-content">
34
+ <div class="crumbs"><a href="/">Account</a> <span class="sep">/</span> Sign-in & security</div>
35
+ <header class="page">
36
+ <div><h1>Sign-in & security</h1><p>Manage your password and review every browser session with access to this account.</p></div>
37
+ </header>
38
+
39
+ <div class="security-layout">
40
+ <div class="security-stack">
41
+ <section class="panel" aria-labelledby="sessions-title">
42
+ <div class="section-heading"><div><h2 id="sessions-title">Active sessions</h2><p class="note">Last-used times and device details help you spot unfamiliar access.</p></div><button class="btn ghost sm" id="revoke-others" type="button">Sign out other sessions</button></div>
43
+ <div class="session-list" id="session-list" aria-live="polite"><div class="compact empty"><strong>Loading sessions…</strong></div></div>
44
+ </section>
45
+ </div>
46
+
47
+ <div class="security-stack">
48
+ <section class="panel security-summary" aria-labelledby="identity-title">
49
+ <h2 id="identity-title">Identity</h2>
50
+ <div class="kv"><span>Username</span><b id="identity-username">—</b></div>
51
+ <div class="kv"><span>Email</span><b id="identity-email">Not configured</b></div>
52
+ <div class="kv"><span>Last sign-in</span><b id="identity-login">—</b></div>
53
+ <div class="kv"><span>Multi-factor authentication</span><b id="identity-mfa">Loading…</b></div>
54
+ <details>
55
+ <summary>Confirm identity for sensitive changes</summary>
56
+ <form class="security-form" id="step-up-form">
57
+ <label>Current password<input name="password" type="password" autocomplete="current-password" required></label>
58
+ <label>Authenticator or recovery code (when enabled)<input name="code" autocomplete="one-time-code"></label>
59
+ <button class="btn ghost" type="submit">Confirm identity</button>
60
+ <div class="security-status" id="step-up-status" role="status"></div>
61
+ </form>
62
+ </details>
63
+ </section>
64
+
65
+ <section class="panel" aria-labelledby="mfa-title">
66
+ <h2 id="mfa-title">Authenticator app</h2>
67
+ <p class="note" id="mfa-copy">Add a time-based code as a second sign-in factor.</p>
68
+ <form class="security-form" id="mfa-enroll-form">
69
+ <label>Current password<input name="password" type="password" autocomplete="current-password" required></label>
70
+ <button class="btn" type="submit">Set up authenticator</button>
71
+ </form>
72
+ <form class="hidden security-form" id="mfa-verify-form">
73
+ <p class="note">Add this secret or setup URI to your authenticator app, then enter its six-digit code.</p>
74
+ <code class="mfa-secret" id="mfa-secret"></code>
75
+ <details><summary>Show full setup URI</summary><code class="mfa-secret" id="mfa-uri"></code></details>
76
+ <label>Six-digit code<input name="code" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" required></label>
77
+ <button class="btn" type="submit">Verify and enable</button>
78
+ </form>
79
+ <form class="hidden security-form" id="mfa-disable-form">
80
+ <label>Current password<input name="password" type="password" autocomplete="current-password" required></label>
81
+ <label>Authenticator or recovery code<input name="code" autocomplete="one-time-code" required></label>
82
+ <button class="btn danger" type="submit">Disable MFA</button>
83
+ </form>
84
+ <div class="hidden recovery-codes" id="recovery-codes" aria-label="Recovery codes"></div>
85
+ <div class="security-status" id="mfa-status" role="status"></div>
86
+ </section>
87
+
88
+ <section class="panel" aria-labelledby="password-title">
89
+ <h2 id="password-title">Change password</h2>
90
+ <p class="note">Changing your password rotates this session and immediately signs out every other session.</p>
91
+ <form class="security-form" id="password-form">
92
+ <label>Current password<input name="currentPassword" type="password" autocomplete="current-password" required></label>
93
+ <label>New password<input name="password" type="password" autocomplete="new-password" minlength="12" required></label>
94
+ <label>Confirm new password<input name="confirmation" type="password" autocomplete="new-password" minlength="12" required></label>
95
+ <button class="btn" type="submit">Change password</button>
96
+ <div class="security-status" id="password-status" role="status"></div>
97
+ </form>
98
+ </section>
99
+ </div>
100
+ </div>
101
+ </main>
102
+
103
+ <script>
104
+ (() => {
105
+ const list = document.querySelector('#session-list')
106
+ const status = document.querySelector('#password-status')
107
+ const mfaStatus = document.querySelector('#mfa-status')
108
+ let currentSessionId = ''
109
+
110
+ const when = value => value ? new Date(value).toLocaleString() : 'Never'
111
+ const request = async (path, options) => {
112
+ const response = await fetch(path, options)
113
+ const result = await response.json().catch(() => ({}))
114
+ if (!response.ok) throw new Error(result.error || 'The request failed.')
115
+ return result
116
+ }
117
+ const renderSessions = sessions => {
118
+ list.replaceChildren()
119
+ const active = sessions.filter(session => session.state === 'active')
120
+ if (!active.length) {
121
+ list.innerHTML = '<div class="compact empty"><strong>No active sessions</strong></div>'
122
+ return
123
+ }
124
+ for (const session of active) {
125
+ const row = document.createElement('div')
126
+ row.className = 'session-row'
127
+ const copy = document.createElement('div')
128
+ copy.className = 'session-copy'
129
+ const title = document.createElement('strong')
130
+ title.textContent = `${session.userAgent || 'Unknown browser'}${session.id === currentSessionId ? ' · This session' : ''}`
131
+ const meta = document.createElement('span')
132
+ meta.textContent = `Last used ${when(session.lastUsedAt)} · Network ${session.networkHint || 'unknown'} · ${session.authMethod}`
133
+ copy.append(title, meta)
134
+ const button = document.createElement('button')
135
+ button.className = 'btn ghost sm'
136
+ button.type = 'button'
137
+ button.textContent = session.id === currentSessionId ? 'Sign out' : 'Revoke'
138
+ button.addEventListener('click', async () => {
139
+ button.disabled = true
140
+ const result = await request('/api/auth/sessions', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: session.id }) }).catch(error => ({ error }))
141
+ if (result.error) { button.disabled = false; button.textContent = result.error.message; return }
142
+ if (result.signedOut) { location.assign('/login'); return }
143
+ await load()
144
+ })
145
+ row.append(copy, button)
146
+ list.append(row)
147
+ }
148
+ }
149
+ const load = async () => {
150
+ const data = await request('/api/auth/security')
151
+ currentSessionId = data.currentSessionId || ''
152
+ document.querySelector('#identity-username').textContent = data.identity?.username || '—'
153
+ document.querySelector('#identity-email').textContent = data.identity?.email ? `${data.identity.email}${data.identity.emailVerifiedAt ? ' · Verified' : ' · Unverified'}` : 'Not configured'
154
+ document.querySelector('#identity-login').textContent = when(data.identity?.lastLoginAt)
155
+ const mfaActive = data.mfa?.factor?.state === 'active'
156
+ document.querySelector('#identity-mfa').textContent = mfaActive ? `Enabled · ${data.mfa.recoveryCodesRemaining} recovery codes left` : 'Not enabled'
157
+ document.querySelector('#mfa-enroll-form').classList.toggle('hidden', mfaActive)
158
+ document.querySelector('#mfa-disable-form').classList.toggle('hidden', !mfaActive)
159
+ renderSessions(data.sessions || [])
160
+ }
161
+ document.querySelector('#revoke-others').addEventListener('click', async event => {
162
+ event.currentTarget.disabled = true
163
+ await request('/api/auth/sessions/revoke-others', { method: 'POST' }).catch(() => undefined)
164
+ event.currentTarget.disabled = false
165
+ await load()
166
+ })
167
+ document.querySelector('#password-form').addEventListener('submit', async event => {
168
+ event.preventDefault()
169
+ const form = event.currentTarget
170
+ const data = new FormData(form)
171
+ if (data.get('password') !== data.get('confirmation')) { status.textContent = 'The new passwords do not match.'; return }
172
+ status.textContent = 'Changing password…'
173
+ try {
174
+ const result = await request('/api/auth/password/change', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ currentPassword: data.get('currentPassword'), password: data.get('password') }) })
175
+ status.textContent = result.message
176
+ form.reset()
177
+ await load()
178
+ }
179
+ catch (error) { status.textContent = error.message }
180
+ })
181
+ document.querySelector('#step-up-form').addEventListener('submit', async event => {
182
+ event.preventDefault()
183
+ const form = event.currentTarget
184
+ const data = new FormData(form)
185
+ const stepStatus = document.querySelector('#step-up-status')
186
+ try {
187
+ await request('/api/auth/step-up', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ password: data.get('password'), code: data.get('code') }) })
188
+ form.reset()
189
+ stepStatus.textContent = 'Identity confirmed for the next 10 minutes.'
190
+ }
191
+ catch (error) { stepStatus.textContent = error.message }
192
+ })
193
+ document.querySelector('#mfa-enroll-form').addEventListener('submit', async event => {
194
+ event.preventDefault()
195
+ const form = event.currentTarget
196
+ const data = new FormData(form)
197
+ mfaStatus.textContent = 'Starting enrollment…'
198
+ try {
199
+ const result = await request('/api/auth/mfa/enroll', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ password: data.get('password') }) })
200
+ document.querySelector('#mfa-secret').textContent = result.secret
201
+ document.querySelector('#mfa-uri').textContent = result.uri
202
+ form.classList.add('hidden')
203
+ document.querySelector('#mfa-verify-form').classList.remove('hidden')
204
+ mfaStatus.textContent = 'Enter the code from your authenticator app.'
205
+ }
206
+ catch (error) { mfaStatus.textContent = error.message }
207
+ })
208
+ document.querySelector('#mfa-verify-form').addEventListener('submit', async event => {
209
+ event.preventDefault()
210
+ const form = event.currentTarget
211
+ const data = new FormData(form)
212
+ try {
213
+ const result = await request('/api/auth/mfa/verify', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ code: data.get('code') }) })
214
+ const recovery = document.querySelector('#recovery-codes')
215
+ recovery.replaceChildren()
216
+ for (const code of result.recoveryCodes) { const item = document.createElement('code'); item.textContent = code; recovery.append(item) }
217
+ recovery.classList.remove('hidden')
218
+ form.classList.add('hidden')
219
+ mfaStatus.textContent = 'MFA enabled. Save these recovery codes now; they are shown once.'
220
+ await load()
221
+ }
222
+ catch (error) { mfaStatus.textContent = error.message }
223
+ })
224
+ document.querySelector('#mfa-disable-form').addEventListener('submit', async event => {
225
+ event.preventDefault()
226
+ const form = event.currentTarget
227
+ const data = new FormData(form)
228
+ try {
229
+ await request('/api/auth/mfa', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ password: data.get('password'), code: data.get('code') }) })
230
+ form.reset()
231
+ document.querySelector('#recovery-codes').classList.add('hidden')
232
+ mfaStatus.textContent = 'MFA disabled and other sessions signed out.'
233
+ await load()
234
+ }
235
+ catch (error) { mfaStatus.textContent = error.message }
236
+ })
237
+ load().catch(error => {
238
+ list.replaceChildren()
239
+ const empty = document.createElement('div')
240
+ empty.className = 'compact empty'
241
+ const message = document.createElement('strong')
242
+ message.textContent = error.message
243
+ empty.append(message)
244
+ list.append(empty)
245
+ })
246
+ })()
247
+ </script>
248
+ </body>
249
+ </html>
@@ -0,0 +1,38 @@
1
+ <script server>
2
+ let __c = null
3
+ try { __c = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __c = null }
4
+ const capabilities = __c?.viewer?.capabilities ?? []
5
+ const canManage = capabilities.includes('applications:manage')
6
+ const canDeploy = capabilities.includes('deployments:create')
7
+ const canStop = capabilities.includes('deployments:cancel')
8
+ const canLogs = capabilities.includes('runtime:logs')
9
+ const canShell = capabilities.includes('runtime:terminal')
10
+ </script>
11
+ <script client>
12
+ const data = state({ applications: [], templates: [], environments: [] })
13
+ const message = state('')
14
+ const diagnostics = state([])
15
+ const source = state('services:\n web:\n image: acme/web:1.0.0\n ports: [3000]\n')
16
+ const selected = state('')
17
+ const deleteState = state({ id: '', confirm: '', removeVolumes: false })
18
+ const output = state({ service: '', text: '' })
19
+ async function post(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 }
20
+ async function refresh(){try{const response=await fetch('/api/compose');const result=await response.json();if(!response.ok)throw new Error(result.error||'Could not load Compose applications.');data.set(result)}catch(error){message.set(error.message)}}
21
+ function environmentId(){return data().environments.find(value=>value.slug===(new URLSearchParams(location.search).get('env')||'production'))?.id||data().environments[0]?.id}
22
+ function tone(status){return ['running','ready'].includes(status)?'ok':['failed','degraded'].includes(status)?'bad':'warn'}
23
+ async function preview(event){event.preventDefault();const form=event.currentTarget.closest('form');try{const result=await post('/api/compose/preview',{name:form.name.value,environmentId:form.environmentId.value,source:source()});diagnostics.set(result.result.diagnostics);message.set(result.result.valid?'Compose definition is deployable.':'Resolve blocking diagnostics before import.')}catch(error){message.set(error.message)}}
24
+ async function importStack(event){event.preventDefault();const form=event.currentTarget;try{const result=await post('/api/compose/import',{name:form.name.value,environmentId:form.environmentId.value,source:source()});diagnostics.set(result.diagnostics);message.set('Compose application imported.');await refresh()}catch(error){message.set(error.message)}}
25
+ async function useTemplate(template){const domain=prompt('Primary domain for '+template.name);if(!domain)return;try{await post('/api/compose/template',{templateId:template.id,templateVersion:template.version,name:template.name,environmentId:environmentId(),inputs:{domain}});message.set('Template created as an editable manifest.');await refresh()}catch(error){message.set(error.message)}}
26
+ async function action(item,name,extra={}){try{await post('/api/compose/action',{id:item.id,action:name,...extra});message.set(name+' queued for '+item.name+'.');deleteState.set({id:'',confirm:'',removeVolumes:false});await refresh()}catch(error){message.set(error.message)}}
27
+ async function logs(item,service){try{const result=await post('/api/compose/logs',{id:item.id,service,lines:200});output.set({service:item.name+' / '+service,text:result.stdout||result.stderr||'No log output.'})}catch(error){message.set(error.message)}}
28
+ async function shell(item,service){const command=prompt('Command arguments (space separated)','sh');if(!command)return;const confirm=prompt('Type '+service+' to run this command');try{const result=await post('/api/compose/shell',{id:item.id,service,command:command.split(/\s+/).filter(Boolean),confirm});output.set({service:item.name+' / '+service,text:result.stdout||result.stderr||'Command completed.'})}catch(error){message.set(error.message)}}
29
+ onMount(()=>{refresh();const timer=setInterval(refresh,5000);return()=>clearInterval(timer)})
30
+ </script>
31
+ <!DOCTYPE html><html lang="en" data-theme="dark"><head>@include('../partials/head',{title:'ts-cloud - Compose applications'})</head><body>
32
+ @include('../partials/nav',{active:'compose-applications',env:'production'})
33
+ <main class="compose-wrap wrap" id="main-content"><div class="crumbs">Applications <span class="sep">/</span> Compose</div><header class="page"><div><h1>Compose applications</h1><p>Inspectable multi-service topology, pinned templates, health-aware deploys, and explicit data ownership.</p></div><button class="btn ghost" @click="refresh()">Refresh</button></header><p class="notice" @show="!!message()">{{message()}}</p>
34
+ <section class="section stats"><div class="panel"><small>Applications</small><strong>{{data().applications.length}}</strong></div><div class="panel"><small>Running</small><strong>{{data().applications.filter(v=>v.status==='running').length}}</strong></div><div class="panel"><small>Services</small><strong>{{data().applications.reduce((n,v)=>n+Object.keys(v.manifest.spec.services).length,0)}}</strong></div><div class="panel"><small>Unhealthy</small><strong>{{data().applications.flatMap(v=>v.services).filter(v=>['unhealthy','failed'].includes(v.status)).length}}</strong></div></section>
35
+ <section class="section"><div class="section-heading"><div><h2>Stacks and topology</h2><p class="note">Dependencies deploy in the shown order; health gates determine readiness.</p></div></div><div class="empty panel" @show="!data().applications.length"><b>No Compose applications</b><span>Import a validated file or start from a pinned template.</span></div><div class="stack-grid"><article class="panel stack" @each="item in data().applications" :key="item.id"><div class="stack-head"><div><h3>{{item.name}}</h3><p class="mono note">{{item.slug}} · v{{item.version}}</p></div><span class="pill" :class="tone(item.status)">{{item.status}}</span></div><div class="topology"><span @each="name in item.manifest.spec.dependencyOrder">{{name}}</span></div><div class="services"><div class="service" @each="service in item.services" :key="service.serviceName"><div><b>{{service.serviceName}}</b><small>{{service.status}} · {{service.healthyReplicas}}/{{service.replicas}} healthy</small></div><div class="actions">@if(canLogs)<button class="btn ghost xs" @click="logs(item,service.serviceName)">Logs</button>@endif @if(canShell)<button class="btn ghost xs" @click="shell(item,service.serviceName)">Shell</button>@endif @if(canDeploy)<button class="btn ghost xs" @click="action(item,'scale',{service:service.serviceName,replicas:Number(prompt('Replicas','1'))})">Scale</button>@endif</div></div></div><div class="domains"><a @each="service in Object.values(item.manifest.spec.services).filter(v=>v.domains.length)" :href="'https://'+service.domains[0]" target="_blank">{{service.name}} · {{service.domains.join(', ')}}</a></div><div class="actions">@if(canDeploy)<button class="btn sm" @click="action(item,item.status==='running'?'redeploy':'deploy')">{{item.status==='running'?'Redeploy':'Deploy'}}</button><button class="btn ghost sm" @click="action(item,'start')">Start</button>@endif @if(canStop)<button class="btn ghost sm" @click="action(item,'stop')">Stop</button><button class="btn danger sm" @click="deleteState.set({id:item.id,confirm:'',removeVolumes:false})">Delete…</button>@endif <a class="btn ghost sm" href="/operations/queue">Operations</a></div><div class="delete" @show="deleteState().id===item.id"><label class="check"><input type="checkbox" :checked="deleteState().removeVolumes" @change="deleteState.set({...deleteState(),removeVolumes:$event.target.checked,confirm:''})"> Permanently delete named volumes</label><p>Type <b>{{deleteState().removeVolumes?item.slug+' delete volumes':item.slug}}</b></p><input :value="deleteState().confirm" @input="deleteState.set({...deleteState(),confirm:$event.target.value})"><button class="btn danger sm" @click="action(item,'delete',{removeVolumes:deleteState().removeVolumes,confirm:deleteState().confirm})">Queue deletion</button></div></article></div></section>
36
+ @if(canManage)<section class="authoring section"><form class="panel" @submit="importStack($event)"><h2>Import Compose</h2><label>Name<input name="name" required value="Compose application"></label><label>Environment<select name="environmentId"><option @each="env in data().environments" :value="env.id">{{env.name}}</option></select></label><label>Compose YAML<textarea rows="18" :value="source()" @input="source.set($event.target.value)"></textarea></label><div class="actions"><button class="btn ghost" type="button" @click="preview($event)">Preview diagnostics</button><button class="btn" type="submit">Import editable manifest</button></div></form><div><h2>Verified templates</h2><div class="template-grid"><article class="panel template" @each="template in data().templates"><div><span class="pill">{{template.category}}</span><h3>{{template.name}}</h3><p>{{template.description}}</p></div><dl><div><dt>Version / source</dt><dd>{{template.version}} · {{template.source}}</dd></div><div><dt>Architecture</dt><dd>{{template.architecture}}</dd></div><div><dt>Minimum</dt><dd>{{template.minimumResources.cpu}} CPU · {{template.minimumResources.memoryMb}} MB</dd></div><div><dt>Verified</dt><dd>{{new Date(template.lastVerifiedAt).toLocaleDateString()}}</dd></div></dl><p class="note">{{template.maintenanceNotes}}</p><button class="btn sm" @click="useTemplate(template)">Use pinned template</button></article></div></div></section>@endif
37
+ <section class="diagnostics panel section" @show="diagnostics().length"><h2>Conversion diagnostics</h2><div @each="issue in diagnostics()" class="diagnostic" :class="issue.severity"><span>{{issue.severity}}</span><code>{{issue.path}}</code><b>{{issue.message}}</b><small>{{issue.alternative||''}}</small></div></section><section class="panel section" @show="!!output().text"><div class="section-heading"><h2>{{output().service}}</h2><button class="btn ghost xs" @click="output.set({service:'',text:''})">Close</button></div><pre>{{output().text}}</pre></section>
38
+ </main><style>.compose-wrap{max-width:1480px}.page,.stack-head,.actions,.section-heading{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}.stack-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.stack{display:grid;gap:14px}.stack h3,.template h3{margin:0}.topology{display:flex;flex-wrap:wrap;gap:6px}.topology span{background:var(--bg3);padding:6px 9px;border-radius:20px;font-size:11px}.topology span+span:before{content:'→';margin-right:7px;color:var(--txt3)}.services{display:grid;gap:7px}.service{display:flex;justify-content:space-between;gap:8px;align-items:center;border:1px solid var(--line);border-radius:8px;padding:9px}.service small{display:block;color:var(--txt3);margin-top:3px}.actions{justify-content:flex-start;flex-wrap:wrap}.delete{display:grid;gap:8px;border:1px solid rgba(235,90,90,.4);padding:12px;border-radius:8px}.delete p{margin:0}.check{display:flex;gap:8px}.authoring{display:grid;grid-template-columns:minmax(360px,.8fr) 1.2fr;gap:15px;align-items:start}.authoring form{display:grid;gap:10px}.authoring label{display:grid;gap:5px}.template-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.template{display:grid;gap:10px}.template dl{display:grid;gap:7px;margin:0}.template dt{font-size:10px;color:var(--txt3)}.template dd{margin:2px 0;font-size:12px}.diagnostic{display:grid;grid-template-columns:70px 220px 1fr;gap:8px;padding:8px;border-top:1px solid var(--line)}.diagnostic.error{color:#ffadad}.diagnostic.warning{color:#ffd58a}.diagnostic small{grid-column:3}pre{max-height:420px;overflow:auto}.empty span{display:block;color:var(--txt3);margin-top:5px}@media(max-width:1000px){.stack-grid,.authoring{grid-template-columns:1fr}}@media(max-width:650px){.stats,.template-grid{grid-template-columns:1fr 1fr}.service{align-items:flex-start;display:grid}.diagnostic{grid-template-columns:1fr}.diagnostic small{grid-column:1}}@media(max-width:420px){.stats,.template-grid{grid-template-columns:1fr}}</style></body></html>