@stacksjs/ts-cloud 0.7.55 → 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.
- package/dist/alerts/alerts.test.d.ts +1 -0
- package/dist/alerts/delivery.d.ts +45 -0
- package/dist/alerts/evaluator.d.ts +12 -0
- package/dist/alerts/health.d.ts +34 -0
- package/dist/alerts/index.d.ts +6 -0
- package/dist/alerts/model.d.ts +170 -0
- package/dist/alerts/store.d.ts +90 -0
- package/dist/alerts/telemetry.d.ts +3 -0
- package/dist/api/client.d.ts +197 -0
- package/dist/api/handler.d.ts +18 -0
- package/dist/api/handler.test.d.ts +1 -0
- package/dist/api/index.d.ts +6 -0
- package/dist/api/openapi.d.ts +2 -0
- package/dist/api/service.d.ts +28 -0
- package/dist/api/types.d.ts +119 -0
- package/dist/auth/encryption.d.ts +3 -0
- package/dist/auth/index.d.ts +8 -0
- package/dist/auth/mailer.d.ts +9 -0
- package/dist/auth/oidc.d.ts +36 -0
- package/dist/auth/oidc.test.d.ts +1 -0
- package/dist/auth/store.d.ts +121 -0
- package/dist/auth/store.test.d.ts +1 -0
- package/dist/auth/totp.d.ts +11 -0
- package/dist/auth/totp.test.d.ts +1 -0
- package/dist/auth/types.d.ts +138 -0
- package/dist/automation/index.d.ts +2 -0
- package/dist/automation/store.d.ts +46 -0
- package/dist/automation/store.test.d.ts +1 -0
- package/dist/automation/types.d.ts +66 -0
- package/dist/aws/acm.d.ts +0 -4
- package/dist/aws/application-autoscaling.d.ts +0 -5
- package/dist/aws/backup.d.ts +42 -0
- package/dist/aws/bedrock.d.ts +0 -5
- package/dist/aws/client.d.ts +3 -6
- package/dist/aws/cloudformation.d.ts +0 -4
- package/dist/aws/cloudfront-media.test.d.ts +1 -0
- package/dist/aws/cloudfront-origin.test.d.ts +1 -0
- package/dist/aws/cloudfront.d.ts +87 -3
- package/dist/aws/cloudwatch-logs.d.ts +2 -4
- package/dist/aws/cloudwatch.d.ts +1 -1
- package/dist/aws/comprehend.d.ts +0 -5
- package/dist/aws/connect.d.ts +0 -4
- package/dist/aws/cost-explorer-cache.d.ts +1 -15
- package/dist/aws/cost-explorer.d.ts +18 -9
- package/dist/aws/cost-explorer.test.d.ts +1 -0
- package/dist/aws/dynamodb.d.ts +0 -4
- package/dist/aws/ec2.d.ts +0 -4
- package/dist/aws/ecr.d.ts +0 -4
- package/dist/aws/ecs.d.ts +17 -7
- package/dist/aws/efs.d.ts +0 -4
- package/dist/aws/elasticache.d.ts +0 -4
- package/dist/aws/elbv2.d.ts +0 -7
- package/dist/aws/email.d.ts +0 -10
- package/dist/aws/eventbridge.d.ts +0 -4
- package/dist/aws/iam.d.ts +1 -5
- package/dist/aws/imap-server.d.ts +0 -7
- package/dist/aws/index.d.ts +4 -3
- package/dist/aws/index.js +31 -31
- package/dist/aws/kendra.d.ts +0 -5
- package/dist/aws/lambda.d.ts +3 -5
- package/dist/aws/opensearch.d.ts +0 -4
- package/dist/aws/personalize.d.ts +0 -5
- package/dist/aws/polly.d.ts +0 -5
- package/dist/aws/rds.d.ts +59 -4
- package/dist/aws/rekognition.d.ts +0 -5
- package/dist/aws/route53-domains.d.ts +0 -4
- package/dist/aws/route53.d.ts +0 -4
- package/dist/aws/s3.d.ts +10 -6
- package/dist/aws/scheduler.d.ts +0 -4
- package/dist/aws/secrets-manager.d.ts +0 -4
- package/dist/aws/setup-sms.d.ts +0 -14
- package/dist/aws/sms.d.ts +0 -10
- package/dist/aws/smtp-server.d.ts +0 -11
- package/dist/aws/sns.d.ts +0 -4
- package/dist/aws/sqs.d.ts +0 -4
- package/dist/aws/ssm.d.ts +0 -4
- package/dist/aws/sts.d.ts +0 -4
- package/dist/aws/support.d.ts +0 -4
- package/dist/aws/textract.d.ts +0 -5
- package/dist/aws/transcribe.d.ts +0 -4
- package/dist/aws/translate.d.ts +0 -5
- package/dist/aws/voice.d.ts +0 -11
- package/dist/aws/xray.d.ts +1 -0
- package/dist/backups/aws-database-source.d.ts +19 -0
- package/dist/backups/aws-database-source.test.d.ts +1 -0
- package/dist/backups/aws-infrastructure-source.d.ts +43 -0
- package/dist/backups/aws-infrastructure-source.test.d.ts +1 -0
- package/dist/backups/control-plane-source.d.ts +16 -0
- package/dist/backups/control-plane-source.test.d.ts +1 -0
- package/dist/backups/docker-volume-source.d.ts +31 -0
- package/dist/backups/docker-volume-source.test.d.ts +1 -0
- package/dist/backups/filesystem-source.d.ts +34 -0
- package/dist/backups/filesystem-source.test.d.ts +1 -0
- package/dist/backups/index.d.ts +10 -0
- package/dist/backups/logical-database-source.d.ts +16 -0
- package/dist/backups/logical-database-source.test.d.ts +1 -0
- package/dist/backups/model.d.ts +121 -0
- package/dist/backups/s3-destination.d.ts +85 -0
- package/dist/backups/s3-destination.test.d.ts +1 -0
- package/dist/backups/service.d.ts +80 -0
- package/dist/backups/service.test.d.ts +1 -0
- package/dist/backups/store.d.ts +30 -0
- package/dist/backups/store.test.d.ts +1 -0
- package/dist/bin/cli.js +3360 -1802
- package/dist/{chunk-tjjgajbh.js → chunk-01d86gt1.js} +301 -9
- package/dist/{chunk-73vcqg6z.js → chunk-0z43x8re.js} +7 -7
- package/dist/{chunk-tnztxpcb.js → chunk-32e7ya18.js} +61 -49
- package/dist/{chunk-qpj3edwz.js → chunk-4cjrg98a.js} +3 -7
- package/dist/{chunk-hsk6fe6x.js → chunk-50jpda9q.js} +2 -2
- package/dist/{chunk-93hjhs78.js → chunk-703nkybg.js} +1 -1
- package/dist/{chunk-d5vqwj8s.js → chunk-9m6b4m74.js} +755 -720
- package/dist/chunk-e05phx5f.js +10 -0
- package/dist/{chunk-f29w04w5.js → chunk-exbcsab2.js} +10 -4
- package/dist/{chunk-eq08r166.js → chunk-f55nkpvb.js} +2 -2
- package/dist/{chunk-3bfv1z4v.js → chunk-fv1bj3yt.js} +63 -56
- package/dist/chunk-fw3q88wk.js +48641 -0
- package/dist/{chunk-3knnr7wh.js → chunk-he9a874b.js} +170 -2
- package/dist/{chunk-zn0nxxa8.js → chunk-hpv68b4a.js} +1070 -1070
- package/dist/chunk-k114qwg9.js +12 -0
- package/dist/{chunk-r16e02bc.js → chunk-vdk53yyq.js} +3900 -4101
- package/dist/{chunk-jp2a8yad.js → chunk-vvkd46k1.js} +15 -13
- package/dist/{chunk-vd87cpvn.js → chunk-wj3s95p9.js} +62 -46
- package/dist/{chunk-mj1tmhte.js → chunk-x9s0cqfx.js} +1 -1
- package/dist/{chunk-3rsfns7x.js → chunk-ybcz6sxc.js} +2048 -387
- package/dist/{chunk-arsh1g5h.js → chunk-zqtpg06c.js} +14 -11
- package/dist/compose/index.d.ts +6 -0
- package/dist/compose/parser.d.ts +13 -0
- package/dist/compose/parser.test.d.ts +1 -0
- package/dist/compose/runtime.d.ts +10 -0
- package/dist/compose/service.d.ts +18 -0
- package/dist/compose/store.d.ts +59 -0
- package/dist/compose/store.test.d.ts +1 -0
- package/dist/compose/templates.d.ts +17 -0
- package/dist/compose/types.d.ts +164 -0
- package/dist/configuration/backends.d.ts +66 -0
- package/dist/configuration/backends.test.d.ts +1 -0
- package/dist/configuration/dotenv.d.ts +14 -0
- package/dist/configuration/dotenv.test.d.ts +1 -0
- package/dist/configuration/index.d.ts +6 -0
- package/dist/configuration/migrate.d.ts +10 -0
- package/dist/configuration/migrate.test.d.ts +1 -0
- package/dist/configuration/model.d.ts +50 -0
- package/dist/configuration/service.d.ts +144 -0
- package/dist/configuration/service.test.d.ts +1 -0
- package/dist/configuration/store.d.ts +33 -0
- package/dist/configuration/store.test.d.ts +1 -0
- package/dist/control-plane/authorization.d.ts +24 -0
- package/dist/control-plane/authorization.test.d.ts +1 -0
- package/dist/control-plane/index.d.ts +8 -0
- package/dist/control-plane/migrations.d.ts +7 -0
- package/dist/control-plane/search.d.ts +25 -0
- package/dist/control-plane/store.d.ts +117 -0
- package/dist/control-plane/store.test.d.ts +1 -0
- package/dist/control-plane/types.d.ts +381 -0
- package/dist/cost/reporting.d.ts +24 -0
- package/dist/cost/reporting.test.d.ts +1 -0
- package/dist/cost/resource-inventory.d.ts +32 -0
- package/dist/cost/resource-inventory.test.d.ts +1 -0
- package/dist/cost/resource-optimization.d.ts +59 -0
- package/dist/cost/resource-optimization.test.d.ts +1 -0
- package/dist/data-services/adapters.d.ts +39 -0
- package/dist/data-services/aws-transports.d.ts +29 -0
- package/dist/data-services/aws-transports.test.d.ts +1 -0
- package/dist/data-services/capabilities.d.ts +2 -0
- package/dist/data-services/data-services.test.d.ts +1 -0
- package/dist/data-services/docker-transport.d.ts +55 -0
- package/dist/data-services/docker-transport.test.d.ts +1 -0
- package/dist/data-services/index.d.ts +8 -0
- package/dist/data-services/model.d.ts +79 -0
- package/dist/data-services/secrets.d.ts +13 -0
- package/dist/data-services/secrets.test.d.ts +1 -0
- package/dist/data-services/service.d.ts +51 -0
- package/dist/data-services/store.d.ts +17 -0
- package/dist/deploy/container-image.d.ts +28 -0
- package/dist/deploy/container-image.test.d.ts +1 -0
- package/dist/deploy/dashboard-audit.test.d.ts +1 -0
- package/dist/deploy/dashboard-auth.d.ts +4 -23
- package/dist/deploy/dashboard-backups.integration.test.d.ts +1 -0
- package/dist/deploy/dashboard-configuration.integration.test.d.ts +1 -0
- package/dist/deploy/dashboard-control-plane.d.ts +37 -0
- package/dist/deploy/dashboard-control-plane.test.d.ts +1 -0
- package/dist/deploy/dashboard-data-services.integration.test.d.ts +1 -0
- package/dist/deploy/dashboard-environment-scope.test.d.ts +1 -0
- package/dist/deploy/dashboard-guard.d.ts +12 -0
- package/dist/deploy/dashboard-guard.test.d.ts +1 -0
- package/dist/deploy/dashboard-identities.d.ts +15 -0
- package/dist/deploy/dashboard-identities.test.d.ts +1 -0
- package/dist/deploy/dashboard-jobs.integration.test.d.ts +1 -0
- package/dist/deploy/dashboard-login-page.d.ts +5 -5
- package/dist/deploy/dashboard-login-page.test.d.ts +1 -0
- package/dist/deploy/dashboard-oidc.integration.test.d.ts +1 -0
- package/dist/deploy/dashboard-onboarding.integration.test.d.ts +1 -0
- package/dist/deploy/dashboard-policy.d.ts +4 -2
- package/dist/deploy/dashboard-queue-worker.integration.test.d.ts +1 -0
- package/dist/deploy/dashboard-release.integration.test.d.ts +1 -0
- package/dist/deploy/dashboard-route-manifest.d.ts +14 -0
- package/dist/deploy/dashboard-route-manifest.test.d.ts +1 -0
- package/dist/deploy/dashboard-security.integration.test.d.ts +1 -0
- package/dist/deploy/dashboard-session.d.ts +3 -14
- package/dist/deploy/dashboard-source.integration.test.d.ts +1 -0
- package/dist/deploy/dashboard-users.d.ts +3 -0
- package/dist/deploy/fullstack-container.d.ts +87 -0
- package/dist/deploy/fullstack-container.test.d.ts +1 -0
- package/dist/deploy/index.d.ts +7 -2
- package/dist/deploy/index.js +65 -25
- package/dist/deploy/local-dashboard-server.d.ts +30 -0
- package/dist/deploy/quick-deploy.d.ts +1 -1
- package/dist/deploy/serverless-operations.d.ts +1 -0
- package/dist/deploy/site-target.d.ts +16 -0
- package/dist/deploy/static-api-origin.d.ts +107 -0
- package/dist/deploy/static-api-origin.test.d.ts +1 -0
- package/dist/deploy/static-site-helper.d.ts +0 -16
- package/dist/deploy/telemetry-access.d.ts +5 -0
- package/dist/deploy/telemetry-access.test.d.ts +1 -0
- package/dist/deploy/telemetry-collection.d.ts +44 -0
- package/dist/deploy/telemetry-collection.test.d.ts +1 -0
- package/dist/dns/index.js +3 -3
- package/dist/dns/route53-adapter.d.ts +1 -1
- package/dist/drivers/hetzner/cloud-init.d.ts +1 -1
- package/dist/drivers/index.d.ts +2 -2
- package/dist/drivers/index.js +6 -6
- package/dist/drivers/shared/notifications.d.ts +1 -1
- package/dist/drivers/shared/rpx-gateway.d.ts +1 -0
- package/dist/fleet/fleet.test.d.ts +1 -0
- package/dist/fleet/index.d.ts +144 -0
- package/dist/fleet/queue.d.ts +3 -0
- package/dist/fleet/ssh-driver.d.ts +34 -0
- package/dist/index.d.ts +28 -4
- package/dist/index.js +1132 -424
- package/dist/jobs/adapters.d.ts +23 -0
- package/dist/jobs/index.d.ts +7 -0
- package/dist/jobs/jobs.test.d.ts +1 -0
- package/dist/jobs/model.d.ts +100 -0
- package/dist/jobs/provider-adapters.d.ts +49 -0
- package/dist/jobs/reconcile.d.ts +19 -0
- package/dist/jobs/schedule.d.ts +9 -0
- package/dist/jobs/service.d.ts +30 -0
- package/dist/jobs/store.d.ts +48 -0
- package/dist/maintenance/drivers.d.ts +117 -0
- package/dist/maintenance/index.d.ts +6 -0
- package/dist/maintenance/maintenance.test.d.ts +1 -0
- package/dist/maintenance/manifest.d.ts +15 -0
- package/dist/maintenance/queue.d.ts +9 -0
- package/dist/maintenance/service.d.ts +31 -0
- package/dist/maintenance/store.d.ts +84 -0
- package/dist/maintenance/types.d.ts +192 -0
- package/dist/media.d.ts +62 -0
- package/dist/media.test.d.ts +1 -0
- package/dist/object-storage/index.d.ts +0 -24
- package/dist/onboarding/archive.d.ts +12 -0
- package/dist/onboarding/artifact-store.d.ts +22 -0
- package/dist/onboarding/detection.d.ts +8 -0
- package/dist/onboarding/index.d.ts +9 -0
- package/dist/onboarding/migrations.d.ts +3 -0
- package/dist/onboarding/onboarding.test.d.ts +1 -0
- package/dist/onboarding/plan.d.ts +3 -0
- package/dist/onboarding/registry.d.ts +41 -0
- package/dist/onboarding/service.d.ts +16 -0
- package/dist/onboarding/store.d.ts +30 -0
- package/dist/onboarding/types.d.ts +204 -0
- package/dist/placement/builds.d.ts +20 -0
- package/dist/placement/drivers.d.ts +54 -0
- package/dist/placement/index.d.ts +6 -0
- package/dist/placement/placement.test.d.ts +1 -0
- package/dist/placement/queue.d.ts +4 -0
- package/dist/placement/service.d.ts +34 -0
- package/dist/placement/store.d.ts +49 -0
- package/dist/placement/types.d.ts +109 -0
- package/dist/preview/index.d.ts +3 -0
- package/dist/preview/service.d.ts +45 -0
- package/dist/preview/service.test.d.ts +1 -0
- package/dist/preview/store.d.ts +47 -0
- package/dist/preview/store.test.d.ts +1 -0
- package/dist/preview/types.d.ts +99 -0
- package/dist/push/apns.d.ts +0 -26
- package/dist/push/fcm.d.ts +0 -23
- package/dist/push/index.d.ts +2 -2
- package/dist/push/index.js +1 -1
- package/dist/queue/deployment-handlers.d.ts +27 -0
- package/dist/queue/deployment-handlers.test.d.ts +1 -0
- package/dist/queue/index.d.ts +4 -0
- package/dist/queue/queue.d.ts +57 -0
- package/dist/queue/queue.test.d.ts +1 -0
- package/dist/queue/types.d.ts +93 -0
- package/dist/queue/worker.d.ts +32 -0
- package/dist/queue/worker.test.d.ts +1 -0
- package/dist/regions/drivers.d.ts +139 -0
- package/dist/regions/index.d.ts +5 -0
- package/dist/regions/queue.d.ts +4 -0
- package/dist/regions/regions.test.d.ts +1 -0
- package/dist/regions/service.d.ts +23 -0
- package/dist/regions/store.d.ts +41 -0
- package/dist/regions/types.d.ts +132 -0
- package/dist/release/index.d.ts +6 -0
- package/dist/release/queue.d.ts +15 -0
- package/dist/release/queue.test.d.ts +1 -0
- package/dist/release/runtime.d.ts +35 -0
- package/dist/release/runtime.test.d.ts +1 -0
- package/dist/release/service.d.ts +28 -0
- package/dist/release/store.d.ts +88 -0
- package/dist/release/store.test.d.ts +1 -0
- package/dist/release/strategy.d.ts +8 -0
- package/dist/release/types.d.ts +103 -0
- package/dist/runtime/adapters/aws.d.ts +5 -0
- package/dist/runtime/adapters/docker.d.ts +53 -0
- package/dist/runtime/adapters/systemd.d.ts +26 -0
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/inventory.d.ts +6 -0
- package/dist/runtime/model.d.ts +130 -0
- package/dist/runtime/normalize.d.ts +5 -0
- package/dist/runtime/operations.d.ts +113 -0
- package/dist/runtime/service.d.ts +32 -0
- package/dist/runtime/streams.d.ts +49 -0
- package/dist/security/artifacts.d.ts +105 -0
- package/dist/security/artifacts.test.d.ts +1 -0
- package/dist/security/container-release.d.ts +35 -0
- package/dist/security/container-release.test.d.ts +1 -0
- package/dist/security/index.d.ts +8 -0
- package/dist/security/integration.d.ts +41 -0
- package/dist/security/integration.test.d.ts +1 -0
- package/dist/security/posture-store.d.ts +80 -0
- package/dist/security/posture-store.test.d.ts +1 -0
- package/dist/security/pre-deploy-scanner.d.ts +0 -4
- package/dist/security/scanners.d.ts +81 -0
- package/dist/security/scanners.test.d.ts +1 -0
- package/dist/security/types.d.ts +150 -0
- package/dist/source/git-workspace.d.ts +26 -0
- package/dist/source/git-workspace.test.d.ts +1 -0
- package/dist/source/index.d.ts +6 -0
- package/dist/source/providers.d.ts +199 -0
- package/dist/source/providers.test.d.ts +1 -0
- package/dist/source/service.d.ts +18 -0
- package/dist/source/store.d.ts +149 -0
- package/dist/source/store.test.d.ts +1 -0
- package/dist/source/types.d.ts +191 -0
- package/dist/source/webhooks.d.ts +33 -0
- package/dist/source/webhooks.test.d.ts +1 -0
- package/dist/ssl/acme-client.d.ts +0 -6
- package/dist/storage/compose.d.ts +4 -0
- package/dist/storage/compose.test.d.ts +1 -0
- package/dist/storage/drivers.d.ts +107 -0
- package/dist/storage/drivers.test.d.ts +1 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/model.d.ts +112 -0
- package/dist/storage/service.d.ts +95 -0
- package/dist/storage/storage.test.d.ts +1 -0
- package/dist/storage/store.d.ts +40 -0
- package/dist/telemetry/correlation.test.d.ts +1 -0
- package/dist/telemetry/index.d.ts +4 -0
- package/dist/telemetry/model.d.ts +145 -0
- package/dist/telemetry/policy.d.ts +8 -0
- package/dist/telemetry/policy.test.d.ts +1 -0
- package/dist/telemetry/redaction.d.ts +9 -0
- package/dist/telemetry/store.d.ts +34 -0
- package/dist/telemetry/store.test.d.ts +1 -0
- package/dist/ui/404.html +1506 -17
- package/dist/ui/accept-invitation.html +1163 -0
- package/dist/ui/{server/backups.html → access-denied.html} +344 -55
- package/dist/ui/account/automation.html +1669 -0
- package/dist/ui/account/security.html +1646 -0
- package/dist/ui/applications/compose.html +1590 -0
- package/dist/ui/applications/new.html +1868 -0
- package/dist/ui/data/backups.html +1757 -0
- package/dist/ui/data/services.html +1723 -0
- package/dist/ui/data/volumes.html +1639 -0
- package/dist/ui/index.html +392 -29
- package/dist/ui/integrations.html +1865 -0
- package/dist/ui/operations/alerts.html +1610 -0
- package/dist/ui/operations/configuration.html +1746 -0
- package/dist/ui/operations/jobs.html +1709 -0
- package/dist/ui/operations/maintenance.html +1540 -0
- package/dist/ui/operations/observability.html +1742 -0
- package/dist/ui/{serverless/scheduler.html → operations/previews.html} +447 -46
- package/dist/ui/operations/queue.html +1615 -0
- package/dist/ui/{server/workers.html → operations/regions.html} +413 -49
- package/dist/ui/operations/releases.html +1596 -0
- package/dist/ui/operations/workloads.html +1775 -0
- package/dist/ui/security.html +1767 -0
- package/dist/ui/server/actions.html +446 -57
- package/dist/ui/server/activity.html +346 -1
- package/dist/ui/server/capacity.html +1542 -0
- package/dist/ui/server/database.html +443 -36
- package/dist/ui/server/deployments.html +439 -53
- package/dist/ui/server/diagnostics.html +346 -1
- package/dist/ui/server/firewall.html +476 -50
- package/dist/ui/server/fleet.html +1577 -0
- package/dist/ui/server/logs.html +389 -25
- package/dist/ui/server/metrics.html +346 -1
- package/dist/ui/server/security.html +346 -1
- package/dist/ui/server/services.html +421 -46
- package/dist/ui/server/sites.html +476 -95
- package/dist/ui/server/ssh-keys.html +476 -46
- package/dist/ui/server/team.html +700 -77
- package/dist/ui/server/terminal.html +444 -54
- package/dist/ui/serverless/alarms.html +430 -42
- package/dist/ui/serverless/assets.html +421 -46
- package/dist/ui/serverless/cost.html +346 -1
- package/dist/ui/serverless/data.html +422 -47
- package/dist/ui/serverless/deployments.html +421 -46
- package/dist/ui/serverless/firewall.html +346 -1
- package/dist/ui/serverless/functions.html +423 -48
- package/dist/ui/serverless/logs.html +389 -25
- package/dist/ui/serverless/metrics.html +346 -1
- package/dist/ui/serverless/queues.html +448 -62
- package/dist/ui/serverless/secrets.html +448 -42
- package/dist/ui/serverless/traces.html +350 -5
- package/dist/ui/serverless.html +434 -58
- package/dist/ui-src/pages/404.stx +18 -0
- package/dist/ui-src/pages/accept-invitation.stx +50 -0
- package/dist/ui-src/pages/access-denied.stx +18 -0
- package/dist/ui-src/pages/account/automation.stx +187 -0
- package/dist/ui-src/pages/account/security.stx +249 -0
- package/dist/ui-src/pages/applications/compose.stx +38 -0
- package/dist/ui-src/pages/applications/new.stx +76 -0
- package/dist/ui-src/pages/data/backups.stx +214 -0
- package/dist/ui-src/pages/data/services.stx +257 -0
- package/dist/ui-src/pages/data/volumes.stx +40 -0
- package/dist/ui-src/pages/index.stx +24 -10
- package/dist/ui-src/pages/integrations.stx +186 -0
- package/dist/ui-src/pages/operations/alerts.stx +40 -0
- package/dist/ui-src/pages/operations/configuration.stx +164 -0
- package/dist/ui-src/pages/operations/jobs.stx +238 -0
- package/dist/ui-src/pages/operations/maintenance.stx +15 -0
- package/dist/ui-src/pages/operations/observability.stx +76 -0
- package/dist/ui-src/pages/operations/previews.stx +62 -0
- package/dist/ui-src/pages/operations/queue.stx +158 -0
- package/dist/ui-src/pages/operations/regions.stx +12 -0
- package/dist/ui-src/pages/operations/releases.stx +39 -0
- package/dist/ui-src/pages/operations/workloads.stx +151 -0
- package/dist/ui-src/pages/partials/head.stx +148 -15
- package/dist/ui-src/pages/partials/nav.stx +491 -74
- package/dist/ui-src/pages/partials/op-confirm.stx +6 -6
- package/dist/ui-src/pages/security.stx +264 -0
- package/dist/ui-src/pages/server/actions.stx +17 -26
- package/dist/ui-src/pages/server/capacity.stx +16 -0
- package/dist/ui-src/pages/server/database.stx +31 -16
- package/dist/ui-src/pages/server/deployments.stx +28 -30
- package/dist/ui-src/pages/server/firewall.stx +25 -30
- package/dist/ui-src/pages/server/fleet.stx +10 -0
- package/dist/ui-src/pages/server/logs.stx +11 -9
- package/dist/ui-src/pages/server/metrics.stx +8 -16
- package/dist/ui-src/pages/server/services.stx +19 -28
- package/dist/ui-src/pages/server/sites.stx +75 -72
- package/dist/ui-src/pages/server/ssh-keys.stx +24 -30
- package/dist/ui-src/pages/server/team.stx +286 -149
- package/dist/ui-src/pages/server/terminal.stx +6 -28
- package/dist/ui-src/pages/serverless/alarms.stx +17 -31
- package/dist/ui-src/pages/serverless/assets.stx +13 -22
- package/dist/ui-src/pages/serverless/cost.stx +15 -3
- package/dist/ui-src/pages/serverless/data.stx +12 -25
- package/dist/ui-src/pages/serverless/deployments.stx +14 -23
- package/dist/ui-src/pages/serverless/firewall.stx +5 -1
- package/dist/ui-src/pages/serverless/functions.stx +20 -26
- package/dist/ui-src/pages/serverless/logs.stx +11 -10
- package/dist/ui-src/pages/serverless/metrics.stx +18 -17
- package/dist/ui-src/pages/serverless/queues.stx +34 -42
- package/dist/ui-src/pages/serverless/secrets.stx +31 -34
- package/dist/ui-src/pages/serverless/traces.stx +2 -6
- package/dist/ui-src/pages/serverless.stx +40 -74
- package/package.json +11 -11
- package/dist/chunk-b17vf1g1.js +0 -13787
- package/dist/chunk-b82pbxyp.js +0 -1572
- package/dist/chunk-pqfzdg68.js +0 -12
- package/dist/chunk-z58fqnjm.js +0 -10
- package/dist/ui-src/pages/server/backups.stx +0 -91
- package/dist/ui-src/pages/server/workers.stx +0 -99
- package/dist/ui-src/pages/serverless/scheduler.stx +0 -89
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
let __q = null
|
|
3
|
+
try { __q = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __q = null }
|
|
4
|
+
const viewerCapabilities = __q?.viewer?.capabilities ?? []
|
|
5
|
+
const canCancel = viewerCapabilities.includes('deployments:cancel')
|
|
6
|
+
const canRetry = viewerCapabilities.includes('deployments:create')
|
|
7
|
+
const canManage = viewerCapabilities.includes('automation:manage')
|
|
8
|
+
</script>
|
|
9
|
+
<script client>
|
|
10
|
+
const operations = state([])
|
|
11
|
+
const concurrency = state({ project: 2, environment: 1, provider: 2, builds: 1 })
|
|
12
|
+
const selectedId = state('')
|
|
13
|
+
const logs = state([])
|
|
14
|
+
const filter = state(new URLSearchParams(location.search).get('state') || 'active')
|
|
15
|
+
const loading = state(true)
|
|
16
|
+
const message = state('')
|
|
17
|
+
const confirmLimits = state('')
|
|
18
|
+
const confirmClear = state('')
|
|
19
|
+
let source = null
|
|
20
|
+
const selected = derived(() => operations().find(item => item.operation.id === selectedId()) || null)
|
|
21
|
+
const visible = derived(() => operations().filter(item => filter() === 'all' ? true : filter() === 'active' ? ['queued', 'running'].includes(item.operation.state) : item.operation.state === filter()))
|
|
22
|
+
function duration(operation) {
|
|
23
|
+
if (!operation.startedAt) return 'Not started'
|
|
24
|
+
const end = operation.finishedAt ? new Date(operation.finishedAt).getTime() : Date.now()
|
|
25
|
+
const seconds = Math.max(0, Math.round((end - new Date(operation.startedAt).getTime()) / 1000))
|
|
26
|
+
return seconds < 60 ? seconds + 's' : Math.round(seconds / 60) + 'm'
|
|
27
|
+
}
|
|
28
|
+
function statusTone(value) { return value === 'succeeded' ? 'ok' : ['failed', 'timed_out'].includes(value) ? 'bad' : ['running', 'queued'].includes(value) ? 'warn' : '' }
|
|
29
|
+
function setFilter(value) { filter.set(value); const url = new URL(location.href); value === 'active' ? url.searchParams.delete('state') : url.searchParams.set('state', value); history.replaceState(null, '', url) }
|
|
30
|
+
async function refresh() {
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch('/api/queue')
|
|
33
|
+
const body = await response.json()
|
|
34
|
+
if (!response.ok) throw new Error(body.error || 'Queue could not be loaded.')
|
|
35
|
+
operations.set(body.operations || [])
|
|
36
|
+
concurrency.set(body.concurrency || concurrency())
|
|
37
|
+
if (selectedId() && !body.operations.some(item => item.operation.id === selectedId())) closeDetail()
|
|
38
|
+
} catch (error) { message.set(error.message || String(error)) }
|
|
39
|
+
finally { loading.set(false) }
|
|
40
|
+
}
|
|
41
|
+
function appendLog(entry) {
|
|
42
|
+
if (!logs().some(item => item.sequence === entry.sequence)) logs.set([...logs(), entry].sort((a, b) => a.sequence - b.sequence))
|
|
43
|
+
}
|
|
44
|
+
async function openDetail(item) {
|
|
45
|
+
selectedId.set(item.operation.id); logs.set([]); if (source) source.close()
|
|
46
|
+
const response = await fetch('/api/queue/logs?id=' + encodeURIComponent(item.operation.id))
|
|
47
|
+
const body = await response.json(); (body.entries || []).forEach(appendLog)
|
|
48
|
+
const cursor = body.cursor || 0
|
|
49
|
+
if (!['succeeded', 'failed', 'cancelled', 'timed_out'].includes(item.operation.state)) {
|
|
50
|
+
source = new EventSource('/api/queue/stream?id=' + encodeURIComponent(item.operation.id) + '&after=' + cursor)
|
|
51
|
+
source.addEventListener('log', event => appendLog(JSON.parse(event.data)))
|
|
52
|
+
source.addEventListener('complete', () => { source.close(); source = null; refresh() })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function closeDetail() { selectedId.set(''); logs.set([]); if (source) source.close(); source = null }
|
|
56
|
+
async function mutate(path, body) {
|
|
57
|
+
message.set('')
|
|
58
|
+
const response = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
|
|
59
|
+
const result = await response.json(); if (!response.ok || result.ok === false) { message.set(result.error || 'Operation failed.'); return }
|
|
60
|
+
message.set('Queue updated.'); await refresh()
|
|
61
|
+
const item = operations().find(value => value.operation.id === body.id); if (item) openDetail(item)
|
|
62
|
+
}
|
|
63
|
+
async function cancelSelected() { const item = selected(); if (item) await mutate('/api/queue/cancel', { id: item.operation.id }) }
|
|
64
|
+
async function retrySelected() { const item = selected(); const errorClass = item?.job.retryClasses?.[0]; if (item && errorClass) await mutate('/api/queue/retry', { id: item.operation.id, errorClass }) }
|
|
65
|
+
async function saveLimits() {
|
|
66
|
+
const response = await fetch('/api/queue/settings', { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: confirmLimits(), concurrency: concurrency() }) })
|
|
67
|
+
const result = await response.json(); if (!response.ok) message.set(result.error || 'Limits were not updated.'); else { concurrency.set(result.concurrency); confirmLimits.set(''); message.set('Concurrency limits updated and audited.') }
|
|
68
|
+
}
|
|
69
|
+
async function clearHistory() {
|
|
70
|
+
const response = await fetch('/api/queue/history', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: confirmClear() }) })
|
|
71
|
+
const result = await response.json(); if (!response.ok) message.set(result.error || 'History was not cleared.'); else { confirmClear.set(''); message.set('Cleared ' + result.deleted + ' operations whose retention had elapsed.'); refresh() }
|
|
72
|
+
}
|
|
73
|
+
onMount(() => { refresh(); const timer = setInterval(refresh, 3000); return () => { clearInterval(timer); if (source) source.close() } })
|
|
74
|
+
</script>
|
|
75
|
+
<!DOCTYPE html>
|
|
76
|
+
<html lang="en" data-theme="dark">
|
|
77
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Deployment queue' })</head>
|
|
78
|
+
<body>
|
|
79
|
+
@include('../partials/nav', { active: 'operation-queue', env: 'production' })
|
|
80
|
+
<div class="queue-wrap wrap">
|
|
81
|
+
<div class="crumbs">Operations <span class="sep">/</span> Deployment queue</div>
|
|
82
|
+
<header class="page">
|
|
83
|
+
<div><h1>Deployment queue</h1><p>Durable deployments and mutations, with ordered progress, resource locks, cancellation, and safe retries.</p></div>
|
|
84
|
+
<div class="queue-limits"><span>Project <b>{{ concurrency().project }}</b></span><span>Environment <b>{{ concurrency().environment }}</b></span><span>Provider <b>{{ concurrency().provider }}</b></span><span>Builds <b>{{ concurrency().builds }}</b></span></div>
|
|
85
|
+
</header>
|
|
86
|
+
<div class="bad notice" @show="!!message()">{{ message() }}</div>
|
|
87
|
+
<div class="queue-layout" :class="{ inspecting: !!selectedId() }">
|
|
88
|
+
<main class="panel queue-main">
|
|
89
|
+
<div class="filters queue-filters">
|
|
90
|
+
<div class="segmented" role="group" aria-label="Filter queue state">
|
|
91
|
+
<button type="button" :class="{ active: filter() === 'active' }" @click="setFilter('active')">Active</button>
|
|
92
|
+
<button type="button" :class="{ active: filter() === 'all' }" @click="setFilter('all')">All</button>
|
|
93
|
+
<button type="button" :class="{ active: filter() === 'failed' }" @click="setFilter('failed')">Failed</button>
|
|
94
|
+
<button type="button" :class="{ active: filter() === 'cancelled' }" @click="setFilter('cancelled')">Cancelled</button>
|
|
95
|
+
</div>
|
|
96
|
+
<button class="btn sm" type="button" @click="refresh()">Refresh</button>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="queue-table-wrap">
|
|
99
|
+
<table class="queue-table">
|
|
100
|
+
<thead><tr><th scope="col">Status</th><th scope="col">Target</th><th scope="col">Operation</th><th scope="col">Actor / trigger</th><th scope="col">Commit</th><th scope="col">Progress</th><th scope="col">Attempt</th><th scope="col">Timing</th></tr></thead>
|
|
101
|
+
<tbody>
|
|
102
|
+
<template :for="item in visible()">
|
|
103
|
+
<tr class="queue-row" :class="{ selected: item.operation.id === selectedId() }" @click="openDetail(item)" tabindex="0">
|
|
104
|
+
<td><span class="pill" :class="statusTone(item.operation.state)">{{ item.operation.state }}</span></td>
|
|
105
|
+
<td><b>{{ item.target.name }}</b><small>{{ item.target.kind }} · {{ item.target.slug }}</small></td>
|
|
106
|
+
<td class="mono">{{ item.operation.kind }}<small>{{ item.operation.id.slice(0, 8) }}</small></td>
|
|
107
|
+
<td>{{ item.actor?.name || 'System' }}<small>{{ item.actor?.kind || 'automation' }}</small></td>
|
|
108
|
+
<td class="mono">{{ item.commit ? String(item.commit).slice(0, 12) : '—' }}</td>
|
|
109
|
+
<td><span @show="!!item.job.currentStep">{{ item.job.currentStep }}</span><span class="blocked" @show="!!item.job.blockedReason">{{ item.job.blockedReason }}</span><small @show="!!item.approximatePosition && item.operation.state === 'queued'">About {{ item.approximatePosition.ahead }} ahead</small></td>
|
|
110
|
+
<td class="mono">{{ item.operation.attempt }} / {{ item.job.maxAttempts }}</td>
|
|
111
|
+
<td>{{ duration(item.operation) }}<small>{{ new Date(item.operation.createdAt).toLocaleString() }}</small></td>
|
|
112
|
+
</tr>
|
|
113
|
+
</template>
|
|
114
|
+
</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="empty" @show="!loading() && visible().length === 0"><strong>No matching work</strong><span>Queued deployments and other long-lived mutations will appear here.</span></div>
|
|
118
|
+
</main>
|
|
119
|
+
<aside class="panel queue-detail" @show="!!selected()">
|
|
120
|
+
<button class="detail-close" type="button" aria-label="Close operation detail" @click="closeDetail()">×</button>
|
|
121
|
+
<template :if="selected()">
|
|
122
|
+
<div class="eyebrow">{{ selected().operation.kind }}</div>
|
|
123
|
+
<h2>{{ selected().target.name }}</h2>
|
|
124
|
+
<div class="detail-meta"><span class="pill" :class="statusTone(selected().operation.state)">{{ selected().operation.state }}</span><span>Attempt {{ selected().operation.attempt }} of {{ selected().job.maxAttempts }}</span><span>{{ duration(selected().operation) }}</span></div>
|
|
125
|
+
<div class="blocked-card" @show="!!selected().job.blockedReason"><b>Waiting</b><span>{{ selected().job.blockedReason }}</span><small>Position is an estimate because earlier work may unblock or retry.</small></div>
|
|
126
|
+
<div class="detail-actions">
|
|
127
|
+
@if (canCancel)<button class="btn danger sm" type="button" @show="['queued','running'].includes(selected().operation.state)" @click="cancelSelected()">{{ selected().operation.state === 'queued' ? 'Cancel queued' : 'Request cancellation' }}</button>@endif
|
|
128
|
+
@if (canRetry)<button class="btn sm" type="button" @show="['failed','cancelled','timed_out'].includes(selected().operation.state) && selected().job.retryClasses.length > 0 && selected().operation.attempt < selected().job.maxAttempts" @click="retrySelected()">Retry {{ selected().job.retryClasses[0] }}</button>@endif
|
|
129
|
+
</div>
|
|
130
|
+
<h3>Structured progress</h3>
|
|
131
|
+
<div class="step-list"><template :for="entry in logs().filter(item => item.stream === 'step')"><div><span></span><b>{{ entry.step || 'Step' }}</b><small>{{ entry.message }}</small></div></template></div>
|
|
132
|
+
<details class="raw-logs" open><summary>Raw logs · {{ logs().length }} chunks</summary><pre><template :for="entry in logs()"><span :class="entry.stream">{{ entry.sequence }} {{ entry.stream.toUpperCase() }}{{ entry.redacted ? ' [REDACTED]' : '' }}{{ entry.truncated ? ' [TRUNCATED]' : '' }} {{ entry.message }}
|
|
133
|
+
</span></template></pre></details>
|
|
134
|
+
<div class="detail-foot mono">{{ selected().operation.id }}</div>
|
|
135
|
+
</template>
|
|
136
|
+
</aside>
|
|
137
|
+
</div>
|
|
138
|
+
@if (canManage)
|
|
139
|
+
<section class="panel queue-admin">
|
|
140
|
+
<div><h2>Queue controls</h2><p>Concurrency changes affect production scheduling and are audited. Retention cleanup never removes jobs before their policy expires.</p></div>
|
|
141
|
+
<div class="limit-grid">
|
|
142
|
+
<label>Project<input type="number" min="1" max="100" :value="concurrency().project" @input="concurrency.set({...concurrency(), project: Number($event.target.value)})"></label>
|
|
143
|
+
<label>Environment<input type="number" min="1" max="100" :value="concurrency().environment" @input="concurrency.set({...concurrency(), environment: Number($event.target.value)})"></label>
|
|
144
|
+
<label>Provider<input type="number" min="1" max="100" :value="concurrency().provider" @input="concurrency.set({...concurrency(), provider: Number($event.target.value)})"></label>
|
|
145
|
+
<label>Builds<input type="number" min="1" max="100" :value="concurrency().builds" @input="concurrency.set({...concurrency(), builds: Number($event.target.value)})"></label>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="confirm-row"><input type="text" placeholder="update queue limits" :value="confirmLimits()" @input="confirmLimits.set($event.target.value)"><button class="btn" type="button" :disabled="confirmLimits() !== 'update queue limits'" @click="saveLimits()">Update limits</button></div>
|
|
148
|
+
<div class="confirm-row"><input type="text" placeholder="clear completed" :value="confirmClear()" @input="confirmClear.set($event.target.value)"><button class="btn danger" type="button" :disabled="confirmClear() !== 'clear completed'" @click="clearHistory()">Clear retained history</button></div>
|
|
149
|
+
</section>
|
|
150
|
+
@endif
|
|
151
|
+
</div>
|
|
152
|
+
<style>
|
|
153
|
+
.queue-wrap{max-width:1500px}.queue-limits{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.queue-limits span{border:1px solid var(--line);background:var(--bg2);border-radius:8px;padding:8px 10px;color:var(--txt3);font-size:12px}.queue-limits b{color:var(--txt);margin-left:5px}.queue-layout{display:grid;grid-template-columns:minmax(0,1fr);gap:16px}.queue-layout.inspecting{grid-template-columns:minmax(620px,1.4fr) minmax(340px,.6fr)}.queue-main{padding:0;overflow:hidden}.queue-filters{padding:14px 16px;margin:0;border-bottom:1px solid var(--line);display:flex;justify-content:space-between}.queue-table-wrap{overflow:auto}.queue-table{min-width:1060px}.queue-row{cursor:pointer}.queue-row.selected{background:color-mix(in srgb,var(--accent) 9%,transparent)}.queue-row td small{display:block;margin-top:4px;color:var(--txt3);font-size:11px}.blocked{color:var(--yellow);font-size:12px}.queue-detail{position:sticky;top:16px;align-self:start;max-height:calc(100vh - 32px);overflow:auto;padding:22px}.detail-close{float:right;border:0;background:transparent;color:var(--txt3);font-size:24px;cursor:pointer}.eyebrow{text-transform:uppercase;letter-spacing:.08em;color:var(--accent);font-size:11px}.queue-detail h2{margin:5px 0 10px}.detail-meta,.detail-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;color:var(--txt3);font-size:12px}.blocked-card{display:grid;gap:5px;margin:16px 0;padding:12px;border:1px solid color-mix(in srgb,var(--yellow) 35%,var(--line));border-radius:8px;background:color-mix(in srgb,var(--yellow) 6%,transparent)}.blocked-card small{color:var(--txt3)}.detail-actions{margin:16px 0}.step-list>div{display:grid;grid-template-columns:12px 1fr;gap:0 8px;margin:10px 0}.step-list span{width:8px;height:8px;margin-top:5px;border-radius:50%;background:var(--accent)}.step-list small{grid-column:2;color:var(--txt3)}.raw-logs{margin-top:14px}.raw-logs pre{max-height:330px;overflow:auto;background:#090c12;border:1px solid var(--line);border-radius:8px;padding:12px;font-size:11px;white-space:pre-wrap}.raw-logs .stderr{color:#ff9b9b}.raw-logs .step{color:#8dd6ff}.detail-foot{margin-top:16px;color:var(--txt3);font-size:10px}.queue-admin{margin-top:16px;display:grid;grid-template-columns:1fr 1fr;gap:14px 24px}.limit-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.limit-grid label{font-size:11px;color:var(--txt3)}.limit-grid input,.confirm-row input{width:100%;margin-top:5px}.confirm-row{display:flex;gap:8px;align-items:end}.confirm-row input{margin:0}.notice{margin-bottom:12px}.notice.bad{padding:10px 12px}
|
|
154
|
+
@media(max-width:1050px){.queue-layout.inspecting{grid-template-columns:1fr}.queue-detail{position:static;max-height:none}.queue-admin{grid-template-columns:1fr}}
|
|
155
|
+
@media(max-width:680px){.queue-limits{justify-content:flex-start}.queue-table{min-width:0}.queue-table thead{display:none}.queue-table tbody,.queue-row{display:block}.queue-row{padding:14px;border-bottom:1px solid var(--line)}.queue-row td{display:grid;grid-template-columns:100px 1fr;padding:4px 0;border:0}.queue-row td:before{color:var(--txt3);font-size:11px}.queue-row td:nth-child(1):before{content:'Status'}.queue-row td:nth-child(2):before{content:'Target'}.queue-row td:nth-child(3):before{content:'Operation'}.queue-row td:nth-child(4):before{content:'Actor'}.queue-row td:nth-child(5):before{content:'Commit'}.queue-row td:nth-child(6):before{content:'Progress'}.queue-row td:nth-child(7):before{content:'Attempt'}.queue-row td:nth-child(8):before{content:'Timing'}.limit-grid{grid-template-columns:repeat(2,1fr)}.confirm-row{flex-direction:column;align-items:stretch}}
|
|
156
|
+
</style>
|
|
157
|
+
</body>
|
|
158
|
+
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script server>import { usePolling } from '../../functions/usePolling.ts'</script><script>
|
|
2
|
+
const items=state([]),selected=state(null),adding=state(false),busy=state(false),error=state(''),notice=state('')
|
|
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(){const body=await api('/api/regions');items.set(body.topologies||[]);if(selected())selected.set((body.topologies||[]).find(item=>item.topology.id===selected().topology.id)||null)}const{pollingState,pollingError,lastUpdatedAt,refresh}=usePolling(load,12000)
|
|
4
|
+
async function post(url,body){busy.set(true);error.set('');try{const result=await api(url,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});await refresh();return result}catch(cause){error.set(cause.message)}finally{busy.set(false)}}
|
|
5
|
+
async function create(event){event.preventDefault();const value=Object.fromEntries(new FormData(event.currentTarget)),regions=String(value.regions).split(',').map((region,index)=>({region:region.trim(),provider:'aws',role:index===0?'primary':'secondary'}));await post('/api/regions',{...value,regions,replicate:['s3','dynamodb','secrets'],maxLagSeconds:Number(value.maxLagSeconds),cdnEnabled:!!value.cdnEnabled,wafEnabled:!!value.wafEnabled,retainOnDestroy:true});adding.set(false);notice.set('Topology saved. Review its rollout plan before applying an immutable revision.')}
|
|
6
|
+
async function action(name){if(!selected())return;const topology=selected().topology;let body={topologyId:topology.id,action:name};if(name==='rollout'){const revision=prompt('Immutable revision or release digest');if(!revision)return;body={...body,revision,manifest:{topology:topology.name}}}if(name==='failover'||name==='failback'){const region=prompt(`Target region (${topology.regions.map(item=>item.region).join(', ')})`);if(!region)return;body={...body,targetRegion:region}}if(name==='destroy'){const confirmation=prompt(`Type ${topology.name} to drain traffic and delete stacks. Replicated data is retained.`);if(confirmation!==topology.name)return;body={...body,confirmation}}const result=await post('/api/regions/action',body);if(result)notice.set(`${name} queued as ${result.operation.id}.`)}
|
|
7
|
+
function tone(status){return ['ready','failed_over'].includes(status)?'ok':['failed','degraded'].includes(status)?'bad':'warn'}function sync(item){return `${item.channels.filter(value=>value.status==='in_sync').length}/${item.channels.length}`}
|
|
8
|
+
</script><!DOCTYPE html><html lang="en" data-theme="dark"><head>@include('../partials/head',{title:'ts-cloud · Regions & failover'})</head><body>@include('../partials/nav',{active:'regions',env:'production'})
|
|
9
|
+
<main class="wrap" id="main-content"><div class="crumbs">Operations <span class="sep">/</span> Regions & failover</div><header class="page"><div><h1>Regions & failover</h1><p>Stage immutable stacks, replication, edge traffic, failover, failback, and retained-data teardown from one durable timeline.</p></div><div class="actions"><span class="pill" :class="pollingState()==='live'?'ok':'warn'">{{ pollingState() }}</span><small>{{ lastUpdatedAt()?.toLocaleTimeString()||'connecting' }}</small><button class="btn sm" @click="adding.set(!adding())">{{ adding()?'Close':'New topology' }}</button></div></header><p class="notice" role="status" @show="!!notice()">{{ notice() }}</p><p class="bad notice" role="alert" @show="!!error()||!!pollingError()">{{ error()||pollingError() }}</p>
|
|
10
|
+
<form class="create panel" @show="adding()" @submit="create($event)"><div class="wide"><h2>New regional topology</h2><p>List the primary first. Replication is configured in both directions so failback has a fresh path.</p></div><label>Name<input name="name" required pattern="[a-z0-9][a-z0-9-]{1,62}"></label><label>Hostname<input name="hostname" required placeholder="api.example.com"></label><label>Regions<input name="regions" required placeholder="us-west-2,us-east-1"></label><label>Traffic policy<select name="trafficPolicy"><option value="active_passive">Active / passive</option><option value="weighted">Weighted</option><option value="latency">Latency</option></select></label><label>Maximum replication lag<input name="maxLagSeconds" type="number" min="0" value="30"></label><label class="check"><input name="cdnEnabled" type="checkbox"> CloudFront</label><label class="check"><input name="wafEnabled" type="checkbox"> WAF</label><button class="btn" type="submit">Save topology</button></form>
|
|
11
|
+
<section class="layout"><div class="cards"><template :for="item in items()"><article class="card panel"><div><span class="pill" :class="tone(item.topology.status)">{{ item.topology.status }}</span><h2>{{ item.topology.name }}</h2><p>{{ item.topology.hostname }} · active {{ item.topology.activeRegion }}</p></div><div class="facts"><span>{{ item.topology.regions.length }} regions</span><span>{{ sync(item) }} replication paths in sync</span><span>{{ item.route?.dnsProvider }} · {{ item.route?.cdnEnabled?'CloudFront · ':'' }}{{ item.route?.wafEnabled?'WAF':'' }}</span></div><button class="btn ghost sm" @click="selected.set(item)">Inspect</button></article></template></div><aside class="detail panel" @show="!!selected()"><div class="actions"><h2>{{ selected()?.topology.name }}</h2><button class="btn ghost sm" @click="selected.set(null)">Close</button></div><div class="actions"><button class="btn sm" @click="action('rollout')" :disabled="busy()">Roll out</button><button class="btn sm warn" @click="action('failover')" :disabled="busy()">Fail over</button><button class="btn ghost sm" @click="action('failback')" :disabled="busy()">Fail back</button><button class="btn danger sm" @click="action('destroy')" :disabled="busy()">Drain & destroy</button></div><h3>Regional stacks</h3><template :for="target in selected()?.targets||[]"><p class="row"><b>{{ target.region }}</b><span>{{ target.role }} · {{ target.status }}</span></p></template><h3>Replication</h3><template :for="channel in selected()?.channels||[]"><p class="row"><b>{{ channel.kind }} → {{ channel.targetRegion }}</b><span>{{ channel.status }} · {{ channel.lagSeconds??'—' }}s</span></p></template><h3>Traffic</h3><pre>{{ JSON.stringify(selected()?.route?.weights||{},null,2) }}</pre><h3>Execution timeline</h3><template :for="execution in selected()?.executions||[]"><div class="execution"><span class="pill" :class="tone(execution.status)">{{ execution.status }}</span><b>{{ execution.kind }}</b><small>{{ execution.currentStep||execution.completedSteps.at(-1)||'queued' }}</small><p @show="!!execution.error">{{ execution.error }}</p></div></template></aside></section>
|
|
12
|
+
</main><style>.actions{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.actions h2{margin-right:auto}.create{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-bottom:1rem}.create label{display:grid;gap:.3rem}.create .check{display:flex;align-items:center}.wide{grid-column:1/-1}.layout{display:grid;grid-template-columns:1fr 430px;gap:1rem}.cards{display:grid;gap:1rem}.card{display:grid;grid-template-columns:1fr 1fr auto;align-items:center;gap:1rem}.facts{display:grid;gap:.3rem}.detail{position:sticky;top:1rem;align-self:start}.row{display:flex;justify-content:space-between;border-bottom:1px solid var(--border);padding:.5rem 0}.execution{display:grid;grid-template-columns:auto 1fr auto;gap:.5rem;align-items:center;border-bottom:1px solid var(--border);padding:.6rem 0}.execution p{grid-column:1/-1;color:var(--danger)}@media(max-width:1000px){.layout{grid-template-columns:1fr}.detail{position:static}.create{grid-template-columns:1fr 1fr}.card{grid-template-columns:1fr}}@media(max-width:600px){.create{grid-template-columns:1fr}.wide{grid-column:auto}}</style></body></html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
let __r = null
|
|
3
|
+
try { __r = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __r = null }
|
|
4
|
+
const capabilities = __r?.viewer?.capabilities ?? []
|
|
5
|
+
const canOperate = capabilities.includes('deployments:create')
|
|
6
|
+
</script>
|
|
7
|
+
<script client>
|
|
8
|
+
const data = state({ releases: [], resources: [], environments: [] })
|
|
9
|
+
const selected = state('')
|
|
10
|
+
const message = state('')
|
|
11
|
+
const filters = state({ environment: '', status: '', kind: '' })
|
|
12
|
+
const promote = state({ targetResourceId: '', config: '{}', strategy: '', approvalRequired: true })
|
|
13
|
+
async function request(body){const response=await fetch('/api/releases/action',{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||'Release action failed.');return result}
|
|
14
|
+
async function refresh(){try{const response=await fetch('/api/releases');const result=await response.json();if(!response.ok)throw new Error(result.error||'Releases could not be loaded.');data.set(result);if(!selected()&&result.releases[0])selected.set(result.releases[0].id)}catch(error){message.set(error.message)}}
|
|
15
|
+
function current(){return data().releases.find(value=>value.id===selected())||data().releases[0]}
|
|
16
|
+
function visible(){return data().releases.filter(value=>(!filters().environment||value.environmentId===filters().environment)&&(!filters().status||value.status===filters().status)&&(!filters().kind||value.kind===filters().kind))}
|
|
17
|
+
function tone(status){return status==='active'?'ok':['failed','rolled_back'].includes(status)?'bad':['activating','awaiting_approval'].includes(status)?'warn':''}
|
|
18
|
+
function short(value){return value?String(value).slice(0,12):'—'}
|
|
19
|
+
function duration(item){if(!item?.activatedAt&&!item?.failedAt)return'Pending';const end=new Date(item.activatedAt||item.failedAt);return Math.max(0,Math.round((end-new Date(item.createdAt))/1000))+'s'}
|
|
20
|
+
function configObject(){try{return JSON.parse(promote().config)}catch{throw new Error('Promotion config must be valid JSON.')}}
|
|
21
|
+
async function act(action,extra={}){const item=current();if(!item)return;try{await request({id:item.id,action,...extra});message.set(action+' accepted for '+item.resource.slug+'.');await refresh()}catch(error){message.set(error.message)}}
|
|
22
|
+
async function approve(decision){const item=current();const confirm=prompt('Type '+item.environment.slug+' to '+decision+' this environment gate');if(confirm==null)return;await act('approve',{decision,confirm,comment:prompt('Approval note (optional)')||undefined})}
|
|
23
|
+
async function rollback(){const item=current();const target=item.previous;if(!target)return;const confirm=prompt('Rollback preview:\nCurrent '+short(item.artifact.digest)+' / config '+short(item.configHash)+'\nRestore release '+short(target.id)+' / config '+short(target.configHash)+'\nData: '+item.comparison.dataCaveat+'\n\nType '+item.resource.slug+' to continue');if(confirm==null)return;await act('rollback',{targetReleaseId:target.id,confirm})}
|
|
24
|
+
async function promoteRelease(event){event.preventDefault();const item=current();const target=data().resources.find(value=>value.id===promote().targetResourceId);const environment=data().environments.find(value=>value.id===target?.environmentId);if(!target||!environment){message.set('Choose a promotion target.');return}const confirm=prompt('Promote the exact artifact '+item.artifact.digest+' without rebuilding.\nType '+environment.slug+' to continue');if(confirm==null)return;let config;try{config=configObject()}catch(error){message.set(error.message);return}await act('promote',{targetResourceId:target.id,config,strategy:promote().strategy||undefined,approvalRequired:promote().approvalRequired,confirm})}
|
|
25
|
+
onMount(()=>{refresh();const timer=setInterval(refresh,5000);return()=>clearInterval(timer)})
|
|
26
|
+
</script>
|
|
27
|
+
<!DOCTYPE html><html lang="en" data-theme="dark"><head>@include('../partials/head',{title:'ts-cloud - Releases & promotion'})</head><body>
|
|
28
|
+
@include('../partials/nav',{active:'releases',env:'production'})
|
|
29
|
+
<main class="releases-wrap wrap" id="main-content"><div class="crumbs">Operations <span class="sep">/</span> Releases & promotion</div><header class="page"><div><h1>Releases & promotion</h1><p>One immutable artifact history across static, compute, serverless, containers, and Compose.</p></div><button class="btn ghost" @click="refresh()">Refresh</button></header><p class="notice" @show="!!message()">{{message()}}</p>
|
|
30
|
+
<section class="section stats"><div class="panel"><small>Total releases</small><strong>{{data().releases.length}}</strong></div><div class="panel"><small>Active</small><strong>{{data().releases.filter(v=>v.status==='active').length}}</strong></div><div class="panel"><small>Needs attention</small><strong>{{data().releases.filter(v=>['failed','awaiting_approval'].includes(v.status)).length}}</strong></div><div class="panel"><small>Pinned</small><strong>{{data().releases.filter(v=>v.pinned).length}}</strong></div></section>
|
|
31
|
+
<section class="panel section toolbar"><label>Environment<select :value="filters().environment" @change="filters.set({...filters(),environment:$event.target.value})"><option value="">All</option><option @each="env in data().environments" :value="env.id">{{env.name}}</option></select></label><label>Status<select :value="filters().status" @change="filters.set({...filters(),status:$event.target.value})"><option value="">All</option><option @each="status in ['built','awaiting_approval','activating','active','failed','rolled_back','superseded']" :value="status">{{status}}</option></select></label><label>Kind<select :value="filters().kind" @change="filters.set({...filters(),kind:$event.target.value})"><option value="">All</option><option @each="kind in ['static','compute','serverless_zip','serverless_image','container','compose']" :value="kind">{{kind}}</option></select></label></section>
|
|
32
|
+
<div class="release-layout section"><section class="panel release-list"><div class="empty" @show="!visible().length"><b>No releases match this view</b><span>Create a release from a verified artifact with the CLI or API.</span></div><button class="release-row" :class="selected()===item.id?'selected':''" @each="item in visible()" :key="item.id" @click="selected.set(item.id)"><span class="pill" :class="tone(item.status)">{{item.status}}</span><strong>{{item.resource?.name||item.resourceId}}</strong><small>{{item.environment?.name}} · {{item.kind}} · {{item.strategy}}</small><code>{{short(item.sourceSha)}} · {{short(item.artifact?.digest)}}</code><time>{{new Date(item.createdAt).toLocaleString()}}</time></button></section>
|
|
33
|
+
<section class="detail" @show="!!current()"><article class="hero panel"><div><span class="pill" :class="tone(current()?.status)">{{current()?.status}}</span><h2>{{current()?.resource?.name}}</h2><p>{{current()?.environment?.name}} · {{current()?.kind}} · {{current()?.strategy}}</p></div><div class="actions">@if(canOperate)<button class="btn sm" @show="['built','failed','rolled_back','superseded'].includes(current()?.status)" @click="act('activate')">Activate</button><button class="btn sm" @show="current()?.status==='awaiting_approval'" @click="approve('approved')">Approve</button><button class="btn ghost sm" @show="current()?.status==='awaiting_approval'" @click="approve('rejected')">Reject</button><button class="btn danger sm" @show="current()?.status==='active'&&!!current()?.previous" @click="rollback()">Rollback…</button><button class="btn ghost sm" @click="act('pin',{pinned:!current()?.pinned,reason:current()?.pinned?undefined:'operator retention'})">{{current()?.pinned?'Unpin':'Pin'}}</button>@endif</div></article>
|
|
34
|
+
<article class="facts panel"><h3>Immutable identity</h3><dl><div><dt>Source commit</dt><dd class="mono">{{current()?.sourceSha||'Not recorded'}}</dd></div><div><dt>Artifact digest</dt><dd class="mono">{{current()?.artifact?.digest}}</dd></div><div><dt>Artifact URI</dt><dd class="mono">{{current()?.artifact?.uri}}</dd></div><div><dt>Config hash</dt><dd class="mono">{{current()?.configHash}}</dd></div><div><dt>Trigger / actor</dt><dd>{{current()?.trigger}} · {{short(current()?.actorId)}}</dd></div><div><dt>Duration</dt><dd>{{duration(current())}}</dd></div><div><dt>Provider versions</dt><dd class="mono">{{JSON.stringify(current()?.transitions.at(-1)?.health||{})}}</dd></div><div><dt>Provenance / attestation</dt><dd class="mono">{{JSON.stringify({provenance:current()?.artifact?.provenance,attestation:current()?.artifact?.attestation})}}</dd></div></dl></article>
|
|
35
|
+
<article class="caveat panel" @show="!!current()?.comparison"><h3>Change and rollback preview</h3><div class="change-grid"><span>Artifact <b>{{current()?.comparison.artifactChanged?'changed':'identical'}}</b></span><span>Source <b>{{current()?.comparison.sourceChanged?'changed':'identical'}}</b></span><span>Config <b>{{current()?.comparison.configChanged?'changed':'identical'}}</b></span><span>Manifest <b>{{current()?.comparison.manifestChanges.length}} change(s)</b></span></div><p>{{current()?.comparison.dataCaveat}}</p><p class="bad-text" @show="current()?.hooks.migrations==='irreversible'">Irreversible migration hooks are recorded. Traffic rollback will not restore data.</p></article>
|
|
36
|
+
<article class="panel"><h3>Activation steps & health</h3><div class="timeline"><div @each="step in current()?.transitions||[]"><span>{{step.trafficPercent==null?'—':step.trafficPercent+'%'}}</span><b>{{step.toStatus}}</b><p>{{step.message}}</p><time>{{new Date(step.createdAt).toLocaleString()}}</time></div></div></article>
|
|
37
|
+
<article class="panel"><h3>Strategy capability</h3><div class="strategy-grid"><div @each="cap in current()?.capabilities||[]" :class="cap.supported?'supported':'unsupported'"><b>{{cap.strategy}}</b><span>{{cap.supported?'Available':'Unavailable'}}</span><p>{{cap.explanation}}</p><small>{{cap.capacityMultiplier}}× capacity · {{cap.costImpact}} cost</small><small>{{cap.rollback}}</small></div></div></article>
|
|
38
|
+
@if(canOperate)<form class="panel promotion" @show="['active','superseded'].includes(current()?.status)" @submit="promoteRelease($event)"><h3>Promote without rebuild</h3><p>The exact digest above is reused. Only target config, strategy, and approval policy may change.</p><label>Target<select required :value="promote().targetResourceId" @change="promote.set({...promote(),targetResourceId:$event.target.value})"><option value="">Choose environment / resource…</option><option @each="resource in data().resources.filter(v=>v.id!==current()?.resourceId)" :value="resource.id">{{data().environments.find(e=>e.id===resource.environmentId)?.name}} / {{resource.name}}</option></select></label><label>Target config (non-secret JSON)<textarea rows="5" :value="promote().config" @input="promote.set({...promote(),config:$event.target.value})"></textarea></label><label>Strategy<select :value="promote().strategy" @change="promote.set({...promote(),strategy:$event.target.value})"><option value="">Keep {{current()?.strategy}}</option><option @each="cap in current()?.capabilities.filter(v=>v.supported)||[]" :value="cap.strategy">{{cap.strategy}}</option></select></label><label class="check"><input type="checkbox" :checked="promote().approvalRequired" @change="promote.set({...promote(),approvalRequired:$event.target.checked})"> Require target approval</label><button class="btn" type="submit">Preview and promote exact artifact</button></form>@endif
|
|
39
|
+
</section></div></main><style>.releases-wrap{max-width:1500px}.page,.hero,.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}.toolbar{display:flex;gap:12px}.toolbar label,.promotion label{display:grid;gap:5px}.release-layout{display:grid;grid-template-columns:340px minmax(0,1fr);gap:14px;align-items:start}.release-list{padding:7px;max-height:1100px;overflow:auto}.release-row{width:100%;display:grid;grid-template-columns:auto 1fr;text-align:left;gap:4px 8px;padding:12px;background:transparent;color:inherit;border:0;border-bottom:1px solid var(--line);cursor:pointer}.release-row:hover,.release-row.selected{background:var(--bg3)}.release-row small,.release-row code,.release-row time{grid-column:2;color:var(--txt3);font-size:11px}.detail{display:grid;gap:12px}.hero h2{margin:8px 0 3px}.hero p{margin:0;color:var(--txt3)}.facts dl{display:grid;grid-template-columns:1fr 1fr;gap:10px}.facts dt{color:var(--txt3);font-size:10px}.facts dd{margin:3px 0;overflow-wrap:anywhere}.change-grid,.strategy-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.change-grid span,.strategy-grid>div{border:1px solid var(--line);padding:10px;border-radius:8px}.strategy-grid>div{display:grid;gap:6px}.strategy-grid .unsupported{opacity:.6}.strategy-grid p{margin:0;font-size:12px}.strategy-grid small{color:var(--txt3)}.timeline>div{display:grid;grid-template-columns:55px 110px 1fr auto;gap:10px;padding:10px 0;border-bottom:1px solid var(--line);align-items:center}.timeline p{margin:0}.timeline time{color:var(--txt3);font-size:11px}.promotion{display:grid;gap:10px}.check{display:flex!important;align-items:center}.empty span{display:block;color:var(--txt3);margin-top:5px}@media(max-width:1050px){.release-layout{grid-template-columns:1fr}.release-list{max-height:360px}.strategy-grid{grid-template-columns:1fr 1fr}}@media(max-width:650px){.stats,.facts dl,.change-grid,.strategy-grid{grid-template-columns:1fr 1fr}.toolbar,.page,.hero{display:grid}.timeline>div{grid-template-columns:45px 1fr}.timeline p,.timeline time{grid-column:2}}@media(max-width:420px){.stats,.facts dl,.change-grid,.strategy-grid{grid-template-columns:1fr}}</style></body></html>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
let __d = null
|
|
3
|
+
try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
|
|
4
|
+
const viewerCapabilities = __d?.viewer?.capabilities ?? []
|
|
5
|
+
const canOperate = viewerCapabilities.length === 0 || viewerCapabilities.includes('runtime:restart')
|
|
6
|
+
const canElevated = viewerCapabilities.length === 0 || viewerCapabilities.includes('runtime:terminal')
|
|
7
|
+
</script>
|
|
8
|
+
<script client>
|
|
9
|
+
import { usePolling } from '../../functions/usePolling.ts'
|
|
10
|
+
|
|
11
|
+
const params = new URLSearchParams(location.search)
|
|
12
|
+
const inventory = state({ generatedAt: '', workloads: [], sources: [], degraded: false })
|
|
13
|
+
const query = state(params.get('search') || '')
|
|
14
|
+
const provider = state(params.get('provider') || '')
|
|
15
|
+
const status = state(params.get('status') || '')
|
|
16
|
+
const serviceFilter = state(params.get('service') || '')
|
|
17
|
+
const selectedId = state('')
|
|
18
|
+
const logs = state([])
|
|
19
|
+
const logsBusy = state(false)
|
|
20
|
+
const streamState = state('idle')
|
|
21
|
+
const streamSession = state(null)
|
|
22
|
+
const operationBusy = state(false)
|
|
23
|
+
const message = state('')
|
|
24
|
+
const typed = state('')
|
|
25
|
+
const replicas = state('1')
|
|
26
|
+
const execCommand = state('')
|
|
27
|
+
const filePath = state('')
|
|
28
|
+
const uploadBase64 = state('')
|
|
29
|
+
const uploadName = state('')
|
|
30
|
+
const fileBusy = state(false)
|
|
31
|
+
const columns = state({ replicas: true, image: true, resources: true, age: true })
|
|
32
|
+
const selected = derived(() => inventory().workloads.find(item => item.id === selectedId()) || null)
|
|
33
|
+
const sources = derived(() => inventory().sources || [])
|
|
34
|
+
const visible = derived(() => inventory().workloads.filter(item => {
|
|
35
|
+
const text = `${item.name} ${item.id} ${item.links?.service || ''} ${item.image || ''} ${Object.values(item.tags || {}).join(' ')}`.toLowerCase()
|
|
36
|
+
return (!query() || text.includes(query().toLowerCase())) && (!provider() || item.provider === provider()) && (!status() || item.status === status()) && (!serviceFilter() || item.links?.service === serviceFilter())
|
|
37
|
+
}))
|
|
38
|
+
function tone(value) { return value === 'running' ? 'ok' : ['failed', 'degraded'].includes(value) ? 'bad' : 'warn' }
|
|
39
|
+
function bytes(value) { if (value == null) return '—'; const units = ['B','KB','MB','GB','TB']; let size = value; let unit = 0; while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit++ } return `${size.toFixed(unit ? 1 : 0)} ${units[unit]}` }
|
|
40
|
+
function age(value) { if (value == null) return '—'; if (value < 60) return `${value}s`; if (value < 3600) return `${Math.floor(value / 60)}m`; if (value < 86400) return `${Math.floor(value / 3600)}h`; return `${Math.floor(value / 86400)}d` }
|
|
41
|
+
function replicaLabel(item) { return item.desiredReplicas == null ? '—' : `${item.runningReplicas || 0}/${item.desiredReplicas}` }
|
|
42
|
+
function capability(item, action) { return item.capabilities?.[action] || { supported: false, reason: 'Provider capability is unavailable.' } }
|
|
43
|
+
function syncUrl() { const url = new URL(location.href); for (const [key, value] of [['search', query()], ['provider', provider()], ['status', status()], ['service', serviceFilter()]]) value ? url.searchParams.set(key, value) : url.searchParams.delete(key); history.replaceState(null, '', url) }
|
|
44
|
+
function setFilter(signal, value) { signal.set(value); syncUrl() }
|
|
45
|
+
async function load() {
|
|
46
|
+
const response = await fetch('/api/runtime/workloads' + location.search)
|
|
47
|
+
const body = await response.json()
|
|
48
|
+
if (!response.ok) throw new Error(body.error || 'Workloads could not be loaded.')
|
|
49
|
+
inventory.set(body)
|
|
50
|
+
if (selectedId() && !body.workloads.some(item => item.id === selectedId())) selectedId.set('')
|
|
51
|
+
}
|
|
52
|
+
const { pollingState, pollingError, pollingLabel, lastUpdatedAt, refresh } = usePolling(load, 10000)
|
|
53
|
+
let eventStream = null
|
|
54
|
+
function disconnectStream() { eventStream?.close(); eventStream = null; streamState.set('idle'); streamSession.set(null) }
|
|
55
|
+
function open(item) { disconnectStream(); selectedId.set(item.id); logs.set([]); message.set(''); typed.set(''); replicas.set(String(item.desiredReplicas ?? 1)); filePath.set(item.mounts?.[0]?.target || `/var/www/${item.links?.project || ''}/${item.links?.service || ''}`) }
|
|
56
|
+
function close() { disconnectStream(); selectedId.set(''); logs.set([]); message.set(''); typed.set('') }
|
|
57
|
+
async function readLogs() {
|
|
58
|
+
const item = selected(); if (!item || logsBusy()) return; logsBusy.set(true); message.set('')
|
|
59
|
+
try { const response = await fetch('/api/runtime/logs?workload=' + encodeURIComponent(item.id) + '&limit=300'); const body = await response.json(); if (!response.ok) throw new Error(body.error || 'Logs could not be loaded.'); logs.set(body.lines || []) }
|
|
60
|
+
catch (error) { message.set(error.message || String(error)) } finally { logsBusy.set(false) }
|
|
61
|
+
}
|
|
62
|
+
async function run(action) {
|
|
63
|
+
const item = selected(); if (!item || operationBusy()) return; operationBusy.set(true); message.set('')
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch('/api/runtime/operations', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ workloadId: item.id, action, confirm: typed(), replicas: action === 'scale' ? Number(replicas()) : undefined }) })
|
|
66
|
+
const body = await response.json(); if (!response.ok || !body.ok) throw new Error(body.error || 'Operation failed.')
|
|
67
|
+
message.set(`${action} accepted as ${body.controlPlaneOperation?.id || 'a durable operation'}.`); typed.set(''); await refresh()
|
|
68
|
+
} catch (error) { message.set(error.message || String(error)) } finally { operationBusy.set(false) }
|
|
69
|
+
}
|
|
70
|
+
function connectStream(session, streamUrl) {
|
|
71
|
+
disconnectStream(); streamSession.set(session); streamState.set('connecting')
|
|
72
|
+
eventStream = new EventSource(streamUrl)
|
|
73
|
+
eventStream.onopen = () => streamState.set('live')
|
|
74
|
+
const append = event => { const chunk = JSON.parse(event.data); logs.set([...logs(), { timestamp: chunk.at, message: chunk.data, stream: chunk.stream }].slice(-1000)); streamState.set('live') }
|
|
75
|
+
eventStream.addEventListener('stdout', append); eventStream.addEventListener('stderr', append); eventStream.addEventListener('system', append)
|
|
76
|
+
eventStream.addEventListener('reset', event => { const info = JSON.parse(event.data); logs.set([{ message: `[${info.droppedBytes} buffered bytes were dropped; stream resumed at the newest output]`, stream: 'system' }]) })
|
|
77
|
+
eventStream.addEventListener('state', event => { const info = JSON.parse(event.data); streamState.set(info.state); eventStream?.close(); eventStream = null; if (info.error) message.set(info.error) })
|
|
78
|
+
eventStream.onerror = () => { if (eventStream) streamState.set('reconnecting') }
|
|
79
|
+
}
|
|
80
|
+
async function startLogFollow() {
|
|
81
|
+
const item = selected(); if (!item || streamState() === 'live') return; logs.set([]); message.set('')
|
|
82
|
+
try { const response = await fetch('/api/runtime/log-sessions', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ workloadId: item.id }) }); const body = await response.json(); if (!response.ok) throw new Error(body.error || 'Log follow could not start.'); connectStream(body.session, body.streamUrl) }
|
|
83
|
+
catch (error) { message.set(error.message || String(error)); streamState.set('failed') }
|
|
84
|
+
}
|
|
85
|
+
async function startExec(preset) {
|
|
86
|
+
const item = selected(); if (!item || typed() !== item.name) return; logs.set([]); message.set('')
|
|
87
|
+
try { const response = await fetch('/api/runtime/exec-sessions', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ workloadId: item.id, preset, command: preset ? undefined : execCommand(), confirm: typed() }) }); const body = await response.json(); if (!response.ok) throw new Error(body.error || 'Exec session could not start.'); connectStream(body.session, body.streamUrl); typed.set('') }
|
|
88
|
+
catch (error) { message.set(error.message || String(error)); streamState.set('failed') }
|
|
89
|
+
}
|
|
90
|
+
async function stopStream() {
|
|
91
|
+
const session = streamSession(); const item = selected(); if (!session || !item) return
|
|
92
|
+
const path = session.kind === 'exec' ? '/api/runtime/exec-sessions' : '/api/runtime/log-sessions'
|
|
93
|
+
await fetch(path, { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: session.id, workloadId: item.id }) }).catch(() => null)
|
|
94
|
+
disconnectStream()
|
|
95
|
+
}
|
|
96
|
+
async function chooseUpload(event) {
|
|
97
|
+
const file = event.target.files?.[0]; if (!file) return
|
|
98
|
+
if (file.size > 49152) { message.set('Uploads are limited to 48 KB.'); event.target.value = ''; return }
|
|
99
|
+
const bytes = new Uint8Array(await file.arrayBuffer()); let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte)
|
|
100
|
+
uploadBase64.set(btoa(binary)); uploadName.set(file.name); message.set(`${file.name} is ready to upload.`)
|
|
101
|
+
}
|
|
102
|
+
async function fileRequest(action) {
|
|
103
|
+
const item = selected(); if (!item || typed() !== item.name || fileBusy()) return; fileBusy.set(true); message.set('')
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(`/api/runtime/files/${action}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ workloadId: item.id, path: filePath(), contentBase64: action === 'write' ? uploadBase64() : undefined, confirm: typed() }) })
|
|
106
|
+
const body = await response.json(); if (!response.ok || !body.ok) throw new Error(body.error || `File ${action} failed.`)
|
|
107
|
+
if (action === 'read') { const binary = atob(body.contentBase64 || ''); const bytes = Uint8Array.from(binary, char => char.charCodeAt(0)); const url = URL.createObjectURL(new Blob([bytes])); const link = document.createElement('a'); link.href = url; link.download = filePath().split('/').pop() || 'download'; link.click(); URL.revokeObjectURL(url); message.set(body.truncated ? `Downloaded the first 48 KB of ${body.size} bytes.` : `Downloaded ${body.size} bytes.`) }
|
|
108
|
+
else { message.set(`Uploaded ${body.size} bytes atomically.`); uploadBase64.set(''); uploadName.set(''); typed.set('') }
|
|
109
|
+
} catch (error) { message.set(error.message || String(error)) } finally { fileBusy.set(false) }
|
|
110
|
+
}
|
|
111
|
+
</script>
|
|
112
|
+
<!DOCTYPE html>
|
|
113
|
+
<html lang="en" data-theme="dark">
|
|
114
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Workloads' })</head>
|
|
115
|
+
<body>
|
|
116
|
+
@include('../partials/nav', { active: 'workloads', env: 'production' })
|
|
117
|
+
<main class="workload-wrap wrap" id="main-content">
|
|
118
|
+
<div class="crumbs">Operations <span class="sep">/</span> Workloads</div>
|
|
119
|
+
<header class="page"><div><h1>Workloads</h1><p>One live inventory for services, containers, tasks, and functions—with explicit provider capabilities and release context.</p></div><div class="freshness"><span class="pill" :class="pollingState() === 'live' ? 'ok' : 'warn'">{{ pollingLabel() }}</span><small>{{ lastUpdatedAt() ? lastUpdatedAt().toLocaleTimeString() : 'connecting' }}</small><button class="btn ghost sm" type="button" @click="refresh()">Refresh</button></div></header>
|
|
120
|
+
<div class="bad notice" role="status" @show="!!pollingError()">{{ pollingError() }}</div>
|
|
121
|
+
<section class="source-strip" aria-label="Runtime discovery sources"><template :for="item in sources()"><span :class="item.status === 'fresh' ? 'fresh' : 'degraded'"><b>{{ item.id }}</b> {{ item.status }} · {{ item.itemCount }}<small @show="!!item.message">{{ item.message }}</small></span></template></section>
|
|
122
|
+
<section class="panel workload-toolbar">
|
|
123
|
+
<label class="search-label">Search<input type="search" placeholder="Name, ID, image, tag…" :value="query()" @input="setFilter(query, $event.target.value)"></label>
|
|
124
|
+
<label>Provider<select :value="provider()" @change="setFilter(provider, $event.target.value)"><option value="">All</option><option value="systemd">systemd</option><option value="docker">docker</option><option value="ecs">ecs</option><option value="lambda">lambda</option></select></label>
|
|
125
|
+
<label>Status<select :value="status()" @change="setFilter(status, $event.target.value)"><option value="">All</option><option value="running">running</option><option value="starting">starting</option><option value="degraded">degraded</option><option value="stopping">stopping</option><option value="stopped">stopped</option><option value="failed">failed</option><option value="unknown">unknown</option></select></label>
|
|
126
|
+
<details><summary>Columns</summary><div class="column-menu"><label><input type="checkbox" :checked="columns().replicas" @change="columns.set({...columns(), replicas: $event.target.checked})">Replicas</label><label><input type="checkbox" :checked="columns().image" @change="columns.set({...columns(), image: $event.target.checked})">Image/runtime</label><label><input type="checkbox" :checked="columns().resources" @change="columns.set({...columns(), resources: $event.target.checked})">Resources</label><label><input type="checkbox" :checked="columns().age" @change="columns.set({...columns(), age: $event.target.checked})">Age</label></div></details>
|
|
127
|
+
<button class="btn ghost sm" type="button" @show="!!serviceFilter()" @click="setFilter(serviceFilter, '')">Clear service: {{ serviceFilter() }}</button>
|
|
128
|
+
</section>
|
|
129
|
+
<div class="workload-layout" :class="{ inspecting: !!selected() }">
|
|
130
|
+
<section class="panel workload-list">
|
|
131
|
+
<div class="workload-table-wrap"><table class="workload-table"><thead><tr><th scope="col">Workload</th><th scope="col">Status</th><th scope="col">Provider</th><th scope="col" @show="columns().replicas">Replicas</th><th scope="col" @show="columns().image">Image / runtime</th><th scope="col" @show="columns().resources">Memory</th><th scope="col" @show="columns().age">Age</th></tr></thead><tbody><template :for="item in visible()"><tr class="workload-row" :class="{ selected: item.id === selectedId() }" tabindex="0" @click="open(item)" @keydown.enter="open(item)"><td><b>{{ item.displayName || item.name }}</b><small>{{ item.links?.service || item.kind }} · {{ item.id }}</small></td><td><span class="pill" :class="tone(item.status)">{{ item.status }}</span><small>{{ item.rawStatus || 'normalized' }}</small></td><td><span class="tag">{{ item.provider }}</span><small>{{ item.runtime || item.kind }}</small></td><td @show="columns().replicas">{{ replicaLabel(item) }}</td><td class="mono" @show="columns().image">{{ item.image || item.runtime || '—' }}</td><td @show="columns().resources">{{ bytes(item.resources?.memoryBytes || item.resources?.memoryLimitBytes) }}</td><td @show="columns().age">{{ age(item.ageSeconds) }}</td></tr></template></tbody></table></div>
|
|
132
|
+
<div class="empty" @show="pollingState() !== 'loading' && visible().length === 0"><strong>No matching workloads</strong><span>Change the filters, or inspect degraded sources above for discovery guidance.</span></div>
|
|
133
|
+
</section>
|
|
134
|
+
<aside class="panel workload-detail" @show="!!selected()"><button class="detail-close" type="button" aria-label="Close workload detail" @click="close()">×</button><template :if="selected()"><div class="detail-content">
|
|
135
|
+
<span class="tag">{{ selected().provider }} · {{ selected().kind }}</span><h2>{{ selected().displayName || selected().name }}</h2><div class="detail-meta"><span class="pill" :class="tone(selected().status)">{{ selected().status }}</span><span class="mono">{{ selected().id }}</span></div>
|
|
136
|
+
<nav class="context-links" aria-label="Related resources"><a @show="!!selected().links?.service" :href="'/server/sites?service=' + encodeURIComponent(selected().links.service)">Service {{ selected().links?.service }}</a><a @show="!!selected().links?.release" :href="'/operations/releases?release=' + encodeURIComponent(selected().links.release)">Release {{ selected().links?.release }}</a><span @show="!!selected().links?.server">Server {{ selected().links?.server }}</span></nav>
|
|
137
|
+
<h3>Lifecycle</h3>@if (canOperate)<div class="capability-grid"><button class="btn ghost sm" type="button" :disabled="operationBusy() || typed() !== selected().name || !capability(selected(), 'start').supported" :title="capability(selected(), 'start').supported ? `start ${selected().name}` : capability(selected(), 'start').reason" @click="run('start')">start</button><button class="btn danger sm" type="button" :disabled="operationBusy() || typed() !== selected().name || !capability(selected(), 'stop').supported" :title="capability(selected(), 'stop').supported ? `stop ${selected().name}` : capability(selected(), 'stop').reason" @click="run('stop')">stop</button><button class="btn ghost sm" type="button" :disabled="operationBusy() || typed() !== selected().name || !capability(selected(), 'restart').supported" :title="capability(selected(), 'restart').supported ? `restart ${selected().name}` : capability(selected(), 'restart').reason" @click="run('restart')">restart</button><button class="btn ghost sm" type="button" :disabled="operationBusy() || typed() !== selected().name || !capability(selected(), 'redeploy').supported" :title="capability(selected(), 'redeploy').supported ? `redeploy ${selected().name}` : capability(selected(), 'redeploy').reason" @click="run('redeploy')">redeploy</button><button class="btn ghost sm" type="button" :disabled="operationBusy() || typed() !== selected().name || !capability(selected(), 'scale').supported" :title="capability(selected(), 'scale').supported ? `scale ${selected().name}` : capability(selected(), 'scale').reason" @click="run('scale')">scale</button></div><div class="scale-row" @show="capability(selected(), 'scale').supported"><label>Desired replicas<input type="number" min="0" max="10000" :value="replicas()" @input="replicas.set($event.target.value)"></label></div><label class="confirm-label">Type <b>{{ selected().name }}</b> to run a mutation<input :value="typed()" @input="typed.set($event.target.value)" autocomplete="off"></label>@else<p class="note">Your current role can inspect this workload but cannot mutate it.</p>@endif<p class="notice" role="status" @show="!!message()">{{ message() }}</p>
|
|
138
|
+
<details open><summary>Replicas / processes ({{ selected().replicas?.length || 0 }})</summary><div class="detail-list"><template :for="replica in selected().replicas || []"><div><b>{{ replica.name || replica.id }}</b><span>{{ replica.status }} · PID {{ replica.pid || '—' }} · restarts {{ replica.restartCount || 0 }}</span></div></template><span @show="!(selected().replicas || []).length">Provider-managed or no active replica.</span></div></details>
|
|
139
|
+
<details><summary>Networks ({{ selected().networks?.length || 0 }})</summary><pre>{{ JSON.stringify(selected().networks || [], null, 2) }}</pre></details><details><summary>Mounts ({{ selected().mounts?.length || 0 }})</summary><pre>{{ JSON.stringify(selected().mounts || [], null, 2) }}</pre></details><details><summary>Redacted configuration</summary><pre>{{ JSON.stringify(selected().config || {}, null, 2) }}</pre></details><details><summary>Provider capabilities</summary><div class="detail-list"><template :for="entry in Object.entries(selected().capabilities || {})"><div><b>{{ entry[0] }}</b><span>{{ entry[1].supported ? 'supported' : entry[1].reason }}</span></div></template></div></details>
|
|
140
|
+
@if (canElevated)<details @show="capability(selected(), 'exec').supported"><summary>Elevated diagnostics and exec</summary><p class="note">Target: <b>{{ selected().name }}</b> on {{ selected().links?.server || selected().provider }}. Presets are read-only; free-form access is container-only.</p><div class="capability-grid"><button class="btn ghost sm" type="button" :disabled="typed() !== selected().name || streamState() === 'live'" @click="startExec('process')">Process status</button><button class="btn ghost sm" type="button" :disabled="typed() !== selected().name || streamState() === 'live'" @click="startExec('sockets')">Listening sockets</button><button class="btn ghost sm" type="button" :disabled="typed() !== selected().name || streamState() === 'live'" @click="startExec('filesystem')">Filesystem usage</button></div><label class="confirm-label" @show="selected().provider === 'docker'">Container command<input :value="execCommand()" @input="execCommand.set($event.target.value)" placeholder="id"></label><button class="btn danger sm" type="button" @show="selected().provider === 'docker'" :disabled="typed() !== selected().name || !execCommand().trim() || streamState() === 'live'" @click="startExec('')">Run container command</button></details><details @show="capability(selected(), 'files').supported"><summary>Scoped file transfer</summary><p class="note">Only service-owned roots and discovered mount targets are allowed. Transfers are capped at 48 KB and uploads replace files atomically with mode 0600.</p><label class="confirm-label">Absolute path<input :value="filePath()" @input="filePath.set($event.target.value)" placeholder="/var/www/project/service/file.txt"></label><label class="confirm-label">Upload file<input type="file" @change="chooseUpload($event)"></label><small @show="!!uploadName()">Ready: {{ uploadName() }}</small><div class="capability-grid"><button class="btn ghost sm" type="button" :disabled="typed() !== selected().name || !filePath() || fileBusy()" @click="fileRequest('read')">Download</button><button class="btn danger sm" type="button" :disabled="typed() !== selected().name || !filePath() || !uploadBase64() || fileBusy()" @click="fileRequest('write')">Upload and replace</button></div></details>@endif
|
|
141
|
+
<div class="logs-head"><h3>Logs <small>{{ streamState() }}</small></h3><div class="capability-grid"><button class="btn ghost sm" type="button" :disabled="logsBusy() || !capability(selected(), 'logs').supported" :title="capability(selected(), 'logs').reason || ''" @click="readLogs()">{{ logsBusy() ? 'Loading…' : 'Load logs' }}</button><button class="btn ghost sm" type="button" :disabled="streamState() === 'live' || !capability(selected(), 'logs').supported" @click="startLogFollow()">Follow</button><button class="btn danger sm" type="button" :disabled="!streamSession()" @click="stopStream()">Close stream</button></div></div><div class="logs" aria-live="polite"><template :for="line in logs()"><div :class="line.stream === 'stderr' ? 'bad' : ''"><span class="ts">{{ line.timestamp || '—' }}</span> {{ line.message }}</div></template><span @show="!logs().length">No log lines loaded.</span></div>
|
|
142
|
+
</div></template></aside>
|
|
143
|
+
</div>
|
|
144
|
+
</main>
|
|
145
|
+
<style>
|
|
146
|
+
.workload-wrap{max-width:1540px}.freshness{display:flex;gap:8px;align-items:center;color:var(--txt3)}.source-strip{display:flex;gap:8px;overflow:auto;margin-bottom:12px}.source-strip>span{display:grid;min-width:max-content;padding:8px 10px;border:1px solid var(--line);border-radius:9px;color:var(--txt3);font-size:11px}.source-strip .fresh{border-color:color-mix(in srgb,var(--ok) 30%,var(--line))}.source-strip .degraded{border-color:color-mix(in srgb,var(--bad) 35%,var(--line))}.source-strip small{max-width:360px;white-space:normal;margin-top:3px}.workload-toolbar{display:flex;align-items:end;gap:10px;margin-bottom:14px;padding:14px}.workload-toolbar>label{display:grid;gap:5px;color:var(--txt3);font-size:11px}.workload-toolbar input,.workload-toolbar select,.confirm-label input,.scale-row input{background:var(--bg2);border:1px solid var(--line);border-radius:8px;color:var(--txt);padding:8px 10px}.search-label{flex:1;min-width:230px}.column-menu{position:absolute;z-index:5;display:grid;gap:8px;margin-top:6px;padding:12px;background:var(--panel);border:1px solid var(--line);border-radius:9px}.workload-layout{display:grid;grid-template-columns:minmax(0,1fr);gap:14px}.workload-layout.inspecting{grid-template-columns:minmax(650px,1.5fr) minmax(360px,.7fr)}.workload-list{padding:0;overflow:hidden}.workload-table-wrap{overflow:auto}.workload-table{min-width:900px}.workload-row{cursor:pointer}.workload-row.selected{background:color-mix(in srgb,var(--accent) 9%,transparent)}.workload-row td small{display:block;max-width:330px;margin-top:4px;color:var(--txt3);font-size:10px;overflow:hidden;text-overflow:ellipsis}.workload-detail{position:sticky;top:14px;align-self:start;max-height:calc(100vh - 28px);overflow:auto;padding:20px}.detail-close{float:right;border:0;background:transparent;color:var(--txt3);font-size:24px;cursor:pointer}.detail-meta,.context-links,.capability-grid,.logs-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.detail-meta .mono{font-size:10px;color:var(--txt3);overflow-wrap:anywhere}.context-links{margin:12px 0}.context-links>*{font-size:11px}.capability-grid{margin:8px 0}.confirm-label,.scale-row label{display:grid;gap:6px;margin:12px 0;color:var(--txt3);font-size:11px}.workload-detail details{border-top:1px solid var(--line);padding:12px 0}.workload-detail summary{cursor:pointer;font-weight:700}.workload-detail pre{max-height:260px;overflow:auto;margin-top:10px;background:#090c12;padding:10px;border-radius:8px;font-size:10px;white-space:pre-wrap}.detail-list{display:grid;gap:8px;margin-top:10px}.detail-list>div{display:grid;gap:2px}.detail-list span{color:var(--txt3);font-size:11px}.logs-head{justify-content:space-between}.logs{min-height:80px}.notice{padding:9px 10px}
|
|
147
|
+
@media(max-width:1100px){.workload-layout.inspecting{grid-template-columns:1fr}.workload-detail{position:static;max-height:none}}
|
|
148
|
+
@media(max-width:700px){.source-strip{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));overflow:visible}.source-strip>span{min-width:0;overflow-wrap:anywhere}.workload-toolbar{align-items:stretch;flex-direction:column}.workload-toolbar>*{width:100%}.workload-table{min-width:0}.workload-table thead{display:none}.workload-table tbody,.workload-row{display:block}.workload-row{padding:12px}.workload-row td{display:grid;grid-template-columns:100px 1fr;padding:5px 0;border:0}.workload-row td:before{color:var(--txt3);font-size:10px}.workload-row td:nth-child(1):before{content:'Workload'}.workload-row td:nth-child(2):before{content:'Status'}.workload-row td:nth-child(3):before{content:'Provider'}.workload-row td:nth-child(4):before{content:'Replicas'}.workload-row td:nth-child(5):before{content:'Image'}.workload-row td:nth-child(6):before{content:'Memory'}.workload-row td:nth-child(7):before{content:'Age'}}
|
|
149
|
+
</style>
|
|
150
|
+
</body>
|
|
151
|
+
</html>
|