@stacksjs/ts-cloud 0.7.56 → 0.7.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,186 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
let __d = null
|
|
3
|
+
try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
|
|
4
|
+
const mode = __d?.mode ?? 'server'
|
|
5
|
+
const project = __d?.project ?? { name: 'Project' }
|
|
6
|
+
const environment = __d?.environment ?? 'production'
|
|
7
|
+
</script>
|
|
8
|
+
<!DOCTYPE html>
|
|
9
|
+
<html lang="en" data-theme="dark">
|
|
10
|
+
<head>
|
|
11
|
+
@include('./partials/head', { title: 'ts-cloud - Git integrations' })
|
|
12
|
+
<style>
|
|
13
|
+
.source-hero { display:grid;grid-template-columns:minmax(0,1fr) auto;gap:24px;align-items:start }
|
|
14
|
+
.source-actions,.row-actions { display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end }
|
|
15
|
+
.source-stats { display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px }
|
|
16
|
+
.source-stat strong { display:block;font-size:1.8rem;margin-top:10px }
|
|
17
|
+
.source-layout { display:grid;grid-template-columns:minmax(0,1.5fr) minmax(320px,.8fr);gap:18px;align-items:start }
|
|
18
|
+
.source-stack,.connection-list,.repository-list,.binding-list,.webhook-list,.delivery-list { display:grid;gap:12px }
|
|
19
|
+
.source-sidebar { position:sticky;top:20px }
|
|
20
|
+
.connection,.repository,.binding,.webhook { border:1px solid var(--line);border-radius:11px;background:var(--panel);padding:16px;display:grid;gap:12px }
|
|
21
|
+
.connection-head,.repository-head,.binding-head,.webhook-head { display:flex;gap:12px;justify-content:space-between;align-items:flex-start }
|
|
22
|
+
.connection-title { display:flex;gap:10px;align-items:center }.provider-mark { display:grid;place-items:center;width:34px;height:34px;border-radius:9px;background:rgba(90,139,224,.12);color:var(--accent);font-weight:800;text-transform:uppercase }
|
|
23
|
+
.connection h3,.repository h3,.binding h3,.webhook h3 { margin:0;font-size:1rem }.connection p,.repository p,.binding p,.webhook p { margin:3px 0 0 }
|
|
24
|
+
.source-meta { display:flex;gap:7px;flex-wrap:wrap;align-items:center;color:var(--muted);font-size:.78rem }
|
|
25
|
+
.source-filters { display:grid;grid-template-columns:minmax(180px,1fr) minmax(150px,.35fr);gap:8px;margin-bottom:12px }
|
|
26
|
+
.source-filters input,.source-filters select,.source-dialog input,.source-dialog select,.source-dialog textarea { width:100%;min-width:0;background:rgba(255,255,255,.045);border:1px solid var(--panel-br);border-radius:8px;padding:9px 10px;color:var(--txt);font:inherit }
|
|
27
|
+
.source-filters input:focus,.source-filters select:focus,.source-dialog input:focus,.source-dialog select:focus,.source-dialog textarea:focus { outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(90,139,224,.12) }
|
|
28
|
+
.source-dialog { position:fixed;inset:50% auto auto 50%;transform:translate(-50%,-50%);margin:0;width:min(620px,calc(100vw - 32px));max-height:calc(100vh - 32px);overflow:auto;box-sizing:border-box;border:1px solid var(--line);border-radius:12px;background:#111722;color:inherit;padding:22px;box-shadow:0 24px 80px rgba(0,0,0,.55) }
|
|
29
|
+
.source-dialog::backdrop { background:rgba(0,0,0,.68) }.source-dialog form { display:grid;gap:12px }
|
|
30
|
+
.source-dialog label { display:grid;gap:6px;color:var(--txt3);font-size:.72rem;font-weight:750;letter-spacing:.04em }.source-dialog label :is(input,select,textarea) { color:var(--txt);font-weight:450;letter-spacing:0 }
|
|
31
|
+
.form-grid { display:grid;grid-template-columns:1fr 1fr;gap:10px }.check-grid { display:grid;grid-template-columns:1fr 1fr;gap:9px }.check-grid label { display:flex;align-items:center;gap:8px;font-size:.82rem }.check-grid input { width:auto }
|
|
32
|
+
.dialog-actions { display:flex;gap:8px;justify-content:flex-end }.status-line { min-height:1.3em;color:var(--muted) }
|
|
33
|
+
.empty-source { padding:26px;text-align:center;border:1px dashed var(--line);border-radius:10px;color:var(--muted) }
|
|
34
|
+
.delivery { display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:8px;align-items:center;font-size:.78rem;border-top:1px solid var(--line);padding-top:8px }
|
|
35
|
+
.impact-list { display:grid;gap:8px;max-height:240px;overflow:auto }.impact { padding:10px;border:1px solid var(--line);border-radius:8px }
|
|
36
|
+
.secret-note { border-left:2px solid #e4c35e;padding-left:10px;color:var(--muted) }
|
|
37
|
+
@media (max-width:900px) { .source-layout,.source-hero { grid-template-columns:1fr }.source-sidebar { position:static }.source-stats { grid-template-columns:1fr 1fr }.source-actions { justify-content:flex-start } }
|
|
38
|
+
@media (max-width:560px) { .source-stats,.source-filters,.form-grid,.check-grid { grid-template-columns:1fr }.connection-head,.repository-head,.binding-head,.webhook-head { display:grid }.row-actions { justify-content:flex-start } }
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
@include('./partials/nav', { active: 'integrations', name: project.name, env: environment })
|
|
43
|
+
<div class="wrap" id="main-content">
|
|
44
|
+
<div class="crumbs"><a href="{{ mode === 'serverless' ? '/serverless' : '/' }}">{{ project.name }}</a> <span class="sep">/</span> Git integrations</div>
|
|
45
|
+
<header class="page source-hero">
|
|
46
|
+
<div><h1>Git integrations</h1><p>Connect source once, bind repositories to applications, and deploy verified pushes or pull requests.</p></div>
|
|
47
|
+
<div class="source-actions"><button class="btn ghost" id="refresh-sources" type="button">Refresh</button><button class="btn" id="add-connection" type="button">Add connection</button></div>
|
|
48
|
+
</header>
|
|
49
|
+
|
|
50
|
+
<div class="section source-stats" aria-label="Git integration summary">
|
|
51
|
+
<div class="panel source-stat"><span class="note">Connections</span><strong id="connection-count">—</strong><small>Provider credentials</small></div>
|
|
52
|
+
<div class="panel source-stat"><span class="note">Repositories</span><strong id="repository-count">—</strong><small>Authorized sources</small></div>
|
|
53
|
+
<div class="panel source-stat"><span class="note">Auto deploy</span><strong id="binding-count">—</strong><small>Active bindings</small></div>
|
|
54
|
+
<div class="panel source-stat"><span class="note">Needs attention</span><strong id="unhealthy-count">—</strong><small>Connections or hooks</small></div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="section source-layout">
|
|
58
|
+
<main class="source-stack">
|
|
59
|
+
<section>
|
|
60
|
+
<div class="section-heading"><div><h2>Connections</h2><p class="note">Credentials stay encrypted and are never returned to this page.</p></div></div>
|
|
61
|
+
<div class="connection-list" id="connection-list"><div class="empty-source">Loading connections…</div></div>
|
|
62
|
+
</section>
|
|
63
|
+
<section>
|
|
64
|
+
<div class="section-heading"><div><h2>Repositories</h2><p class="note">Browse only repositories granted to a connection.</p></div></div>
|
|
65
|
+
<div class="source-filters"><input id="repository-search" type="search" placeholder="Search repositories…" aria-label="Search repositories"><select id="repository-connection" aria-label="Connection"><option value="">All connections</option></select></div>
|
|
66
|
+
<div class="repository-list" id="repository-list"><div class="empty-source">No repositories synchronized yet.</div></div>
|
|
67
|
+
</section>
|
|
68
|
+
</main>
|
|
69
|
+
<aside class="source-sidebar source-stack">
|
|
70
|
+
<section class="panel"><div class="section-heading"><div><h2>Application bindings</h2><p class="note">Branch, tag, root, and watched-path rules.</p></div></div><div class="binding-list" id="binding-list"><div class="note">No source bindings.</div></div></section>
|
|
71
|
+
<section class="panel"><div class="section-heading"><div><h2>Webhook delivery</h2><p class="note">Verified, replay-safe event history.</p></div></div><div class="webhook-list" id="webhook-list"><div class="note">No webhooks configured.</div></div></section>
|
|
72
|
+
</aside>
|
|
73
|
+
</div>
|
|
74
|
+
<p class="status-line" id="page-status" role="status"></p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<dialog class="source-dialog" id="connection-dialog">
|
|
78
|
+
<form id="connection-form">
|
|
79
|
+
<div><h2>Add Git connection</h2><p class="note">Request read-only repository metadata/content and webhook scopes whenever the provider permits it.</p></div>
|
|
80
|
+
<div class="form-grid"><label>Provider<select name="provider"><option value="github">GitHub App</option><option value="gitlab">GitLab</option><option value="bitbucket">Bitbucket</option><option value="gitea">Gitea</option><option value="generic_https">Generic HTTPS</option><option value="generic_ssh">Generic SSH</option></select></label><label>Connection name<input name="name" maxlength="80" required placeholder="Production GitHub"></label></div>
|
|
81
|
+
<div class="form-grid"><label>Host<input name="host" type="url" required value="https://github.com"></label><label>Owner / workspace<input name="owner" placeholder="acme"></label></div>
|
|
82
|
+
<div data-app-fields><div class="form-grid"><label>GitHub App ID<input name="appId" autocomplete="off"></label><label>Installation ID<input name="installationId" autocomplete="off"></label></div><label>GitHub App private key<textarea name="privateKey" rows="4" autocomplete="off" spellcheck="false"></textarea></label></div>
|
|
83
|
+
<div data-token-fields hidden><div class="form-grid"><label>Access token<input name="token" type="password" autocomplete="new-password"></label><label>Token username (optional)<input name="username" autocomplete="off"></label></div></div>
|
|
84
|
+
<div data-generic-fields hidden><div class="form-grid"><label>Repository name<input name="repositoryFullName" placeholder="acme/web"></label><label>Default branch<input name="defaultBranch" value="main"></label></div><label>Credential-free clone URL<input name="repositoryUrl" placeholder="https://git.example/acme/web.git"></label></div>
|
|
85
|
+
<div data-ssh-fields hidden><label>Deploy key name<input name="deployKeyName" maxlength="80" placeholder="Readonly production"></label><label>Public key<textarea name="publicKey" rows="2" autocomplete="off" spellcheck="false" placeholder="ssh-ed25519 …"></textarea></label><label>Private key<textarea name="deployPrivateKey" rows="4" autocomplete="off" spellcheck="false" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"></textarea></label><div class="form-grid"><label>SSH host<input name="sshHost" placeholder="git.example"></label><label>Pinned host key<textarea name="hostKey" rows="2" autocomplete="off" spellcheck="false" placeholder="ssh-ed25519 …"></textarea></label></div></div>
|
|
86
|
+
<p class="secret-note">Tokens and private keys are encrypted at rest. Clone URLs containing credentials are rejected.</p>
|
|
87
|
+
<div class="dialog-actions"><button class="btn ghost" type="button" data-close>Cancel</button><button class="btn" type="submit">Connect</button></div><p class="status-line" id="connection-status" role="status"></p>
|
|
88
|
+
</form>
|
|
89
|
+
</dialog>
|
|
90
|
+
|
|
91
|
+
<dialog class="source-dialog" id="binding-dialog">
|
|
92
|
+
<form id="binding-form">
|
|
93
|
+
<div><h2>Bind repository</h2><p class="note" id="binding-repository"></p></div><input type="hidden" name="connectionId"><input type="hidden" name="repositoryId"><input type="hidden" name="repositoryFullName">
|
|
94
|
+
<div class="form-grid"><label>Application<select name="resourceId" required></select></label><label>Deploy branch<select name="defaultBranch" required><option>main</option></select></label></div>
|
|
95
|
+
<div class="form-grid"><label>Branch rule<input name="branchRule" placeholder="main or release/**"></label><label>Tag rule<input name="tagRule" placeholder="v*"></label></div>
|
|
96
|
+
<div class="form-grid"><label>Monorepo root<input name="monorepoRoot" value="." placeholder="apps/web"></label><label>SSH deploy key<select name="deployKeyId"><option value="">Provider credential / none</option></select></label></div>
|
|
97
|
+
<div class="form-grid"><label>Include paths<textarea name="includePaths" rows="3" placeholder="apps/web/** packages/shared/**"></textarea></label><label>Exclude paths<textarea name="excludePaths" rows="3" placeholder="**/*.md docs/**"></textarea></label></div>
|
|
98
|
+
<div class="form-grid"><label>Clone history<select name="cloneDepth"><option value="20">Shallow · 20 commits</option><option value="1">Shallow · latest commit</option><option value="">Full history</option></select></label><label>Event types<select name="events"><option value="push,pr">Push + pull request</option><option value="push">Push only</option><option value="pr">Pull request only</option></select></label></div>
|
|
99
|
+
<div class="check-grid"><label><input type="checkbox" name="submodules"> Include submodules</label><label><input type="checkbox" name="autoDeploy" checked> Auto-deploy pushes</label></div>
|
|
100
|
+
<div class="dialog-actions"><button class="btn ghost" type="button" data-close>Cancel</button><button class="btn" type="submit">Create binding</button></div><p class="status-line" id="binding-status" role="status"></p>
|
|
101
|
+
</form>
|
|
102
|
+
</dialog>
|
|
103
|
+
|
|
104
|
+
<dialog class="source-dialog" id="rotate-dialog"><form id="rotate-form"><div><h2>Rotate credential</h2><p class="note" id="rotate-name"></p></div><input type="hidden" name="id"><label>Replacement access token<input name="token" type="password" autocomplete="new-password"></label><label>Credential expiry (optional)<input name="expiresAt" type="datetime-local"></label><p class="secret-note">The old encrypted credential is replaced immediately. Test the connection after rotation.</p><div class="dialog-actions"><button class="btn ghost" type="button" data-close>Cancel</button><button class="btn" type="submit">Rotate</button></div><p class="status-line" id="rotate-status"></p></form></dialog>
|
|
105
|
+
|
|
106
|
+
<dialog class="source-dialog" id="disconnect-dialog"><form id="disconnect-form"><div><h2>Disconnect source</h2><p class="note">Dependent automatic deploys will be disabled; they never fall back to another credential.</p></div><input type="hidden" name="id"><div class="impact-list" id="impact-list"></div><div class="dialog-actions"><button class="btn ghost" type="button" data-close>Cancel</button><button class="btn danger" type="submit">Disconnect and disable</button></div><p class="status-line" id="disconnect-status"></p></form></dialog>
|
|
107
|
+
|
|
108
|
+
<script>
|
|
109
|
+
(() => {
|
|
110
|
+
const state = { data: null }
|
|
111
|
+
const status = document.querySelector('#page-status')
|
|
112
|
+
const connectionList = document.querySelector('#connection-list'), repositoryList = document.querySelector('#repository-list'), bindingList = document.querySelector('#binding-list'), webhookList = document.querySelector('#webhook-list')
|
|
113
|
+
const request = async (path, options = {}) => { const response = await fetch(path, { credentials:'same-origin', ...options }); const body = await response.json().catch(() => ({})); if (!response.ok) { const error = new Error(body.error || `Request failed (${response.status})`); error.stepUpRequired = body.stepUpRequired; throw error } return body }
|
|
114
|
+
const node = (tag, cls, text) => { const value = document.createElement(tag); if (cls) value.className = cls; if (text != null) value.textContent = text; return value }
|
|
115
|
+
const formatDate = value => value ? new Date(value).toLocaleString() : 'never'
|
|
116
|
+
const tone = value => ['healthy','active','enqueued','verified'].includes(value) ? 'ok' : ['degraded','expired','failed','rejected','invalid'].includes(value) ? 'bad' : 'warn'
|
|
117
|
+
const connection = id => state.data.connections.find(item => item.id === id)
|
|
118
|
+
const lines = value => String(value || '').split(/\r?\n|,/).map(item => item.trim()).filter(Boolean)
|
|
119
|
+
const api = (path, method, body) => request(path, { method, headers:{'content-type':'application/json'}, body:JSON.stringify(body) })
|
|
120
|
+
const setStatus = (element, message, bad = false) => { element.textContent = message; element.style.color = bad ? '#ff858c' : '' }
|
|
121
|
+
|
|
122
|
+
function badge(value) { const pill = node('span', `pill ${tone(value)}`, value); return pill }
|
|
123
|
+
function action(label, handler, cls = 'btn ghost sm') { const button = node('button', cls, label); button.type = 'button'; button.addEventListener('click', handler); return button }
|
|
124
|
+
function renderSummary() {
|
|
125
|
+
document.querySelector('#connection-count').textContent = state.data.connections.length
|
|
126
|
+
document.querySelector('#repository-count').textContent = state.data.repositories.length
|
|
127
|
+
document.querySelector('#binding-count').textContent = state.data.bindings.filter(item => item.status === 'active' && item.autoDeploy).length
|
|
128
|
+
document.querySelector('#unhealthy-count').textContent = state.data.connections.filter(item => !['healthy','pending'].includes(item.status)).length + state.data.webhooks.filter(item => item.status === 'degraded').length
|
|
129
|
+
}
|
|
130
|
+
function renderConnections() {
|
|
131
|
+
connectionList.replaceChildren()
|
|
132
|
+
if (!state.data.connections.length) { connectionList.append(node('div','empty-source','Connect GitHub, GitLab, Bitbucket, Gitea, or a generic Git host.')); return }
|
|
133
|
+
const select = document.querySelector('#repository-connection'); const previous = select.value; select.replaceChildren(new Option('All connections',''))
|
|
134
|
+
for (const item of state.data.connections) {
|
|
135
|
+
select.append(new Option(item.name,item.id))
|
|
136
|
+
const card=node('article','connection'), head=node('div','connection-head'), title=node('div','connection-title'), mark=node('span','provider-mark',item.provider.split('_')[0].slice(0,2)), copy=node('div')
|
|
137
|
+
copy.append(node('h3','',item.name),node('p','note',`${item.provider.replace('_',' ')} · ${item.host}${item.owner ? ` · ${item.owner}` : ''}`)); title.append(mark,copy); head.append(title,badge(item.status)); card.append(head)
|
|
138
|
+
const keys=state.data.deployKeys.filter(key=>key.connectionId===item.id);const meta=node('div','source-meta'); meta.append(node('span','',`Scopes: ${item.grantedScopes.join(', ') || 'not reported'}`),node('span','',`Tested: ${formatDate(item.lastTestedAt)}`),node('span','',`Synced: ${formatDate(item.lastSyncedAt)}`),node('span','',`Deploy keys: ${keys.length}`)); card.append(meta)
|
|
139
|
+
for(const key of keys){const keyRow=node('div','source-meta');keyRow.append(node('span','',`${key.name} · ${key.publicKeyFingerprint.slice(0,22)}…`),action('Revoke key',()=>revokeDeployKey(key),'btn danger ghost sm'));card.append(keyRow)}
|
|
140
|
+
if (item.healthMessage) card.append(node('p','note',item.healthMessage))
|
|
141
|
+
const actions=node('div','row-actions'); actions.append(action('Test',()=>connectionAction(item,'test')),action('Sync repositories',()=>syncRepositories(item)),action('Rotate',()=>openRotate(item)),action('Disconnect',()=>openDisconnect(item),'btn danger ghost sm')); card.append(actions); connectionList.append(card)
|
|
142
|
+
}
|
|
143
|
+
select.value = state.data.connections.some(item => item.id === previous) ? previous : ''
|
|
144
|
+
}
|
|
145
|
+
function renderRepositories() {
|
|
146
|
+
const search=document.querySelector('#repository-search').value.toLowerCase(), selected=document.querySelector('#repository-connection').value
|
|
147
|
+
repositoryList.replaceChildren(); const values=state.data.repositories.filter(item => (!selected || item.connectionId === selected) && item.fullName.toLowerCase().includes(search))
|
|
148
|
+
if (!values.length) { repositoryList.append(node('div','empty-source','No repositories match this view. Synchronize a hosted connection or add a manual generic repository.')); return }
|
|
149
|
+
for (const item of values) { const card=node('article','repository'),head=node('div','repository-head'),copy=node('div');copy.append(node('h3','',item.fullName),node('p','note',`${connection(item.connectionId)?.name || 'Connection'} · default ${item.defaultBranch}`));head.append(copy,badge(item.visibility));card.append(head);const meta=node('div','source-meta');meta.append(node('span','',item.archived?'Archived':'Active repository'),node('span','',`Synced ${formatDate(item.syncedAt)}`));card.append(meta);const actions=node('div','row-actions');actions.append(action('Browse branches',()=>browseBranches(item)),action('Bind application',()=>openBinding(item),'btn sm'),action('Configure webhook',()=>createWebhook(item)));card.append(actions);repositoryList.append(card) }
|
|
150
|
+
}
|
|
151
|
+
function renderBindings() {
|
|
152
|
+
bindingList.replaceChildren(); if (!state.data.bindings.length) { bindingList.append(node('div','note','No source bindings.')); return }
|
|
153
|
+
for (const item of state.data.bindings) { const card=node('article','binding'),head=node('div','binding-head'),copy=node('div');copy.append(node('h3','',item.repositoryFullName),node('p','note',`${item.branchRule || item.defaultBranch} · ${item.monorepoRoot}`));head.append(copy,badge(item.status));card.append(head);const meta=node('div','source-meta');meta.append(node('span','',item.autoDeploy?'Push deploy on':'Push deploy off'),node('span','',item.pullRequestPreviews?'PR previews on':'PR previews off'),node('span','',item.cloneDepth?`Depth ${item.cloneDepth}`:'Full clone'));card.append(meta);if(item.includePaths.length||item.excludePaths.length)card.append(node('p','note',`Watch ${item.includePaths.join(', ') || 'all'} · ignore ${item.excludePaths.join(', ') || 'none'}`));const actions=node('div','row-actions');actions.append(action(item.autoDeploy?'Pause auto-deploy':'Enable auto-deploy',()=>updateBinding(item,{autoDeploy:!item.autoDeploy})),action(item.status==='active'?'Disable':'Enable',()=>updateBinding(item,{status:item.status==='active'?'disabled':'active',disabledReason:item.status==='active'?'Disabled by operator':''})));card.append(actions);bindingList.append(card) }
|
|
154
|
+
}
|
|
155
|
+
function renderWebhooks() {
|
|
156
|
+
webhookList.replaceChildren(); if(!state.data.webhooks.length){webhookList.append(node('div','note','No webhooks configured.'));return}
|
|
157
|
+
for(const item of state.data.webhooks){const card=node('article','webhook'),head=node('div','webhook-head'),copy=node('div');copy.append(node('h3','',item.repositoryFullName),node('p','note',`${item.events.join(' + ')} · provider ${item.providerWebhookId || 'manual/pending'}`));head.append(copy,badge(item.status));card.append(head);if(item.healthMessage)card.append(node('p','note',item.healthMessage));const actions=node('div','row-actions');actions.append(action('Reconcile',()=>webhookAction(item,'PATCH')),action('Remove',()=>webhookAction(item,'DELETE'),'btn danger ghost sm'));card.append(actions);const deliveries=node('div','delivery-list');for(const delivery of (item.deliveries||[]).slice(0,5)){const row=node('div','delivery');row.append(badge(delivery.status),node('span','',`${delivery.event}${delivery.commitSha?` · ${delivery.commitSha.slice(0,8)}`:''}`),node('time','note',formatDate(delivery.receivedAt)));deliveries.append(row)}if(!item.deliveries?.length)deliveries.append(node('div','note','No deliveries yet.'));card.append(deliveries);webhookList.append(card)}
|
|
158
|
+
}
|
|
159
|
+
function render(){renderSummary();renderConnections();renderRepositories();renderBindings();renderWebhooks()}
|
|
160
|
+
async function load(){setStatus(status,'Loading integrations…');try{state.data=await request(`/api/sources${location.search}`);render();setStatus(status,'')}catch(error){setStatus(status,error.message,true)}}
|
|
161
|
+
async function connectionAction(item,actionName){setStatus(status,`${actionName==='test'?'Testing':'Updating'} ${item.name}…`);try{await api(`/api/sources/connections${location.search}`,'PATCH',{id:item.id,action:actionName});await load()}catch(error){setStatus(status,error.message,true)}}
|
|
162
|
+
async function syncRepositories(item){setStatus(status,`Synchronizing ${item.name}…`);try{await api(`/api/sources/repositories/sync${location.search}`,'POST',{connectionId:item.id});await load()}catch(error){setStatus(status,error.message,true)}}
|
|
163
|
+
async function browseBranches(repo){setStatus(status,`Loading branches for ${repo.fullName}…`);try{const query=new URLSearchParams({connectionId:repo.connectionId,repositoryId:repo.id,repository:repo.fullName,type:'branches'});const key=state.data.deployKeys.find(item=>item.connectionId===repo.connectionId);if(key)query.set('deployKeyId',key.id);const result=await request(`/api/sources/references?${query}`);setStatus(status,result.refs.length?`Branches: ${result.refs.map(item=>item.name).join(', ')}`:'No branches returned.')}catch(error){setStatus(status,error.message,true)}}
|
|
164
|
+
async function createWebhook(repo){setStatus(status,`Configuring webhook for ${repo.fullName}…`);try{const result=await api(`/api/sources/webhooks${location.search}`,'POST',{connectionId:repo.connectionId,repositoryId:repo.id,repositoryFullName:repo.fullName,events:['push','pull_request']});await load();setStatus(status,result.endpoint?`Webhook ready: ${result.endpoint}`:'Webhook saved. Configure a public base URL to reconcile it.')}catch(error){setStatus(status,error.message,true)}}
|
|
165
|
+
async function updateBinding(item,changes){try{await api(`/api/sources/bindings${location.search}`,'PATCH',{id:item.id,version:item.version,...changes});await load()}catch(error){setStatus(status,error.message,true)}}
|
|
166
|
+
async function webhookAction(item,method){setStatus(status,`${method==='PATCH'?'Reconciling':'Removing'} webhook…`);try{await api(`/api/sources/webhooks${location.search}`,method,{id:item.id});await load()}catch(error){setStatus(status,error.message,true)}}
|
|
167
|
+
async function revokeDeployKey(key){if(!confirm(`Revoke ${key.name}? Every binding using it will be disabled.`))return;setStatus(status,`Revoking ${key.name}…`);try{await api(`/api/sources/deploy-keys${location.search}`,'DELETE',{id:key.id});await load()}catch(error){setStatus(status,error.message,true)}}
|
|
168
|
+
|
|
169
|
+
const connectionDialog=document.querySelector('#connection-dialog'),connectionForm=document.querySelector('#connection-form')
|
|
170
|
+
function providerFields(){const provider=connectionForm.elements.provider.value;const defaults={github:'https://github.com',gitlab:'https://gitlab.com',bitbucket:'https://bitbucket.org',gitea:'https://gitea.com',generic_https:'https://git.example',generic_ssh:'https://git.example'};connectionForm.elements.host.value=defaults[provider];connectionForm.querySelector('[data-app-fields]').hidden=provider!=='github';connectionForm.querySelector('[data-token-fields]').hidden=provider==='github'||provider==='generic_ssh';connectionForm.querySelector('[data-generic-fields]').hidden=!provider.startsWith('generic_');connectionForm.querySelector('[data-ssh-fields]').hidden=provider!=='generic_ssh'}
|
|
171
|
+
document.querySelector('#add-connection').addEventListener('click',()=>{connectionForm.reset();providerFields();connectionDialog.showModal()});connectionForm.elements.provider.addEventListener('change',providerFields)
|
|
172
|
+
connectionForm.addEventListener('submit',async event=>{event.preventDefault();const out=document.querySelector('#connection-status'),data=Object.fromEntries(new FormData(connectionForm));data.authKind=data.provider==='github'?'app':data.provider==='generic_ssh'?'deploy_key':'access_token';setStatus(out,'Encrypting and saving connection…');try{await api(`/api/sources/connections${location.search}`,'POST',data);connectionForm.reset();connectionDialog.close();await load()}catch(error){setStatus(out,error.message,true)}})
|
|
173
|
+
|
|
174
|
+
const bindingDialog=document.querySelector('#binding-dialog'),bindingForm=document.querySelector('#binding-form')
|
|
175
|
+
async function openBinding(repo){bindingForm.reset();bindingForm.elements.connectionId.value=repo.connectionId;bindingForm.elements.repositoryId.value=repo.id;bindingForm.elements.repositoryFullName.value=repo.fullName;bindingForm.elements.monorepoRoot.value='.';document.querySelector('#binding-repository').textContent=repo.fullName;const resources=bindingForm.elements.resourceId;resources.replaceChildren();for(const item of state.data.resources)resources.append(new Option(item.name,item.id));const deployKeys=bindingForm.elements.deployKeyId;deployKeys.replaceChildren(new Option('Provider credential / none',''));const availableKeys=state.data.deployKeys.filter(item=>item.connectionId===repo.connectionId);for(const key of availableKeys)deployKeys.append(new Option(`${key.name} · ${key.host}`,key.id));if(connection(repo.connectionId)?.provider==='generic_ssh'&&availableKeys[0])deployKeys.value=availableKeys[0].id;const branches=bindingForm.elements.defaultBranch;branches.replaceChildren(new Option(repo.defaultBranch,repo.defaultBranch));bindingDialog.showModal();try{const query=new URLSearchParams({connectionId:repo.connectionId,repositoryId:repo.id,repository:repo.fullName,type:'branches'});if(deployKeys.value)query.set('deployKeyId',deployKeys.value);const result=await request(`/api/sources/references?${query}`);branches.replaceChildren(...result.refs.map(item=>new Option(item.name,item.name)));branches.value=result.refs.some(item=>item.name===repo.defaultBranch)?repo.defaultBranch:result.refs[0]?.name||repo.defaultBranch}catch(error){setStatus(document.querySelector('#binding-status'),`Branch discovery unavailable: ${error.message}`,true)}}
|
|
176
|
+
bindingForm.addEventListener('submit',async event=>{event.preventDefault();const out=document.querySelector('#binding-status'),data=Object.fromEntries(new FormData(bindingForm));data.includePaths=lines(data.includePaths);data.excludePaths=lines(data.excludePaths);data.submodules=bindingForm.elements.submodules.checked;data.autoDeploy=data.events!=='pr';data.pullRequestPreviews=data.events!=='push';data.cloneDepth=data.cloneDepth?Number(data.cloneDepth):undefined;setStatus(out,'Creating source binding…');try{await api(`/api/sources/bindings${location.search}`,'POST',data);bindingDialog.close();await load()}catch(error){setStatus(out,error.message,true)}})
|
|
177
|
+
|
|
178
|
+
const rotateDialog=document.querySelector('#rotate-dialog'),rotateForm=document.querySelector('#rotate-form');function openRotate(item){rotateForm.reset();rotateForm.elements.id.value=item.id;document.querySelector('#rotate-name').textContent=item.name;rotateDialog.showModal()}rotateForm.addEventListener('submit',async event=>{event.preventDefault();const out=document.querySelector('#rotate-status'),data=Object.fromEntries(new FormData(rotateForm));data.action='rotate';if(data.expiresAt)data.expiresAt=new Date(data.expiresAt).toISOString();setStatus(out,'Rotating encrypted credential…');try{await api(`/api/sources/connections${location.search}`,'PATCH',data);rotateForm.reset();rotateDialog.close();await load()}catch(error){setStatus(out,error.message,true)}})
|
|
179
|
+
const disconnectDialog=document.querySelector('#disconnect-dialog'),disconnectForm=document.querySelector('#disconnect-form');async function openDisconnect(item){const out=document.querySelector('#disconnect-status');disconnectForm.elements.id.value=item.id;document.querySelector('#impact-list').replaceChildren(node('div','note','Loading affected applications…'));disconnectDialog.showModal();try{const result=await api(`/api/sources/connections${location.search}`,'DELETE',{id:item.id,preview:true});const list=document.querySelector('#impact-list');list.replaceChildren();if(!result.affectedBindings.length)list.append(node('div','note','No active application bindings.'));for(const binding of result.affectedBindings)list.append(node('div','impact',`${binding.repositoryFullName} · ${binding.branchRule||binding.defaultBranch} · ${binding.monorepoRoot}`));setStatus(out,`${result.affectedBindings.length} binding${result.affectedBindings.length===1?'':'s'} will be disabled.`)}catch(error){setStatus(out,error.message,true)}}disconnectForm.addEventListener('submit',async event=>{event.preventDefault();const out=document.querySelector('#disconnect-status');setStatus(out,'Disconnecting and disabling dependents…');try{await api(`/api/sources/connections${location.search}`,'DELETE',{id:disconnectForm.elements.id.value});disconnectDialog.close();await load()}catch(error){setStatus(out,error.message,true)}})
|
|
180
|
+
for(const button of document.querySelectorAll('[data-close]'))button.addEventListener('click',()=>button.closest('dialog').close())
|
|
181
|
+
document.querySelector('#refresh-sources').addEventListener('click',load);document.querySelector('#repository-search').addEventListener('input',renderRepositories);document.querySelector('#repository-connection').addEventListener('change',renderRepositories)
|
|
182
|
+
load()
|
|
183
|
+
})()
|
|
184
|
+
</script>
|
|
185
|
+
</body>
|
|
186
|
+
</html>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
import { usePolling } from '../../functions/usePolling.ts'
|
|
3
|
+
const alerts=state([]),checks=state([]),rules=state([]),channels=state([]),routes=state([]),deliveries=state([]),notice=state(''),tab=state('alerts')
|
|
4
|
+
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}
|
|
5
|
+
async function optional(url,key){try{return(await api(url))[key]||[]}catch{return[]}}
|
|
6
|
+
async function load(){const [a,h,r,c,n,d]=await Promise.all([optional('/api/alerts','alerts'),optional('/api/health/checks','checks'),optional('/api/alerts/rules','rules'),optional('/api/notifications/channels','channels'),optional('/api/notifications/routes','routes'),optional('/api/notifications/deliveries?limit=100','deliveries')]);alerts.set(a);checks.set(h);rules.set(r);channels.set(c);routes.set(n);deliveries.set(d)}
|
|
7
|
+
const {pollingState,pollingError,lastUpdatedAt,refresh}=usePolling(load,15000)
|
|
8
|
+
function body(form){return Object.fromEntries(new FormData(form))}
|
|
9
|
+
async function submitHealth(event){event.preventDefault();const value=body(event.currentTarget),kind=value.kind,config=kind==='tcp'?{port:Number(value.port)}:kind==='command'?{command:String(value.command).split(/\s+/).filter(Boolean)}:{method:value.method,expectedStatuses:String(value.expectedStatuses).split(',').map(Number),expectedBody:value.expectedBody||undefined};await api('/api/health/checks',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({...value,kind,config,intervalSeconds:Number(value.intervalSeconds),timeoutSeconds:Number(value.timeoutSeconds),failureThreshold:Number(value.failureThreshold),recoveryThreshold:Number(value.recoveryThreshold),regions:String(value.regions).split(',').map(x=>x.trim()).filter(Boolean)})});event.currentTarget.reset();notice.set('Health check created.');refresh()}
|
|
10
|
+
async function submitRule(event){event.preventDefault();const value=body(event.currentTarget);await api('/api/alerts/rules',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({...value,threshold:value.threshold===''?undefined:Number(value.threshold),recoveryThreshold:value.recoveryThreshold===''?undefined:Number(value.recoveryThreshold),consecutive:Number(value.consecutive),recoveryConsecutive:Number(value.recoveryConsecutive),windowMs:Number(value.windowMinutes)*60000,groupBy:String(value.groupBy||'').split(',').filter(Boolean)})});event.currentTarget.reset();notice.set('Alert rule created.');refresh()}
|
|
11
|
+
async function submitChannel(event){event.preventDefault();const value=body(event.currentTarget),config=value.kind==='email'?{to:String(value.destination).split(',').map(x=>x.trim()),from:value.from||undefined}:value.kind==='telegram'?{chatId:value.destination}:{};await api('/api/notifications/channels',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:value.name,kind:value.kind,config,credential:value.credential||undefined})});event.currentTarget.reset();notice.set('Channel created with encrypted credentials.');refresh()}
|
|
12
|
+
async function submitRoute(event){event.preventDefault();const value=body(event.currentTarget),quietHours=value.quietStart&&value.quietEnd?{timezone:value.timezone||'UTC',start:value.quietStart,end:value.quietEnd}:undefined;await api('/api/notifications/routes',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:value.name,priority:Number(value.priority),matcher:{severities:value.severity?[value.severity]:undefined,eventTypes:value.eventType?[value.eventType]:undefined},channelIds:[value.channelId],quietHours,groupWaitSeconds:Number(value.groupWaitSeconds),reminderSeconds:value.reminderMinutes?Number(value.reminderMinutes)*60:undefined,template:value.template||undefined,rateLimitPerMinute:Number(value.rateLimitPerMinute),escalation:[]})});event.currentTarget.reset();notice.set('Routing rule created.');refresh()}
|
|
13
|
+
async function alertAction(id,action,extra={}){await api('/api/alerts/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({id,action,...extra})});notice.set(`${action} recorded.`);refresh()}
|
|
14
|
+
async function runCheck(id,test=false){const result=await api(`/api/health/checks/${test?'test':'run'}`,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({id})});notice.set(test?`Test ${result.result.status}; no incident created.`:`Check ${result.result.status}; alert rules evaluated.`);refresh()}
|
|
15
|
+
async function toggle(path,id,enabled){await api(path,{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({id,enabled})});refresh()}
|
|
16
|
+
async function toggleChannel(id,status){await api('/api/notifications/channels',{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({id,status})});refresh()}
|
|
17
|
+
async function testChannel(id){const result=await api('/api/notifications/channels/test',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({id})});notice.set(result.result.ok?'Test delivered; no incident created.':result.result.error);refresh()}
|
|
18
|
+
async function retry(id){await api('/api/notifications/deliveries/retry',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({id})});refresh()}
|
|
19
|
+
function duration(alert){const end=new Date(alert.resolvedAt||alert.lastSeenAt),start=new Date(alert.firstSeenAt),seconds=Math.max(0,(end-start)/1000);return seconds<60?`${Math.round(seconds)}s`:seconds<3600?`${Math.round(seconds/60)}m`:`${(seconds/3600).toFixed(1)}h`}
|
|
20
|
+
function tone(state){return state==='firing'||state==='dead'||state==='failed'?'bad':state==='pending'||state==='retrying'||state==='silenced'?'warn':'ok'}
|
|
21
|
+
</script>
|
|
22
|
+
<!DOCTYPE html><html lang="en" data-theme="dark"><head>@include('../partials/head',{title:'ts-cloud · Alerts & health'})</head><body>
|
|
23
|
+
@include('../partials/nav',{active:'alerts',env:'production'})
|
|
24
|
+
<main class="alert-wrap wrap" id="main-content"><div class="crumbs">Operations <span class="sep">/</span> Alerts & health</div>
|
|
25
|
+
<header class="page"><div><h1>Alerts & health</h1><p>Health checks, stateful alert evaluation, ownership, silences, routing, and delivery diagnostics.</p></div><div class="header-actions"><span class="pill" :class="pollingState()==='live'?'ok':'warn'">{{ pollingState() }}</span><small>{{ lastUpdatedAt()?.toLocaleTimeString()||'connecting' }}</small><button class="btn ghost sm" @click="refresh()">Refresh</button></div></header>
|
|
26
|
+
<p class="notice" role="status" @show="!!notice()">{{ notice() }}</p><p class="bad notice" role="alert" @show="!!pollingError()">{{ pollingError() }}</p>
|
|
27
|
+
<nav class="tabs" aria-label="Alert workspace"><button :class="{active:tab()==='alerts'}" @click="tab.set('alerts')">Alerts</button><button :class="{active:tab()==='health'}" @click="tab.set('health')">Health checks</button><button :class="{active:tab()==='rules'}" @click="tab.set('rules')">Rules</button><button :class="{active:tab()==='routing'}" @click="tab.set('routing')">Channels & routing</button><button :class="{active:tab()==='delivery'}" @click="tab.set('delivery')">Delivery</button></nav>
|
|
28
|
+
|
|
29
|
+
<section @show="tab()==='alerts'"><div class="summary"><article class="panel"><b>Firing</b><strong>{{ alerts().filter(x=>x.state==='firing').length }}</strong></article><article class="panel"><b>Pending</b><strong>{{ alerts().filter(x=>x.state==='pending').length }}</strong></article><article class="panel"><b>Silenced</b><strong>{{ alerts().filter(x=>x.state==='silenced').length }}</strong></article><article class="panel"><b>Resolved</b><strong>{{ alerts().filter(x=>x.state==='resolved').length }}</strong></article></div><div class="cards-list"><template :for="item in alerts()"><article class="alert-card panel"><div><span class="pill" :class="tone(item.state)">{{ item.state }}</span><span class="pill">{{ item.severity }}</span><h2>{{ item.title }}</h2></div><p class="note">First {{ new Date(item.firstSeenAt).toLocaleString() }} · last {{ new Date(item.lastSeenAt).toLocaleString() }} · duration {{ duration(item) }} · occurrences {{ item.occurrenceCount }}</p><pre>{{ JSON.stringify(item.evidence,null,2) }}</pre><div class="meta"><span>Owner {{ item.ownerActorId||'unassigned' }}</span><span>{{ item.acknowledgedAt?'Acknowledged '+new Date(item.acknowledgedAt).toLocaleString():'Unacknowledged' }}</span></div><div class="actions"><button class="btn ghost sm" @show="!item.acknowledgedAt" @click="alertAction(item.id,'acknowledge')">Acknowledge</button><button class="btn ghost sm" @click="alertAction(item.id,'assign',{ownerActorId:'me'})">Assign to me</button><button class="btn ghost sm" @show="item.state==='firing'" @click="alertAction(item.id,'silence',{until:new Date(Date.now()+3600000).toISOString()})">Silence 1h</button><a class="btn ghost sm" :href="'/operations/observability?releaseId='+encodeURIComponent(item.evidence?.releaseId||'')">Correlate</a></div></article></template><div class="empty panel" @show="!alerts().length"><strong>No alerts</strong><span>Healthy and empty are distinct. Add a check or metric rule to begin evaluation.</span></div></div></section>
|
|
30
|
+
|
|
31
|
+
<section @show="tab()==='health'" class="split"><div><template :for="item in checks()"><article class="row-card panel"><div><span class="pill" :class="tone(item.results?.[0]?.status==='unhealthy'?'firing':item.results?.[0]?.status||'pending')">{{ item.results?.[0]?.status||'no data' }}</span><b>{{ item.name }}</b><code>{{ item.kind }} · {{ item.target }}</code></div><p class="note">every {{ item.intervalSeconds }}s · timeout {{ item.timeoutSeconds }}s · fail {{ item.failureThreshold }} / recover {{ item.recoveryThreshold }} · agents {{ item.regions.join(', ')||'local' }}</p><div class="actions"><button class="btn ghost sm" @click="runCheck(item.id,true)">Preview/test</button><button class="btn ghost sm" @click="runCheck(item.id,false)">Run & evaluate</button><button class="btn ghost sm" @click="toggle('/api/health/checks',item.id,!item.enabled)">{{ item.enabled?'Disable':'Enable' }}</button></div></article></template></div><form class="editor panel" @submit="submitHealth($event)"><h2>New health check</h2><label>Name<input name="name" required></label><label>Kind<select name="kind"><option>http</option><option>tcp</option><option>command</option></select></label><label>Target<input name="target" required placeholder="https://app.example/health or host"></label><label>HTTP method<input name="method" value="GET"></label><label>Expected statuses<input name="expectedStatuses" value="200"></label><label>Expected body (optional)<input name="expectedBody"></label><label>TCP port<input name="port" type="number"></label><label>Command argv (admin only)<input name="command" placeholder="/usr/bin/systemctl is-active app"></label><label>Interval seconds<input name="intervalSeconds" type="number" min="10" value="60"></label><label>Timeout seconds<input name="timeoutSeconds" type="number" min="1" value="10"></label><label>Failure threshold<input name="failureThreshold" type="number" min="1" value="3"></label><label>Recovery threshold<input name="recoveryThreshold" type="number" min="1" value="2"></label><label>Agents / regions<input name="regions" value="local"></label><button class="btn" type="submit">Create check</button><p class="note">Preview/test records no production incident. Commands are argv-only and never run through a shell.</p></form></section>
|
|
32
|
+
|
|
33
|
+
<section @show="tab()==='rules'" class="split"><div><template :for="item in rules()"><article class="row-card panel"><div><span class="pill">{{ item.severity }}</span><b>{{ item.name }}</b><code>{{ item.signal }} {{ item.operator }} {{ item.threshold??'unhealthy' }}</code></div><p class="note">{{ item.consecutive }} failures · {{ item.recoveryConsecutive }} recoveries · no-data {{ item.noDataPolicy }} · v{{ item.version }}</p><button class="btn ghost sm" @click="toggle('/api/alerts/rules',item.id,!item.enabled)">{{ item.enabled?'Disable':'Enable' }}</button></article></template></div><form class="editor panel" @submit="submitRule($event)"><h2>New alert rule</h2><label>Name<input name="name" required></label><label>Health check<select name="healthCheckId"><option value="">Metric query</option><template :for="check in checks()"><option :value="check.id">{{ check.name }}</option></template></select></label><label>Metric signal<input name="signal" placeholder="errors.count"></label><label>Operator<select name="operator"><option value="unhealthy">unhealthy</option><option value="gt">greater than</option><option value="gte">greater/equal</option><option value="lt">less than</option><option value="lte">less/equal</option></select></label><label>Threshold<input name="threshold" type="number" step="any"></label><label>Recovery threshold<input name="recoveryThreshold" type="number" step="any"></label><label>Window minutes<input name="windowMinutes" type="number" value="5"></label><label>Consecutive failures<input name="consecutive" type="number" min="1" value="3"></label><label>Consecutive recoveries<input name="recoveryConsecutive" type="number" min="1" value="2"></label><label>No data<select name="noDataPolicy"><option>pending</option><option>ignore</option><option>firing</option></select></label><label>Severity<select name="severity"><option>warning</option><option>critical</option><option>info</option></select></label><label>Group by<input name="groupBy" placeholder="resourceId,region"></label><button class="btn" type="submit">Create rule</button></form></section>
|
|
34
|
+
|
|
35
|
+
<section @show="tab()==='routing'" class="routing-grid"><div><h2>Channels</h2><template :for="item in channels()"><article class="row-card panel"><div><span class="pill" :class="tone(item.status)">{{ item.status }}</span><b>{{ item.name }}</b><code>{{ item.kind }} · credential {{ item.credentialFingerprint||'none' }}</code></div><div class="actions"><button class="btn ghost sm" @click="testChannel(item.id)">Test</button><button class="btn ghost sm" @click="toggleChannel(item.id,item.status==='active'?'paused':'active')">{{ item.status==='active'?'Pause':'Resume' }}</button></div><p class="bad note" @show="!!item.lastError">{{ item.lastError }}</p></article></template><form class="editor panel" @submit="submitChannel($event)"><h3>New channel</h3><label>Name<input name="name" required></label><label>Type<select name="kind"><option>slack</option><option>discord</option><option>teams</option><option>telegram</option><option>email</option><option>webhook</option></select></label><label>Destination (email recipients or Telegram chat ID)<input name="destination"></label><label>From (email)<input name="from"></label><label>Encrypted credential<input name="credential" type="password" autocomplete="new-password" placeholder="Webhook URL, bot token, or {url,signingSecret}"></label><button class="btn" type="submit">Create channel</button></form></div><div><h2>Routing rules</h2><template :for="item in routes()"><article class="row-card panel"><div><span class="pill">priority {{ item.priority }}</span><b>{{ item.name }}</b></div><p class="note">{{ item.channelIds.length }} channels · quiet {{ item.quietHours?item.quietHours.start+'–'+item.quietHours.end+' '+item.quietHours.timezone:'off' }} · reminder {{ item.reminderSeconds?item.reminderSeconds+'s':'off' }} · limit {{ item.rateLimitPerMinute }}/min</p><button class="btn ghost sm" @click="toggle('/api/notifications/routes',item.id,!item.enabled)">{{ item.enabled?'Disable':'Enable' }}</button></article></template><form class="editor panel" @submit="submitRoute($event)"><h3>New route</h3><label>Name<input name="name" required></label><label>Priority<input name="priority" type="number" value="0"></label><label>Severity<select name="severity"><option value="">Any</option><option>critical</option><option>warning</option><option>info</option></select></label><label>Event<select name="eventType"><option value="">Any</option><option>firing</option><option>resolved</option><option>reminder</option><option>escalation</option></select></label><label>Channel<select name="channelId" required><template :for="channel in channels()"><option :value="channel.id">{{ channel.name }}</option></template></select></label><label>Quiet start<input name="quietStart" type="time"></label><label>Quiet end<input name="quietEnd" type="time"></label><label>Timezone<input name="timezone" value="UTC"></label><label>Group wait seconds<input name="groupWaitSeconds" type="number" value="30"></label><label>Reminder minutes<input name="reminderMinutes" type="number"></label><label>Rate limit / minute<input name="rateLimitPerMinute" type="number" min="1" value="60"></label><label>Message template<input name="template" placeholder="[{{severity}}] {{title}} · {{state}}"></label><button class="btn" type="submit">Create route</button></form></div></section>
|
|
36
|
+
|
|
37
|
+
<section @show="tab()==='delivery'"><div class="panel table-wrap"><table><thead><tr><th scope="col">Updated</th><th scope="col">Event</th><th scope="col">Channel</th><th scope="col">State</th><th scope="col">Attempts</th><th scope="col">Diagnostic</th><th scope="col">Actions</th></tr></thead><tbody><template :for="item in deliveries()"><tr><td>{{ new Date(item.updatedAt).toLocaleString() }}</td><td>{{ item.eventType }}</td><td class="mono">{{ item.channelId }}</td><td><span class="pill" :class="tone(item.state)">{{ item.state }}</span></td><td>{{ item.attempt }}/{{ item.maxAttempts }}</td><td>{{ item.error||item.responseStatus||'—' }}</td><td><button class="btn ghost sm" @show="item.state!=='delivered'" @click="retry(item.id)">Retry</button></td></tr></template></tbody></table><div class="empty" @show="!deliveries().length"><strong>No notification deliveries</strong><span>Channel tests are tracked on the channel and do not create fake incidents.</span></div></div></section>
|
|
38
|
+
</main><style>
|
|
39
|
+
.alert-wrap{max-width:1450px}.tabs{display:flex;gap:5px;overflow:auto;border-bottom:1px solid var(--line);margin-bottom:18px}.tabs button{border:0;background:transparent;color:var(--txt3);padding:10px 14px;white-space:nowrap}.tabs button.active{color:var(--txt);border-bottom:2px solid var(--accent)}.summary{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.summary article{display:grid;gap:8px}.summary strong{font-size:28px}.cards-list{display:grid;gap:10px;margin-top:12px}.alert-card h2{display:inline;font-size:17px;margin-left:8px}.alert-card pre{max-height:150px;overflow:auto;background:var(--bg2);padding:10px;border-radius:8px}.meta,.actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.meta{justify-content:space-between;color:var(--txt3);font-size:11px}.split,.routing-grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(300px,1fr);gap:14px}.row-card{margin-bottom:9px}.row-card>div:first-child{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.row-card code{color:var(--txt3)}.editor{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;align-content:start;position:sticky;top:10px}.editor h2,.editor h3,.editor .note,.editor button{grid-column:1/-1}.editor label{display:grid;gap:5px;color:var(--txt3);font-size:11px}.editor input,.editor select{width:100%;background:var(--bg2);border:1px solid var(--line);border-radius:8px;color:var(--txt);padding:8px}.routing-grid>div>h2{margin-top:0}.table-wrap{overflow:auto}.mono{font-family:var(--mono);font-size:11px}@media(max-width:850px){.summary{grid-template-columns:repeat(2,1fr)}.split,.routing-grid{grid-template-columns:1fr}.editor{position:static}}@media(max-width:520px){.summary{grid-template-columns:1fr}.editor{grid-template-columns:1fr}.editor>*{grid-column:1!important}}
|
|
40
|
+
</style></body></html>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
const entries = state([])
|
|
3
|
+
const effectiveEntries = state([])
|
|
4
|
+
const scopeProject = state(null)
|
|
5
|
+
const environments = state([])
|
|
6
|
+
const resources = state([])
|
|
7
|
+
const functions = state([])
|
|
8
|
+
const previews = state([])
|
|
9
|
+
const warnings = state([])
|
|
10
|
+
const configurationHash = state('')
|
|
11
|
+
const tab = state('variable')
|
|
12
|
+
const scopeType = state('environment')
|
|
13
|
+
const scopeId = state('')
|
|
14
|
+
const search = state('')
|
|
15
|
+
const effective = state(true)
|
|
16
|
+
const selected = state([])
|
|
17
|
+
const notice = state('')
|
|
18
|
+
const error = state('')
|
|
19
|
+
const busy = state(false)
|
|
20
|
+
const plan = state(null)
|
|
21
|
+
const editorOpen = state(false)
|
|
22
|
+
const importOpen = state(false)
|
|
23
|
+
const dotenvSource = state('')
|
|
24
|
+
const secretKeys = state('')
|
|
25
|
+
const currentEnv = new URLSearchParams(location.search).get('env') || 'production'
|
|
26
|
+
|
|
27
|
+
function endpoint(path) { return `${path}${path.includes('?') ? '&' : '?'}env=${encodeURIComponent(currentEnv)}` }
|
|
28
|
+
async function api(path, options) {
|
|
29
|
+
const response = await fetch(endpoint(path), options)
|
|
30
|
+
const type = response.headers.get('content-type') || ''
|
|
31
|
+
const body = type.includes('json') ? await response.json() : await response.text()
|
|
32
|
+
if (!response.ok || body?.ok === false) { const failure = new Error(body?.error || 'Request failed.'); failure.stepUpRequired = body?.stepUpRequired; throw failure }
|
|
33
|
+
return body
|
|
34
|
+
}
|
|
35
|
+
function scopeContext() {
|
|
36
|
+
const type = scopeType(), id = scopeId()
|
|
37
|
+
const resource = resources().find(item => item.id === id)
|
|
38
|
+
const functionResource = functions().find(item => item.id === id)
|
|
39
|
+
const preview = previews().find(item => item.id === id)
|
|
40
|
+
return { scopeType: type, scopeId: id, environmentId: type === 'environment' ? id : resource?.environmentId || functionResource?.environmentId || preview?.environmentId, resourceId: ['service','function'].includes(type) ? id : preview?.resourceId }
|
|
41
|
+
}
|
|
42
|
+
function chooseDefault(type, data = { project: scopeProject(), environments: environments(), resources: resources(), functions: functions(), previews: previews() }) {
|
|
43
|
+
if (type === 'project') return data.project?.id || ''
|
|
44
|
+
if (type === 'environment') return data.environments.find(item => item.slug === currentEnv)?.id || data.environments[0]?.id || ''
|
|
45
|
+
if (type === 'service') { const env = data.environments.find(item => item.slug === currentEnv); return data.resources.find(item => !env || item.environmentId === env.id)?.id || data.resources[0]?.id || '' }
|
|
46
|
+
if (type === 'function') { const env = data.environments.find(item => item.slug === currentEnv); return data.functions.find(item => !env || item.environmentId === env.id)?.id || data.functions[0]?.id || '' }
|
|
47
|
+
return data.previews[0]?.id || ''
|
|
48
|
+
}
|
|
49
|
+
function changeScope(type) { scopeType.set(type); scopeId.set(chooseDefault(type)); selected.set([]); void load() }
|
|
50
|
+
async function load() {
|
|
51
|
+
busy.set(true)
|
|
52
|
+
try {
|
|
53
|
+
const context = scopeId() ? scopeContext() : { scopeType: scopeType(), scopeId: '' }
|
|
54
|
+
const query = new URLSearchParams()
|
|
55
|
+
query.set('scopeType', context.scopeType); if (context.scopeId) query.set('scopeId', context.scopeId); if (context.environmentId) query.set('environmentId', context.environmentId); if (context.resourceId) query.set('resourceId', context.resourceId); if (search()) query.set('search', search())
|
|
56
|
+
const result = await api(`/api/configuration?${query}`)
|
|
57
|
+
scopeProject.set(result.scopes?.project || null); environments.set(result.scopes?.environments || []); resources.set(result.scopes?.resources || []); functions.set(result.scopes?.functions || []); previews.set(result.scopes?.previews || [])
|
|
58
|
+
if (!scopeId()) { const id = chooseDefault(scopeType(), result.scopes); scopeId.set(id); if (id) return await load() }
|
|
59
|
+
entries.set(result.entries || []); effectiveEntries.set(result.effectiveEntries || []); warnings.set(result.warnings || []); configurationHash.set(result.configurationHash || ''); error.set('')
|
|
60
|
+
}
|
|
61
|
+
catch (cause) { error.set(cause.message) }
|
|
62
|
+
finally { busy.set(false) }
|
|
63
|
+
}
|
|
64
|
+
function shown() { return (effective() ? effectiveEntries() : entries()).filter(item => item.kind === tab()) }
|
|
65
|
+
function selectTab(value) { tab.set(value); selected.set([]); void load() }
|
|
66
|
+
function toggle(id) { selected.set(selected().includes(id) ? selected().filter(value => value !== id) : [...selected(), id]) }
|
|
67
|
+
function allSelected() { const ids = shown().filter(item => !item.inherited).map(item => item.id); selected.set(ids.length && ids.every(id => selected().includes(id)) ? [] : ids) }
|
|
68
|
+
function when(value) { return value ? new Date(value).toLocaleString() : 'Never' }
|
|
69
|
+
function scopeLabel(entry) {
|
|
70
|
+
if (entry.scope.type === 'project') return 'Project'
|
|
71
|
+
if (entry.scope.type === 'environment') return environments().find(item => item.id === entry.scope.id)?.name || 'Environment'
|
|
72
|
+
if (entry.scope.type === 'service') return resources().find(item => item.id === entry.scope.id)?.name || 'Service'
|
|
73
|
+
if (entry.scope.type === 'function') return functions().find(item => item.id === entry.scope.id)?.name || 'Function'
|
|
74
|
+
return previews().find(item => item.id === entry.scope.id)?.name || 'Preview'
|
|
75
|
+
}
|
|
76
|
+
function formData(form) { return Object.fromEntries(new FormData(form)) }
|
|
77
|
+
async function previewChange(form) {
|
|
78
|
+
const value = formData(form), context = scopeContext()
|
|
79
|
+
try { plan.set((await api('/api/configuration/plan', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ...context, values: { [value.key]: value.value || value.reference || '' } }) })).plan); error.set('') }
|
|
80
|
+
catch (cause) { error.set(cause.message) }
|
|
81
|
+
}
|
|
82
|
+
async function save(event) {
|
|
83
|
+
event.preventDefault(); busy.set(true); plan.set(null)
|
|
84
|
+
const form = event.currentTarget, value = formData(form), existing = entries().find(item => item.key === value.key), confirm = existing ? window.prompt(`Type ${value.key} to replace this ${tab()}.`) || '' : ''
|
|
85
|
+
if (existing && confirm !== value.key) { busy.set(false); return }
|
|
86
|
+
try {
|
|
87
|
+
const result = await api('/api/configuration', { method: 'POST', headers: { 'content-type': 'application/json', 'idempotency-key': crypto.randomUUID() }, body: JSON.stringify({ ...scopeContext(), key: value.key, kind: tab(), value: value.backend === 'external' ? undefined : value.value, reference: value.backend === 'external' ? value.reference : undefined, backend: tab() === 'secret' ? value.backend : undefined, required: value.required === 'on', expectedVersion: existing?.version, confirm }) })
|
|
88
|
+
notice.set(`${value.key} ${existing ? 'updated' : 'created'}. ${result.mutation.affectedResourceIds.length} service(s) need deployment.`); form.reset(); editorOpen.set(false); await load()
|
|
89
|
+
}
|
|
90
|
+
catch (cause) { error.set(cause.stepUpRequired ? `${cause.message} Verify your session under Sign-in & security, then retry.` : cause.message) }
|
|
91
|
+
finally { busy.set(false) }
|
|
92
|
+
}
|
|
93
|
+
async function reveal(item) {
|
|
94
|
+
try { const result = await api('/api/configuration/reveal', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: item.id }) }); await navigator.clipboard.writeText(result.value); notice.set(`${item.key} copied. Clipboard access was audited.`); error.set('') }
|
|
95
|
+
catch (cause) { error.set(cause.stepUpRequired ? `${cause.message} Verify your session, then retry.` : cause.message) }
|
|
96
|
+
}
|
|
97
|
+
async function removeEntry(item) {
|
|
98
|
+
const confirm = window.prompt(`Type ${item.key} to delete it. Dependent services may require redeployment.`) || ''; if (confirm !== item.key) return
|
|
99
|
+
try { await api('/api/configuration', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: item.id, expectedVersion: item.version, confirm }) }); notice.set(`${item.key} deleted.`); await load() }
|
|
100
|
+
catch (cause) { error.set(cause.message) }
|
|
101
|
+
}
|
|
102
|
+
async function bulkDelete() {
|
|
103
|
+
if (!selected().length || (window.prompt('Type bulk to delete the selected entries.') || '') !== 'bulk') return
|
|
104
|
+
try { const result = await api('/api/configuration/bulk', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ids: selected(), action: 'delete', confirm: 'bulk' }) }); notice.set(`Deleted ${result.removed.length} entries.`); selected.set([]); await load() }
|
|
105
|
+
catch (cause) { error.set(cause.message) }
|
|
106
|
+
}
|
|
107
|
+
async function transfer(item) {
|
|
108
|
+
const targetScopeType = window.prompt('Destination scope: project, environment, service, function, or preview', ['service','function'].includes(item.scope.type) ? 'environment' : 'service') || ''
|
|
109
|
+
if (!['project', 'environment', 'service', 'function', 'preview'].includes(targetScopeType)) return
|
|
110
|
+
const options = targetScopeType === 'project' ? [scopeProject()] : targetScopeType === 'environment' ? environments() : targetScopeType === 'service' ? resources() : targetScopeType === 'function' ? functions() : previews()
|
|
111
|
+
const hint = options.map(value => `${value.name || value.slug}: ${value.id}`).join('\n')
|
|
112
|
+
const targetScopeId = window.prompt(`Destination id:\n${hint}`, options[0]?.id || '') || ''; if (!targetScopeId) return
|
|
113
|
+
const mode = window.confirm('Move instead of copy? OK moves; Cancel copies.') ? 'move' : 'copy'
|
|
114
|
+
const confirm = window.prompt(`Type ${item.key} to ${mode} it.`) || ''; if (confirm !== item.key) return
|
|
115
|
+
try { await api('/api/configuration/transfer', { method: 'POST', headers: { 'content-type': 'application/json', 'idempotency-key': crypto.randomUUID() }, body: JSON.stringify({ id: item.id, targetScopeType, targetScopeId, mode, confirm }) }); notice.set(`${item.key} ${mode === 'move' ? 'moved' : 'copied'}.`); await load() }
|
|
116
|
+
catch (cause) { error.set(cause.message) }
|
|
117
|
+
}
|
|
118
|
+
async function importDotenv(event) {
|
|
119
|
+
event.preventDefault(); busy.set(true)
|
|
120
|
+
try {
|
|
121
|
+
const result = await api('/api/configuration/import', { method: 'POST', headers: { 'content-type': 'application/json', 'idempotency-key': crypto.randomUUID() }, body: JSON.stringify({ ...scopeContext(), source: dotenvSource(), secretKeys: secretKeys().split(',').map(value => value.trim()).filter(Boolean), confirm: 'import' }) })
|
|
122
|
+
notice.set(`Imported ${result.mutation.added.length} new and ${result.mutation.changed.length} changed entries.`); dotenvSource.set(''); secretKeys.set(''); importOpen.set(false); await load()
|
|
123
|
+
}
|
|
124
|
+
catch (cause) { error.set(cause.message) }
|
|
125
|
+
finally { busy.set(false) }
|
|
126
|
+
}
|
|
127
|
+
async function exportVariables() { try { const response = await fetch(endpoint(`/api/configuration/export?${new URLSearchParams(scopeContext())}`)); if (!response.ok) throw new Error('Export failed.'); const blob = await response.blob(), anchor = document.createElement('a'); anchor.href = URL.createObjectURL(blob); anchor.download = 'ts-cloud.env'; anchor.click(); URL.revokeObjectURL(anchor.href) } catch (cause) { error.set(cause.message) } }
|
|
128
|
+
let searchTimer
|
|
129
|
+
function searchChanged(value) { search.set(value); clearTimeout(searchTimer); searchTimer = setTimeout(() => void load(), 250) }
|
|
130
|
+
onMount(() => { void load() })
|
|
131
|
+
</script>
|
|
132
|
+
<!DOCTYPE html>
|
|
133
|
+
<html lang="en" data-theme="dark">
|
|
134
|
+
<head>@include('../partials/head', { title: 'ts-cloud · Environment & secrets' })</head>
|
|
135
|
+
<body>
|
|
136
|
+
@include('../partials/nav', { active: 'configuration', env: 'production' })
|
|
137
|
+
<main class="configuration-wrap wrap" id="main-content">
|
|
138
|
+
<div class="crumbs">Operations <span class="sep">/</span> Environment & secrets</div>
|
|
139
|
+
<header class="page"><div><h1>Environment & secrets</h1><p>Manage inherited runtime configuration across project, environment, service, and preview scopes from one deploy-safe control plane.</p></div><div class="header-actions"><button class="btn ghost" @click="importOpen.set(!importOpen())">Import .env</button><button class="btn ghost" @click="exportVariables()">Export variables</button><button class="btn" @click="editorOpen.set(!editorOpen())">New {{ tab() }}</button></div></header>
|
|
140
|
+
|
|
141
|
+
<p class="notice" role="status" @show="!!notice()">{{ notice() }}</p><p class="bad notice" role="alert" @show="!!error()">{{ error() }}</p>
|
|
142
|
+
<section class="panel scope-bar" aria-label="Configuration scope">
|
|
143
|
+
<div><span class="eyebrow">Scope</span><div class="segmented"><button :class="{active:scopeType()==='project'}" @click="changeScope('project')">Project</button><button :class="{active:scopeType()==='environment'}" @click="changeScope('environment')">Environment</button><button :class="{active:scopeType()==='service'}" @click="changeScope('service')">Service</button><button :class="{active:scopeType()==='function'}" @click="changeScope('function')">Function</button><button :class="{active:scopeType()==='preview'}" @click="changeScope('preview')">Preview</button></div></div>
|
|
144
|
+
<label @show="scopeType()==='project'"><span>Target</span><select :value="scopeId()" @change="scopeId.set($event.currentTarget.value);selected.set([]);load()"><option :value="scopeProject()?.id">{{ scopeProject()?.name || 'Project' }}</option></select></label>
|
|
145
|
+
<label @show="scopeType()==='environment'"><span>Target environment</span><select :value="scopeId()" @change="scopeId.set($event.currentTarget.value);selected.set([]);load()"><template :for="item in environments()"><option :value="item.id">{{ item.name || item.slug }}</option></template></select></label>
|
|
146
|
+
<label @show="scopeType()==='service'"><span>Target service</span><select :value="scopeId()" @change="scopeId.set($event.currentTarget.value);selected.set([]);load()"><template :for="item in resources()"><option :value="item.id">{{ item.name || item.slug }}</option></template></select></label>
|
|
147
|
+
<label @show="scopeType()==='function'"><span>Target function</span><select :value="scopeId()" @change="scopeId.set($event.currentTarget.value);selected.set([]);load()"><template :for="item in functions()"><option :value="item.id">{{ item.name || item.slug }}</option></template></select></label>
|
|
148
|
+
<label @show="scopeType()==='preview'"><span>Target preview</span><select :value="scopeId()" @change="scopeId.set($event.currentTarget.value);selected.set([]);load()"><template :for="item in previews()"><option :value="item.id">{{ item.name }} · {{ item.status }}</option></template></select></label>
|
|
149
|
+
<div class="scope-health"><span class="pill" :class="warnings().length?'warn':'ok'">{{ warnings().length ? warnings().length+' warning(s)' : 'references healthy' }}</span><code :title="configurationHash()">{{ configurationHash().slice(0,18) || 'not resolved' }}</code></div>
|
|
150
|
+
</section>
|
|
151
|
+
|
|
152
|
+
<section class="import-panel panel" @show="importOpen()"><div><h2>Import dotenv</h2><p class="note">Paste or upload content. Conflicting duplicates stop the import; the preview and audit trail contain key names only.</p></div><form @submit="importDotenv($event)"><label>Dotenv content<textarea required rows="8" class="mono" :value="dotenvSource()" @input="dotenvSource.set($event.currentTarget.value)" placeholder="PUBLIC_URL=https://example.com DATABASE_URL=..."></textarea></label><label>Secret keys <input :value="secretKeys()" @input="secretKeys.set($event.currentTarget.value)" placeholder="DATABASE_URL, API_TOKEN"><span>Comma-separated. All other keys are plaintext variables and may be exported.</span></label><button class="btn" :disabled="busy()">Validate & import</button></form></section>
|
|
153
|
+
|
|
154
|
+
<section class="editor-panel panel" @show="editorOpen()"><form @submit="save($event)"><div><h2>Set {{ tab() }}</h2><p class="note">Existing production values require typed confirmation. Secret values are write-only.</p></div><label>Key<input name="key" required pattern="[A-Za-z_][A-Za-z0-9_]*" placeholder="DATABASE_URL" autocomplete="off"></label><label @show="tab()==='secret'">Backend<select name="backend"><option value="local_encrypted">Encrypted local/box</option><option value="aws_secrets_manager">AWS Secrets Manager</option><option value="aws_ssm">AWS SSM Parameter Store</option><option value="external">External reference</option></select></label><label class="wide">{{ tab()==='secret'?'Value (write-only)':'Value' }}<textarea name="value" rows="3" :type="tab()==='secret'?'password':'text'" placeholder="Value, including multiline content"></textarea></label><label class="wide" @show="tab()==='secret'">External reference<input name="reference" placeholder="vault://team/app/token"><span>Used only when External reference is selected.</span></label><label class="check"><input name="required" type="checkbox"> Required at deployment</label><div class="actions"><button class="btn ghost" type="button" @click="previewChange($event.currentTarget.form)">Preview key diff</button><button class="btn" type="submit" :disabled="busy()">Save {{ tab() }}</button></div></form><div class="change-plan" @show="!!plan()"><h3>Change preview</h3><p><b>{{ plan()?.added.length || 0 }}</b> add · <b>{{ plan()?.changed.length || 0 }}</b> change · <b>{{ plan()?.removed.length || 0 }}</b> remove · <b>{{ plan()?.affectedResourceIds.length || 0 }}</b> affected services</p><code>{{ [...(plan()?.added||[]),...(plan()?.changed||[]),...(plan()?.removed||[])].join(', ') }}</code></div></section>
|
|
155
|
+
|
|
156
|
+
<nav class="tabs" aria-label="Configuration kind"><button :class="{active:tab()==='variable'}" @click="selectTab('variable')">Variables <span>{{ entries().filter(item=>item.kind==='variable').length }}</span></button><button :class="{active:tab()==='secret'}" @click="selectTab('secret')">Secrets <span>{{ entries().filter(item=>item.kind==='secret').length }}</span></button></nav>
|
|
157
|
+
<section class="panel table-tools"><input type="search" placeholder="Search keys" aria-label="Search configuration" @input="searchChanged($event.currentTarget.value)"><label class="check"><input type="checkbox" :checked="effective()" @change="effective.set($event.currentTarget.checked)"> Show inherited effective values</label><span class="spacer"></span><button class="btn danger sm" @show="selected().length" @click="bulkDelete()">Delete {{ selected().length }} selected</button><button class="btn ghost sm" @click="load()">Refresh</button></section>
|
|
158
|
+
<section class="config-table panel"><table><thead><tr><th scope="col"><input type="checkbox" aria-label="Select all" @change="allSelected()"></th><th scope="col">Key</th><th scope="col">Scope & precedence</th><th scope="col">{{ tab()==='secret'?'Backend':'Value' }}</th><th scope="col">Use & rotation</th><th scope="col" class="table-actions">Actions</th></tr></thead><tbody><template :for="item in shown()"><tr><td><input type="checkbox" :checked="selected().includes(item.id)" :disabled="item.inherited" :aria-label="'Select '+item.key" @change="toggle(item.id)"></td><td><div class="key-cell"><code>{{ item.key }}</code><span class="pill" @show="item.required">required</span><span class="pill warn" @show="item.inherited">inherited</span><span class="pill" @show="item.overridden">overrides</span></div></td><td><b>{{ scopeLabel(item) }}</b><small>{{ item.scope.type }} · v{{ item.version }}</small></td><td><div @show="item.kind==='secret'"><b>{{ item.backend || 'restricted' }}</b><small>{{ item.backendVersion ? 'backend v'+item.backendVersion : 'metadata hidden' }}</small><code @show="!!item.reference">{{ item.reference }}</code></div><code class="value-preview" @show="item.kind==='variable'">{{ item.value }}</code></td><td><b>{{ item.rotatedAt ? 'Rotated '+when(item.rotatedAt) : 'Not rotated' }}</b><small>Last used {{ when(item.lastUsedAt) }}</small><small>Updated {{ when(item.updatedAt) }}</small></td><td class="table-actions"><div class="row-actions"><button class="btn ghost sm" @show="item.kind==='secret'" @click="reveal(item)">Reveal & copy</button><button class="btn ghost sm" @show="!item.inherited" @click="transfer(item)">Move / copy</button><button class="btn danger sm" @show="!item.inherited" @click="removeEntry(item)">Delete</button></div></td></tr></template><tr @show="!shown().length"><td colspan="6"><div class="compact empty"><strong>No {{ tab() }}s at this scope</strong><span>Create one here, import dotenv, or enable the inherited view.</span></div></td></tr></tbody></table></section>
|
|
159
|
+
<aside class="warning-list" @show="warnings().length"><h2>Deployment warnings</h2><template :for="warning in warnings()"><p class="notice warn"><code>{{ warning.key || 'scope' }}</code> {{ warning.message }}</p></template></aside>
|
|
160
|
+
</main>
|
|
161
|
+
<style>
|
|
162
|
+
.configuration-wrap{max-width:1500px}.scope-bar{display:grid;grid-template-columns:1.4fr 1fr auto;gap:24px;align-items:end}.eyebrow,.scope-bar label span{display:block;color:var(--txt3);font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:.08em}.segmented{display:flex;gap:4px;background:var(--bg);padding:4px;border-radius:8px;width:max-content}.segmented button{border:0;background:transparent;color:var(--txt3);padding:8px 12px;border-radius:6px}.segmented button.active{background:var(--panel2);color:var(--txt)}.scope-bar select{min-width:240px;background:var(--bg)!important;color:var(--txt)!important;border:1px solid var(--line)!important}.scope-health{display:flex;flex-direction:column;align-items:flex-end;gap:8px}.import-panel,.editor-panel{margin-top:18px;padding:22px;display:grid;grid-template-columns:minmax(240px,.7fr) minmax(420px,1.3fr);gap:28px}.import-panel form,.editor-panel form{display:grid;grid-template-columns:1fr 1fr;gap:14px}.import-panel label:first-child,.wide{grid-column:1/-1}.editor-panel>form>div:first-child{grid-column:1/-1}.editor-panel label,.import-panel label{display:flex;flex-direction:column;gap:7px}.editor-panel label span,.import-panel label span{color:var(--txt3);font-size:12px}.change-plan{border-left:1px solid var(--line);padding-left:24px}.tabs{display:flex;gap:4px;margin-top:22px;border-bottom:1px solid var(--line)}.tabs button{border:0;background:transparent;color:var(--txt3);padding:12px 16px;border-bottom:2px solid transparent}.tabs button.active{color:var(--txt);border-bottom-color:var(--accent)}.tabs button span{opacity:.6}.table-tools{display:flex;gap:16px;align-items:center;padding:12px 16px;border-radius:0}.table-tools input[type=search]{max-width:360px}.spacer{flex:1}.config-table{overflow:auto;border-top-left-radius:0;border-top-right-radius:0}.config-table table{min-width:1100px}.config-table td{vertical-align:top}.config-table td:nth-child(3),.config-table td:nth-child(4),.config-table td:nth-child(5){display:table-cell}.config-table td b,.config-table td small,.config-table td code{display:block;margin-bottom:5px}.key-cell{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.key-cell code{font-weight:700;margin:0}.value-preview{max-width:320px;white-space:pre-wrap;overflow-wrap:anywhere}.row-actions{flex-wrap:wrap}.warning-list{margin-top:20px}.check{display:flex!important;flex-direction:row!important;align-items:center;gap:8px!important}.actions{display:flex;gap:10px;align-items:end}@media(max-width:900px){.scope-bar,.import-panel,.editor-panel{grid-template-columns:1fr}.scope-health{align-items:flex-start}.import-panel form,.editor-panel form{grid-template-columns:1fr}.config-table{overflow-x:auto}}
|
|
163
|
+
</style>
|
|
164
|
+
</body></html>
|