From 002d969a5dafaebbf9efe5d7b0aa307addbeb7a1 Mon Sep 17 00:00:00 2001 From: jarek Date: Sat, 9 May 2026 09:54:46 +0200 Subject: [PATCH] 1.0.28 --- Dockerfile | 2 +- collector/main.go | 2 +- drizzle-pg/0006_add_git_stack_context_dir.sql | 2 + drizzle-pg/meta/0006_snapshot.json | 3051 ++++++++++++++++ drizzle-pg/meta/_journal.json | 7 + drizzle/0006_add_git_stack_context_dir.sql | 2 + drizzle/meta/0006_snapshot.json | 3172 +++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 4 +- src/lib/components/StackEnvVarsPanel.svelte | 9 +- src/lib/components/WhatsNewModal.svelte | 2 +- src/lib/data/changelog.json | 25 + src/lib/server/db.ts | 87 +- src/lib/server/db/schema/index.ts | 2 + src/lib/server/db/schema/pg-schema.ts | 2 + src/lib/server/docker.ts | 54 +- src/lib/server/git.ts | 90 +- src/lib/server/notifications.ts | 48 +- src/lib/server/scheduler/index.ts | 91 +- .../server/scheduler/tasks/system-cleanup.ts | 9 + src/lib/server/stack-scanner.ts | 46 +- src/lib/server/stacks.ts | 46 +- src/lib/stores/containers.ts | 14 +- src/lib/stores/settings.ts | 22 + .../api/containers/pending-updates/+server.ts | 12 +- src/routes/api/git/stacks/+server.ts | 11 +- src/routes/api/git/stacks/[id]/+server.ts | 2 + src/routes/api/registry/catalog/+server.ts | 4 +- src/routes/api/registry/search/+server.ts | 52 +- src/routes/api/registry/tags/+server.ts | 4 +- .../schedules/system/[id]/toggle/+server.ts | 11 +- src/routes/api/settings/general/+server.ts | 30 +- .../api/volumes/[name]/export/+server.ts | 31 +- src/routes/containers/+page.svelte | 21 +- src/routes/containers/BatchUpdateModal.svelte | 8 +- .../containers/ContainerSettingsTab.svelte | 164 +- .../containers/CreateContainerModal.svelte | 19 + .../containers/EditContainerModal.svelte | 51 +- src/routes/dashboard/EnvironmentTile.svelte | 12 +- src/routes/settings/about/AboutTab.svelte | 13 +- .../environments/EnvironmentModal.svelte | 50 +- src/routes/settings/general/GeneralTab.svelte | 33 + .../notifications/NotificationModal.svelte | 2 + src/routes/stacks/GitStackModal.svelte | 90 +- src/routes/stacks/ImportStackModal.svelte | 13 +- src/routes/stacks/StackModal.svelte | 6 +- 46 files changed, 7202 insertions(+), 233 deletions(-) create mode 100644 drizzle-pg/0006_add_git_stack_context_dir.sql create mode 100644 drizzle-pg/meta/0006_snapshot.json create mode 100644 drizzle/0006_add_git_stack_context_dir.sql create mode 100644 drizzle/meta/0006_snapshot.json diff --git a/Dockerfile b/Dockerfile index 004eaeb..e98dd97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ RUN APKO_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64") " - busybox" \ " - tzdata" \ " - docker-cli" \ - " - docker-compose=5.1.3-r0" \ + " - docker-compose=5.1.3-r2" \ " - docker-cli-buildx" \ " - sqlite" \ " - postgresql-client" \ diff --git a/collector/main.go b/collector/main.go index 4482cc2..7f5bd8d 100644 --- a/collector/main.go +++ b/collector/main.go @@ -421,7 +421,7 @@ func (m *manager) collectMetrics(env *environment) { sCtx, sCancel := context.WithTimeout(env.ctx, 10*time.Second) defer sCancel() - sResp, sErr := env.doRequest(sCtx, "GET", fmt.Sprintf("/containers/%s/stats?stream=false&one-shot=true", id)) + sResp, sErr := env.doRequest(sCtx, "GET", fmt.Sprintf("/containers/%s/stats?stream=false", id)) if sErr != nil { return } diff --git a/drizzle-pg/0006_add_git_stack_context_dir.sql b/drizzle-pg/0006_add_git_stack_context_dir.sql new file mode 100644 index 0000000..f27a5d0 --- /dev/null +++ b/drizzle-pg/0006_add_git_stack_context_dir.sql @@ -0,0 +1,2 @@ +ALTER TABLE "git_stacks" ADD COLUMN "context_dir" text;--> statement-breakpoint +ALTER TABLE "git_stacks" ADD COLUMN "no_build_cache" boolean DEFAULT false; diff --git a/drizzle-pg/meta/0006_snapshot.json b/drizzle-pg/meta/0006_snapshot.json new file mode 100644 index 0000000..be94a09 --- /dev/null +++ b/drizzle-pg/meta/0006_snapshot.json @@ -0,0 +1,3051 @@ +{ + "id": "0006_add_git_stack_context_dir", + "prevId": "0005_add_api_tokens", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_settings": { + "name": "auth_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_update_settings": { + "name": "auto_update_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.config_sets": { + "name": "config_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.container_events": { + "name": "container_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_notifications": { + "name": "environment_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "notification_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_name_unique": { + "name": "environments_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_credentials": { + "name": "git_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_repositories": { + "name": "git_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_stacks": { + "name": "git_stacks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_dir": { + "name": "context_dir", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_on_deploy": { + "name": "build_on_deploy", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "no_build_cache": { + "name": "no_build_cache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repull_images": { + "name": "repull_images", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "force_redeploy": { + "name": "force_redeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hawser_tokens": { + "name": "hawser_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics": { + "name": "host_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_percent": { + "name": "memory_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_used": { + "name": "memory_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "memory_total": { + "name": "memory_total", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ldap_config": { + "name": "ldap_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_config": { + "name": "oidc_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_container_updates": { + "name": "pending_container_updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_image": { + "name": "current_image", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pending_container_updates_environment_id_environments_id_fk": { + "name": "pending_container_updates_environment_id_environments_id_fk", + "tableFrom": "pending_container_updates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pending_container_updates_environment_id_container_id_unique": { + "name": "pending_container_updates_environment_id_container_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registries": { + "name": "registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registries_name_unique": { + "name": "registries_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule_executions": { + "name": "schedule_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + { + "expression": "schedule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_environment_variables": { + "name": "stack_environment_variables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_events": { + "name": "stack_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_sources": { + "name": "stack_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_compose_path": { + "name": "external_compose_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_env_path": { + "name": "external_env_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "role_id", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vulnerability_scans": { + "name": "vulnerability_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanned_at": { + "name": "scanned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_tokens": { + "name": "api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "api_tokens_user_id_idx": { + "name": "api_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_tokens_token_prefix_idx": { + "name": "api_tokens_token_prefix_idx", + "columns": [ + { + "expression": "token_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_tokens_user_id_users_id_fk": { + "name": "api_tokens_user_id_users_id_fk", + "tableFrom": "api_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_tokens_token_hash_unique": { + "name": "api_tokens_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/_journal.json b/drizzle-pg/meta/_journal.json index e919fd8..33e441a 100644 --- a/drizzle-pg/meta/_journal.json +++ b/drizzle-pg/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1775312212996, "tag": "0005_add_api_tokens", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1777220350655, + "tag": "0006_add_git_stack_context_dir", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/0006_add_git_stack_context_dir.sql b/drizzle/0006_add_git_stack_context_dir.sql new file mode 100644 index 0000000..6858bba --- /dev/null +++ b/drizzle/0006_add_git_stack_context_dir.sql @@ -0,0 +1,2 @@ +ALTER TABLE `git_stacks` ADD `context_dir` text;--> statement-breakpoint +ALTER TABLE `git_stacks` ADD `no_build_cache` integer DEFAULT false; \ No newline at end of file diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..d1583c2 --- /dev/null +++ b/drizzle/meta/0006_snapshot.json @@ -0,0 +1,3172 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "65931916-e53c-4e46-8901-82969a2e6bf0", + "prevId": "1d5b42c6-dad2-4a6a-a27f-86862c5ad820", + "tables": { + "api_tokens": { + "name": "api_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "api_tokens_token_hash_unique": { + "name": "api_tokens_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "api_tokens_user_id_idx": { + "name": "api_tokens_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "api_tokens_token_prefix_idx": { + "name": "api_tokens_token_prefix_idx", + "columns": [ + "token_prefix" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_tokens_user_id_users_id_fk": { + "name": "api_tokens_user_id_users_id_fk", + "tableFrom": "api_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_settings": { + "name": "auth_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auto_update_settings": { + "name": "auto_update_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_updated": { + "name": "last_updated", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "columns": [ + "environment_id", + "container_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config_sets": { + "name": "config_sets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "container_events": { + "name": "container_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_notifications": { + "name": "environment_notifications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "columns": [ + "environment_id", + "notification_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environments_name_unique": { + "name": "environments_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_credentials": { + "name": "git_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_repositories": { + "name": "git_repositories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'compose.yaml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_stacks": { + "name": "git_stacks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'compose.yaml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "context_dir": { + "name": "context_dir", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build_on_deploy": { + "name": "build_on_deploy", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "no_build_cache": { + "name": "no_build_cache", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "repull_images": { + "name": "repull_images", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "force_redeploy": { + "name": "force_redeploy", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hawser_tokens": { + "name": "hawser_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics": { + "name": "host_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_percent": { + "name": "memory_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ldap_config": { + "name": "ldap_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_config": { + "name": "oidc_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pending_container_updates": { + "name": "pending_container_updates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_image": { + "name": "current_image", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "pending_container_updates_environment_id_container_id_unique": { + "name": "pending_container_updates_environment_id_container_id_unique", + "columns": [ + "environment_id", + "container_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "pending_container_updates_environment_id_environments_id_fk": { + "name": "pending_container_updates_environment_id_environments_id_fk", + "tableFrom": "pending_container_updates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "registries": { + "name": "registries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "registries_name_unique": { + "name": "registries_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_executions": { + "name": "schedule_executions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_at": { + "name": "triggered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + "schedule_type", + "schedule_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_environment_variables": { + "name": "stack_environment_variables", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_secret": { + "name": "is_secret", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "columns": [ + "stack_name", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_events": { + "name": "stack_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_sources": { + "name": "stack_sources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_path": { + "name": "env_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "columns": [ + "user_id", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "columns": [ + "user_id", + "role_id", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vulnerability_scans": { + "name": "vulnerability_scans", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanned_at": { + "name": "scanned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + "environment_id", + "image_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 6e39aa6..c1a5a0c 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1775311743346, "tag": "0005_add_api_tokens", "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1777220350655, + "tag": "0006_add_git_stack_context_dir", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index e84b6ad..6e568c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dockhand", "private": true, - "version": "1.0.27", + "version": "1.0.28", "type": "module", "scripts": { "dev": "npx vite dev", @@ -78,7 +78,7 @@ "cronstrue": "3.9.0", "devalue": "5.6.4", "drizzle-orm": "0.45.2", - "fast-xml-parser": "5.5.8", + "fast-xml-parser": "5.7.3", "js-yaml": "4.1.1", "ldapts": "8.1.3", "nodemailer": "8.0.5", diff --git a/src/lib/components/StackEnvVarsPanel.svelte b/src/lib/components/StackEnvVarsPanel.svelte index 795f2e5..0590ab1 100644 --- a/src/lib/components/StackEnvVarsPanel.svelte +++ b/src/lib/components/StackEnvVarsPanel.svelte @@ -200,8 +200,8 @@ * Sync rawContent TO variables. * Parses raw content for non-secrets, preserves existing secrets. */ - function syncRawToVariables() { - const { vars, warnings } = parseRawContent(rawContent); + function syncRawToVariables(content?: string) { + const { vars, warnings } = parseRawContent(content ?? rawContent); parseWarnings = warnings; // Preserve existing secrets (they're not in rawContent) @@ -240,8 +240,9 @@ // Form → Text: sync variables to raw (preserves comments) syncVariablesToRaw(); } else if (newMode === 'form' && viewMode === 'text') { - // Text → Form: sync raw to variables (preserves secrets) - syncRawToVariables(); + // Text → Form: use textEditorContent which falls back to generatedRawContent + // when rawContent is empty (fixes vars lost on view switch for git stacks) + syncRawToVariables(textEditorContent); } viewMode = newMode; diff --git a/src/lib/components/WhatsNewModal.svelte b/src/lib/components/WhatsNewModal.svelte index 6f715b3..89e2ebf 100644 --- a/src/lib/components/WhatsNewModal.svelte +++ b/src/lib/components/WhatsNewModal.svelte @@ -62,7 +62,7 @@ ({release.date})
- {#each release.changes as change} + {#each [...release.changes].sort((a, b) => a.type === b.type ? 0 : a.type === 'feature' ? -1 : 1) as change} {@const { icon: Icon, class: iconClass } = getChangeIcon(change.type)}
diff --git a/src/lib/data/changelog.json b/src/lib/data/changelog.json index 07e7619..9c69f0d 100644 --- a/src/lib/data/changelog.json +++ b/src/lib/data/changelog.json @@ -1,4 +1,29 @@ [ + { + "version": "1.0.28", + "date": "2026-05-09", + "changes": [ + { "type": "feature", "text": "context directory for git stacks — reference files from anywhere in the repo (#864)" }, + { "type": "feature", "text": "no-cache build option for git stacks (#880)" }, + { "type": "fix", "text": "env vars lost when switching between raw/form view (#964)" }, + { "type": "fix", "text": "compose name property not respected during stack scan (#922)" }, + { "type": "feature", "text": "editable schedule for scanner cache cleanup (#979)" }, + { "type": "fix", "text": "container labels cannot be deleted (#984)" }, + { "type": "fix", "text": "env var values leaked in deploy logs — now all values are redacted (#985)" }, + { "type": "fix", "text": "volume export keeps helper container alive, preventing volume prune/deletion (#983)" }, + { "type": "fix", "text": "ntfy self-hosted notifications fail when using ?auth= query parameter (#840)" }, + { "type": "fix", "text": "scrollbar appears in dashboard tiles when content overflows (#969)" }, + { "type": "fix", "text": "case-sensitive environment sort order — lowercase names sorted after uppercase (#975)" }, + { "type": "fix", "text": "inaccurate dashboard CPU gauge caused by one-shot stats flag (#932)" }, + { "type": "feature", "text": "ntfy notifications support ?tags=, ?title=, and ?priority= URL query parameters (#689)" }, + { "type": "fix", "text": "stack .env file wiped when saving from graph view (#988)" }, + { "type": "feature", "text": "dismiss update available indicators without updating (#853)" }, + { "type": "feature", "text": "public IP setting available for hawser-edge environments — enables clickable port links (#350)" }, + { "type": "fix", "text": "git stack creation silently destroys existing stacks with the same name (#1001)" }, + { "type": "feature", "text": "static IP/MAC address configuration for containers (#297)" } + ], + "imageTag": "fnsys/dockhand:v1.0.28" + }, { "version": "1.0.27", "comingSoon": false, diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index 4016815..0ade7f5 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -113,7 +113,7 @@ export function initDatabase() { // ============================================================================= export async function getEnvironments(): Promise { - const results = await db.select().from(environments).orderBy(asc(environments.name)); + const results = await db.select().from(environments).orderBy(sql`lower(${environments.name})`); return results.map((e: Environment) => ({ ...e, tlsKey: decrypt(e.tlsKey), @@ -2088,7 +2088,9 @@ export interface GitStackData { autoUpdateCron: string; webhookEnabled: boolean; webhookSecret: string | null; + contextDir: string | null; buildOnDeploy: boolean; + noBuildCache: boolean; repullImages: boolean; forceRedeploy: boolean; lastSync: string | null; @@ -2124,7 +2126,9 @@ export async function getGitStacks(environmentId?: number): Promise autoUpdateCron: gitStacks.autoUpdateCron, webhookEnabled: gitStacks.webhookEnabled, webhookSecret: gitStacks.webhookSecret, + contextDir: gitStacks.contextDir, buildOnDeploy: gitStacks.buildOnDeploy, + noBuildCache: gitStacks.noBuildCache, repullImages: gitStacks.repullImages, forceRedeploy: gitStacks.forceRedeploy, lastSync: gitStacks.lastSync, @@ -2314,7 +2328,9 @@ export async function getGitStack(id: number): Promise autoUpdateCron: row.autoUpdateCron, webhookEnabled: row.webhookEnabled, webhookSecret: row.webhookSecret, + contextDir: row.contextDir ?? null, buildOnDeploy: row.buildOnDeploy ?? false, + noBuildCache: row.noBuildCache ?? false, repullImages: row.repullImages ?? false, forceRedeploy: row.forceRedeploy ?? false, lastSync: row.lastSync, @@ -2346,7 +2362,9 @@ export async function getGitStackByName(stackName: string, environmentId?: numbe autoUpdateCron: gitStacks.autoUpdateCron, webhookEnabled: gitStacks.webhookEnabled, webhookSecret: gitStacks.webhookSecret, + contextDir: gitStacks.contextDir, buildOnDeploy: gitStacks.buildOnDeploy, + noBuildCache: gitStacks.noBuildCache, repullImages: gitStacks.repullImages, forceRedeploy: gitStacks.forceRedeploy, lastSync: gitStacks.lastSync, @@ -2383,7 +2401,9 @@ export async function getGitStackByName(stackName: string, environmentId?: numbe autoUpdateCron: row.autoUpdateCron, webhookEnabled: row.webhookEnabled, webhookSecret: row.webhookSecret, + contextDir: row.contextDir ?? null, buildOnDeploy: row.buildOnDeploy ?? false, + noBuildCache: row.noBuildCache ?? false, repullImages: row.repullImages ?? false, forceRedeploy: row.forceRedeploy ?? false, lastSync: row.lastSync, @@ -2415,7 +2435,9 @@ export async function getGitStackByWebhookSecret(secret: string): Promise { @@ -2487,12 +2513,14 @@ export async function createGitStack(data: { repositoryId: data.repositoryId, composePath: data.composePath || 'compose.yaml', envFilePath: data.envFilePath || null, + contextDir: data.contextDir || null, autoUpdate: data.autoUpdate || false, autoUpdateSchedule: data.autoUpdateSchedule || 'daily', autoUpdateCron: data.autoUpdateCron || '0 3 * * *', webhookEnabled: data.webhookEnabled || false, webhookSecret: data.webhookSecret || null, buildOnDeploy: data.buildOnDeploy ?? false, + noBuildCache: data.noBuildCache ?? false, repullImages: data.repullImages ?? false, forceRedeploy: data.forceRedeploy ?? false }).returning(); @@ -2511,7 +2539,9 @@ export async function updateGitStack(id: number, data: Partial): P if (data.autoUpdateCron !== undefined) updateData.autoUpdateCron = data.autoUpdateCron; if (data.webhookEnabled !== undefined) updateData.webhookEnabled = data.webhookEnabled; if (data.webhookSecret !== undefined) updateData.webhookSecret = data.webhookSecret; + if (data.contextDir !== undefined) updateData.contextDir = data.contextDir; if (data.buildOnDeploy !== undefined) updateData.buildOnDeploy = data.buildOnDeploy; + if (data.noBuildCache !== undefined) updateData.noBuildCache = data.noBuildCache; if (data.repullImages !== undefined) updateData.repullImages = data.repullImages; if (data.forceRedeploy !== undefined) updateData.forceRedeploy = data.forceRedeploy; if (data.lastSync !== undefined) updateData.lastSync = data.lastSync; @@ -2549,7 +2579,9 @@ export async function getEnabledAutoUpdateGitStacks(): Promise { autoUpdateCron: gitStacks.autoUpdateCron, webhookEnabled: gitStacks.webhookEnabled, webhookSecret: gitStacks.webhookSecret, + contextDir: gitStacks.contextDir, buildOnDeploy: gitStacks.buildOnDeploy, + noBuildCache: gitStacks.noBuildCache, repullImages: gitStacks.repullImages, forceRedeploy: gitStacks.forceRedeploy, lastSync: gitStacks.lastSync, @@ -2639,7 +2675,9 @@ export async function getAllAutoUpdateGitStacks(): Promise { autoUpdateCron: row.autoUpdateCron, webhookEnabled: row.webhookEnabled, webhookSecret: row.webhookSecret, + contextDir: row.contextDir ?? null, buildOnDeploy: row.buildOnDeploy ?? false, + noBuildCache: row.noBuildCache ?? false, repullImages: row.repullImages ?? false, forceRedeploy: row.forceRedeploy ?? false, lastSync: row.lastSync, @@ -4057,8 +4095,11 @@ const SCHEDULE_CLEANUP_CRON_KEY = 'schedule_cleanup_cron'; const EVENT_CLEANUP_CRON_KEY = 'event_cleanup_cron'; const SCHEDULE_CLEANUP_ENABLED_KEY = 'schedule_cleanup_enabled'; const EVENT_CLEANUP_ENABLED_KEY = 'event_cleanup_enabled'; +const SCANNER_CLEANUP_CRON_KEY = 'scanner_cleanup_cron'; +const SCANNER_CLEANUP_ENABLED_KEY = 'scanner_cleanup_enabled'; const DEFAULT_SCHEDULE_CLEANUP_CRON = '0 3 * * *'; // Daily at 3 AM const DEFAULT_EVENT_CLEANUP_CRON = '30 3 * * *'; // Daily at 3:30 AM +const DEFAULT_SCANNER_CLEANUP_CRON = '0 3 * * 0'; // Weekly Sunday at 3 AM export async function getScheduleRetentionDays(): Promise { const result = await db.select().from(settings).where(eq(settings.key, SCHEDULE_RETENTION_KEY)); @@ -4192,6 +4233,50 @@ export async function setEventCleanupEnabled(enabled: boolean): Promise { } } +export async function getScannerCleanupCron(): Promise { + const result = await db.select().from(settings).where(eq(settings.key, SCANNER_CLEANUP_CRON_KEY)); + if (result[0]) { + return result[0].value || DEFAULT_SCANNER_CLEANUP_CRON; + } + return DEFAULT_SCANNER_CLEANUP_CRON; +} + +export async function setScannerCleanupCron(cron: string): Promise { + const existing = await db.select().from(settings).where(eq(settings.key, SCANNER_CLEANUP_CRON_KEY)); + if (existing.length > 0) { + await db.update(settings) + .set({ value: cron, updatedAt: new Date().toISOString() }) + .where(eq(settings.key, SCANNER_CLEANUP_CRON_KEY)); + } else { + await db.insert(settings).values({ + key: SCANNER_CLEANUP_CRON_KEY, + value: cron + }); + } +} + +export async function getScannerCleanupEnabled(): Promise { + const result = await db.select().from(settings).where(eq(settings.key, SCANNER_CLEANUP_ENABLED_KEY)); + if (result[0]) { + return result[0].value === 'true'; + } + return true; // Enabled by default +} + +export async function setScannerCleanupEnabled(enabled: boolean): Promise { + const existing = await db.select().from(settings).where(eq(settings.key, SCANNER_CLEANUP_ENABLED_KEY)); + if (existing.length > 0) { + await db.update(settings) + .set({ value: enabled ? 'true' : 'false', updatedAt: new Date().toISOString() }) + .where(eq(settings.key, SCANNER_CLEANUP_ENABLED_KEY)); + } else { + await db.insert(settings).values({ + key: SCANNER_CLEANUP_ENABLED_KEY, + value: enabled ? 'true' : 'false' + }); + } +} + // ============================================================================= // EXTERNAL STACK PATHS // ============================================================================= diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 8c73eaa..941bd0d 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -315,7 +315,9 @@ export const gitStacks = sqliteTable('git_stacks', { autoUpdateCron: text('auto_update_cron').default('0 3 * * *'), webhookEnabled: integer('webhook_enabled', { mode: 'boolean' }).default(false), webhookSecret: text('webhook_secret'), + contextDir: text('context_dir'), // Working directory relative to repo root (null = compose file's directory) buildOnDeploy: integer('build_on_deploy', { mode: 'boolean' }).default(false), + noBuildCache: integer('no_build_cache', { mode: 'boolean' }).default(false), repullImages: integer('repull_images', { mode: 'boolean' }).default(false), forceRedeploy: integer('force_redeploy', { mode: 'boolean' }).default(false), lastSync: text('last_sync'), diff --git a/src/lib/server/db/schema/pg-schema.ts b/src/lib/server/db/schema/pg-schema.ts index 23cc370..13cb591 100644 --- a/src/lib/server/db/schema/pg-schema.ts +++ b/src/lib/server/db/schema/pg-schema.ts @@ -318,7 +318,9 @@ export const gitStacks = pgTable('git_stacks', { autoUpdateCron: text('auto_update_cron').default('0 3 * * *'), webhookEnabled: boolean('webhook_enabled').default(false), webhookSecret: text('webhook_secret'), + contextDir: text('context_dir'), // Working directory relative to repo root (null = compose file's directory) buildOnDeploy: boolean('build_on_deploy').default(false), + noBuildCache: boolean('no_build_cache').default(false), repullImages: boolean('repull_images').default(false), forceRedeploy: boolean('force_redeploy').default(false), lastSync: timestamp('last_sync', { mode: 'string' }), diff --git a/src/lib/server/docker.ts b/src/lib/server/docker.ts index fc83290..e8ca5da 100644 --- a/src/lib/server/docker.ts +++ b/src/lib/server/docker.ts @@ -1248,6 +1248,12 @@ export interface CreateContainerOptions { networkIpv6Address?: string; /** Gateway priority for the primary network (Docker Engine 28+) */ networkGwPriority?: number; + /** Per-network endpoint configuration (IPv4, IPv6, aliases) */ + networkConfigs?: Record; user?: string | null; privileged?: boolean; healthcheck?: HealthcheckConfig | null; @@ -1431,10 +1437,25 @@ export async function createContainer(options: CreateContainerOptions, envId?: n for (const network of options.networks) { const isFirstNetwork = network === options.networks[0]; + const netCfg = options.networkConfigs?.[network]; const endpointConfig: any = {}; - // Apply aliases, static IP, and gateway priority only to the first (primary) network - if (isFirstNetwork) { + // Per-network config from networkConfigs (takes precedence) + if (netCfg) { + if (netCfg.aliases && netCfg.aliases.length > 0) { + endpointConfig.Aliases = netCfg.aliases; + } + if (netCfg.ipv4Address || netCfg.ipv6Address) { + endpointConfig.IPAMConfig = {}; + if (netCfg.ipv4Address) { + endpointConfig.IPAMConfig.IPv4Address = netCfg.ipv4Address; + } + if (netCfg.ipv6Address) { + endpointConfig.IPAMConfig.IPv6Address = netCfg.ipv6Address; + } + } + } else if (isFirstNetwork) { + // Backward compat: apply flat fields to first network if no networkConfigs if (options.networkAliases && options.networkAliases.length > 0) { endpointConfig.Aliases = options.networkAliases; } @@ -1617,9 +1638,22 @@ export async function createContainer(options: CreateContainerOptions, envId?: n containerConfig.StopTimeout = options.stopTimeout; } - // MAC address + // MAC address — set both top-level (API <1.44) and endpoint config (API 1.44+) if (options.macAddress) { containerConfig.MacAddress = options.macAddress; + + // For Docker API 1.44+, MacAddress must be in EndpointConfig + const primaryNetwork = options.networks?.[0] || options.networkMode || 'bridge'; + if (containerConfig.NetworkingConfig?.EndpointsConfig?.[primaryNetwork]) { + containerConfig.NetworkingConfig.EndpointsConfig[primaryNetwork].MacAddress = options.macAddress; + } else { + containerConfig.NetworkingConfig = containerConfig.NetworkingConfig || { EndpointsConfig: {} }; + containerConfig.NetworkingConfig.EndpointsConfig = containerConfig.NetworkingConfig.EndpointsConfig || {}; + containerConfig.NetworkingConfig.EndpointsConfig[primaryNetwork] = { + ...containerConfig.NetworkingConfig.EndpointsConfig[primaryNetwork], + MacAddress: options.macAddress + }; + } } // Extra hosts (/etc/hosts entries) @@ -2325,11 +2359,15 @@ export async function updateContainer(id: string, options: Partial k.startsWith('com.docker.')) + ), + ...options.labels + } + : existingOptions.labels }; // 1. Stop old container diff --git a/src/lib/server/git.ts b/src/lib/server/git.ts index 4ab59ee..2751210 100644 --- a/src/lib/server/git.ts +++ b/src/lib/server/git.ts @@ -110,21 +110,14 @@ if (!existsSync(GIT_REPOS_DIR)) { } /** - * Mask sensitive values in environment variables for safe logging. + * Redact all env var values for safe logging. Only key names are preserved. */ -function maskSecrets(vars: Record): Record { - const masked: Record = {}; - const secretPatterns = /password|secret|token|key|api_key|apikey|auth|credential|private/i; - for (const [key, value] of Object.entries(vars)) { - if (secretPatterns.test(key)) { - masked[key] = '***'; - } else if (value.length > 50) { - masked[key] = value.substring(0, 10) + '...(truncated)'; - } else { - masked[key] = value; - } +function redactEnvVarsForLog(vars: Record): Record { + const redacted: Record = {}; + for (const key of Object.keys(vars)) { + redacted[key] = '***'; } - return masked; + return redacted; } function getRepoPath(repoId: number): string { @@ -843,15 +836,15 @@ export async function syncGitStack(stackId: number): Promise { // (e.g., config files, scripts, additional env files) let changedFiles: string[] = []; if (commitChanged) { - // Get the directory containing the compose file (relative to repo root) - const composeDirRelative = dirname(gitStack.composePath); - console.log(`${logPrefix} Checking for changes in directory: ${composeDirRelative || '(root)'}`); + // Use contextDir if set, otherwise fall back to compose file's directory + const diffDirRelative = gitStack.contextDir || dirname(gitStack.composePath); + console.log(`${logPrefix} Checking for changes in directory: ${diffDirRelative || '(root)'}`); const diffResult = await getChangedFilesInDir( repoPath, previousCommit, newCommit, - composeDirRelative || '.', + diffDirRelative || '.', env ); @@ -893,10 +886,29 @@ export async function syncGitStack(stackId: number): Promise { console.log(`${logPrefix} Compose content:`); console.log(composeContent); - // Determine the compose directory and filename (for copying all files) - const composeDir = dirname(composePath); - const composeFileName = basename(gitStack.composePath); // e.g., "docker-compose.yaml" - console.log(`${logPrefix} Compose directory:`, composeDir); + // Determine the source directory and compose filename + // If contextDir is set, use it as the source directory (relative to repo root) + // and compute composeFileName as relative path from contextDir to compose file + let composeDir: string; + let composeFileName: string; + if (gitStack.contextDir) { + const contextDirAbsolute = resolve(repoPath, gitStack.contextDir); + // Validate: context dir must be within repo + if (!contextDirAbsolute.startsWith(repoPath)) { + throw new Error('Context directory must be within the repository'); + } + // Validate: compose file must be within context directory + const relCompose = relative(contextDirAbsolute, composePath); + if (relCompose.startsWith('..')) { + throw new Error('Compose file must be within the context directory'); + } + composeDir = contextDirAbsolute; + composeFileName = relCompose; // e.g., "apps/myapp/compose.yaml" + } else { + composeDir = dirname(composePath); + composeFileName = basename(gitStack.composePath); // e.g., "docker-compose.yaml" + } + console.log(`${logPrefix} Source directory (composeDir):`, composeDir); console.log(`${logPrefix} Compose filename:`, composeFileName); // Read env file if configured (optional - don't fail if missing) @@ -998,7 +1010,7 @@ export async function deployGitStack(stackId: number, options?: { force?: boolea console.log(`${logPrefix} Sync result - env file vars:`, syncResult.envFileVars ? Object.keys(syncResult.envFileVars).length : 0); if (syncResult.envFileVars && Object.keys(syncResult.envFileVars).length > 0) { console.log(`${logPrefix} Env file var keys:`, Object.keys(syncResult.envFileVars).join(', ')); - console.log(`${logPrefix} Env file vars (masked):`, JSON.stringify(maskSecrets(syncResult.envFileVars), null, 2)); + console.log(`${logPrefix} Env file vars (masked):`, JSON.stringify(redactEnvVarsForLog(syncResult.envFileVars), null, 2)); } // Check if there are changes - skip redeploy if no changes and not forced @@ -1038,6 +1050,7 @@ export async function deployGitStack(stackId: number, options?: { force?: boolea envFileName: syncResult.envFileName, // Env file relative to compose dir (for --env-file flag, optional) forceRecreate, build: gitStack.buildOnDeploy, + noBuildCache: gitStack.noBuildCache, pullPolicy: gitStack.repullImages ? 'always' : undefined }); @@ -1214,15 +1227,15 @@ export async function deployGitStackWithProgress( // Normalize to 7-char short hash for comparison (DB stores 7-char, git returns 40-char) const commitChanged = previousCommit?.substring(0, 7) !== newCommit.substring(0, 7); - // Check if any files in the compose file's directory have changed + // Check if any files in the context/compose directory have changed // (for consistency with syncGitStack, though this function always deploys) if (commitChanged) { - const composeDir = dirname(gitStack.composePath); + const diffDir = gitStack.contextDir || dirname(gitStack.composePath); const diffResult = await getChangedFilesInDir( repoPath, previousCommit, newCommit, - composeDir || '.', + diffDir || '.', env ); updated = diffResult.changed; @@ -1243,8 +1256,24 @@ export async function deployGitStackWithProgress( const composeContent = readFileSync(composePath, 'utf-8'); - // Determine the compose directory (for copying all files) - const composeDir = dirname(composePath); + // Determine the source directory and compose filename + let composeDir: string; + let progressComposeFileName: string; + if (gitStack.contextDir) { + const contextDirAbsolute = resolve(repoPath, gitStack.contextDir); + if (!contextDirAbsolute.startsWith(repoPath)) { + throw new Error('Context directory must be within the repository'); + } + const relCompose = relative(contextDirAbsolute, composePath); + if (relCompose.startsWith('..')) { + throw new Error('Compose file must be within the context directory'); + } + composeDir = contextDirAbsolute; + progressComposeFileName = relCompose; + } else { + composeDir = dirname(composePath); + progressComposeFileName = basename(gitStack.composePath); + } // Read env file if configured (optional - don't fail if missing) let envFileVars: Record | undefined; @@ -1291,16 +1320,17 @@ export async function deployGitStackWithProgress( compose: composeContent, envId: gitStack.environmentId, sourceDir: composeDir, // Copy entire directory from git repo - composeFileName: basename(gitStack.composePath), // Use original compose filename from repo + composeFileName: progressComposeFileName, // Compose filename relative to source dir envFileName, // Env file relative to compose dir (for --env-file flag, optional) build: gitStack.buildOnDeploy, + noBuildCache: gitStack.noBuildCache, pullPolicy: gitStack.repullImages ? 'always' : undefined }); if (result.success) { // Record the stack source with resolved compose path for consistency const stackDir = await getStackDir(gitStack.stackName, gitStack.environmentId); - const resolvedComposePath = join(stackDir, basename(gitStack.composePath)); + const resolvedComposePath = join(stackDir, progressComposeFileName); await upsertStackSource({ stackName: gitStack.stackName, @@ -1431,7 +1461,7 @@ export function parseEnvFileContent(content: string, stackName?: string): Record console.log(`${logPrefix} Parsed env vars count:`, Object.keys(result).length); console.log(`${logPrefix} Parsed env var keys:`, Object.keys(result).join(', ')); - console.log(`${logPrefix} Parsed env vars (masked):`, JSON.stringify(maskSecrets(result), null, 2)); + console.log(`${logPrefix} Parsed env vars (masked):`, JSON.stringify(redactEnvVarsForLog(result), null, 2)); if (skippedLines.length > 0) { console.log(`${logPrefix} Skipped lines (${skippedLines.length}):`, skippedLines.slice(0, 10).join('; ')); } diff --git a/src/lib/server/notifications.ts b/src/lib/server/notifications.ts index 299c0fa..06921b3 100644 --- a/src/lib/server/notifications.ts +++ b/src/lib/server/notifications.ts @@ -345,7 +345,10 @@ async function sendNtfy(appriseUrl: string, payload: NotificationPayload): Promi // Supported formats: // ntfy://topic (public ntfy.sh) // ntfy://host/topic (custom server, no auth) - // ntfy://user:pass@host/topic (custom server with auth) + // ntfy://user:pass@host/topic (custom server with basic auth) + // ntfy://token@host/topic (custom server with bearer token) + // ntfy://host/topic?auth=BASE64 (custom server with base64-encoded bearer token) + // Query params: ?tags=ship,whale &title=Custom &priority=5 // ntfys:// variants for HTTPS const isSecure = appriseUrl.startsWith('ntfys'); const path = appriseUrl.replace(/^ntfys?:\/\//, ''); @@ -353,37 +356,60 @@ async function sendNtfy(appriseUrl: string, payload: NotificationPayload): Promi let url: string; let authHeader: string | null = null; + // Extract query parameters (?auth=, ?tags=, ?title=, ?priority=) + let queryAuth: string | null = null; + let queryTags: string | null = null; + let queryTitle: string | null = null; + let queryPriority: string | null = null; + let cleanPath = path; + const qIndex = path.indexOf('?'); + if (qIndex !== -1) { + const params = new URLSearchParams(path.substring(qIndex + 1)); + queryAuth = params.get('auth'); + queryTags = params.get('tags'); + queryTitle = params.get('title'); + queryPriority = params.get('priority'); + cleanPath = path.substring(0, qIndex); + } + // Check for user:pass@host/topic format (Basic auth) - const basicMatch = path.match(/^([^:]+):([^@]+)@(.+)$/); + const basicMatch = cleanPath.match(/^([^:]+):([^@]+)@(.+)$/); if (basicMatch) { const [, user, pass, hostAndTopic] = basicMatch; const basic = Buffer.from(`${user}:${pass}`).toString('base64'); authHeader = `Basic ${basic}`; url = `${isSecure ? 'https' : 'http'}://${hostAndTopic}`; - } else if (path.includes('@') && path.includes('/')) { + } else if (cleanPath.includes('@') && cleanPath.includes('/')) { // token@host/topic -> Bearer token auth - const tokenMatch = path.match(/^([^@]+)@(.+)$/); + const tokenMatch = cleanPath.match(/^([^@]+)@(.+)$/); if (tokenMatch) { const [, token, hostAndTopic] = tokenMatch; authHeader = `Bearer ${token}`; url = `${isSecure ? 'https' : 'http'}://${hostAndTopic}`; } else { // Fallback to custom server without auth - url = `${isSecure ? 'https' : 'http'}://${path}`; + url = `${isSecure ? 'https' : 'http'}://${cleanPath}`; } - } else if (path.includes('/')) { + } else if (cleanPath.includes('/')) { // Custom server without auth - url = `${isSecure ? 'https' : 'http'}://${path}`; + url = `${isSecure ? 'https' : 'http'}://${cleanPath}`; } else { // Default ntfy.sh - url = `https://ntfy.sh/${path}`; + url = `https://ntfy.sh/${cleanPath}`; + } + + // Apply ?auth= as fallback if no explicit auth was set + if (!authHeader && queryAuth) { + const decoded = Buffer.from(queryAuth, 'base64').toString(); + authHeader = decoded.startsWith('Bearer ') ? decoded : `Bearer ${decoded}`; } const titleWithEnv = payload.environmentName ? `${payload.title} [${payload.environmentName}]` : payload.title; + const defaultTags = payload.type || 'info'; const headers: Record = { - 'Title': titleWithEnv, - 'Priority': payload.type === 'error' ? '5' : payload.type === 'warning' ? '4' : '3', - 'Tags': payload.type || 'info' + 'Title': queryTitle || titleWithEnv, + 'Priority': queryPriority || (payload.type === 'error' ? '5' : payload.type === 'warning' ? '4' : '3'), + 'Tags': queryTags ? `${queryTags},${defaultTags}` : defaultTags }; if (authHeader) { diff --git a/src/lib/server/scheduler/index.ts b/src/lib/server/scheduler/index.ts index 1243c7b..f323515 100644 --- a/src/lib/server/scheduler/index.ts +++ b/src/lib/server/scheduler/index.ts @@ -17,10 +17,12 @@ import { getGitStack, getScheduleCleanupCron, getEventCleanupCron, + getScannerCleanupCron, getScheduleRetentionDays, getEventRetentionDays, getScheduleCleanupEnabled, getEventCleanupEnabled, + getScannerCleanupEnabled, getEnvironments, getEnvUpdateCheckSettings, getAllEnvUpdateCheckSettings, @@ -64,6 +66,31 @@ let scannerCacheCleanupJob: Cron | null = null; // Scheduler state let isRunning = false; +/** + * Scanner cache cleanup function that cleans local and all remote environments. + * Shared between cron job, timezone refresh, and manual trigger. + */ +async function scannerCleanupAllEnvs(): Promise<{ volumes: string[]; dirs: string[] }> { + const { cleanupScannerCache } = await import('../scanner'); + const envs = await getEnvironments(); + + // Clean local cache (volumes + bind mount dirs) + const localResult = await cleanupScannerCache(); + + // Clean remote environment volumes + for (const env of envs) { + try { + const envResult = await cleanupScannerCache(env.id); + localResult.volumes.push(...envResult.volumes); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + console.log(`[Scanner] Skipping cache cleanup for env "${env.name}" (id=${env.id}): ${msg}`); + } + } + + return localResult; +} + /** * Clean up stale 'syncing' states from git stacks. * Called on startup to recover from crashes during sync operations. @@ -107,6 +134,7 @@ export async function startScheduler(): Promise { // Get cron expressions and default timezone from database const scheduleCleanupCron = await getScheduleCleanupCron(); const eventCleanupCron = await getEventCleanupCron(); + const scannerCleanupCron = await getScannerCleanupCron(); const defaultTimezone = await getDefaultTimezone(); // Start system cleanup jobs (static schedules with default timezone) @@ -134,35 +162,18 @@ export async function startScheduler(): Promise { await runVolumeHelperCleanupJob('cron', volumeCleanupFns); }); - // Scanner cache cleanup runs weekly (Sunday 3am) to prevent DB volume bloat - const scannerCleanupFn = async () => { - const { cleanupScannerCache } = await import('../scanner'); - const envs = await getEnvironments(); - - // Clean local cache (volumes + bind mount dirs) - const localResult = await cleanupScannerCache(); - - // Clean remote environment volumes - for (const env of envs) { - try { - const envResult = await cleanupScannerCache(env.id); - localResult.volumes.push(...envResult.volumes); - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - console.log(`[Scanner] Skipping cache cleanup for env "${env.name}" (id=${env.id}): ${msg}`); - } - } - - return localResult; - }; - scannerCacheCleanupJob = new Cron('0 3 * * 0', { timezone: defaultTimezone, legacyMode: false }, async () => { - await runScannerCacheCleanupJob('cron', scannerCleanupFn); - }); + // Scanner cache cleanup to prevent DB volume bloat (configurable schedule) + const scannerCleanupEnabled = await getScannerCleanupEnabled(); + if (scannerCleanupEnabled) { + scannerCacheCleanupJob = new Cron(scannerCleanupCron, { timezone: defaultTimezone, legacyMode: false }, async () => { + await runScannerCacheCleanupJob('cron', scannerCleanupAllEnvs); + }); + } console.log(`[Scheduler] System schedule cleanup: ${scheduleCleanupCron} [${defaultTimezone}]`); console.log(`[Scheduler] System event cleanup: ${eventCleanupCron} [${defaultTimezone}]`); console.log(`[Scheduler] Volume helper cleanup: every 30 minutes [${defaultTimezone}]`); - console.log(`[Scheduler] Scanner cache cleanup: weekly (Sunday 3am) [${defaultTimezone}]`); + console.log(`[Scheduler] Scanner cache cleanup: ${scannerCleanupEnabled ? scannerCleanupCron : 'disabled'} [${defaultTimezone}]`); // Register all dynamic schedules from database await refreshAllSchedules(); @@ -497,6 +508,8 @@ export async function refreshSystemJobs(): Promise { // Get current settings const scheduleCleanupCron = await getScheduleCleanupCron(); const eventCleanupCron = await getEventCleanupCron(); + const scannerCleanupCron = await getScannerCleanupCron(); + const scannerCleanupEnabled = await getScannerCleanupEnabled(); const defaultTimezone = await getDefaultTimezone(); // Cleanup functions to pass to the job @@ -536,18 +549,16 @@ export async function refreshSystemJobs(): Promise { await runVolumeHelperCleanupJob('cron', volumeCleanupFns); }); - const scannerCleanupFn = async () => { - const { cleanupScannerCache } = await import('../scanner'); - return cleanupScannerCache(); - }; - scannerCacheCleanupJob = new Cron('0 3 * * 0', { timezone: defaultTimezone, legacyMode: false }, async () => { - await runScannerCacheCleanupJob('cron', scannerCleanupFn); - }); + if (scannerCleanupEnabled) { + scannerCacheCleanupJob = new Cron(scannerCleanupCron, { timezone: defaultTimezone, legacyMode: false }, async () => { + await runScannerCacheCleanupJob('cron', scannerCleanupAllEnvs); + }); + } console.log(`[Scheduler] System schedule cleanup: ${scheduleCleanupCron} [${defaultTimezone}]`); console.log(`[Scheduler] System event cleanup: ${eventCleanupCron} [${defaultTimezone}]`); console.log(`[Scheduler] Volume helper cleanup: every 30 minutes [${defaultTimezone}]`); - console.log(`[Scheduler] Scanner cache cleanup: weekly (Sunday 3am) [${defaultTimezone}]`); + console.log(`[Scheduler] Scanner cache cleanup: ${scannerCleanupEnabled ? scannerCleanupCron : 'disabled'} [${defaultTimezone}]`); } // ============================================================================= @@ -682,11 +693,7 @@ export async function triggerSystemJob(jobId: string): Promise<{ success: boolea }); return { success: true }; } else if (jobId === String(SYSTEM_SCANNER_CLEANUP_ID) || jobId === 'scanner-cache-cleanup') { - const scannerCleanupFn = async () => { - const { cleanupScannerCache } = await import('../scanner'); - return cleanupScannerCache(); - }; - runScannerCacheCleanupJob('manual', scannerCleanupFn); + runScannerCacheCleanupJob('manual', scannerCleanupAllEnvs); return { success: true }; } else { return { success: false, error: 'Unknown system job ID' }; @@ -712,8 +719,10 @@ export async function getSystemSchedules(): Promise { const eventRetention = await getEventRetentionDays(); const scheduleCleanupCron = await getScheduleCleanupCron(); const eventCleanupCron = await getEventCleanupCron(); + const scannerCleanupCron = await getScannerCleanupCron(); const scheduleCleanupEnabled = await getScheduleCleanupEnabled(); const eventCleanupEnabled = await getEventCleanupEnabled(); + const scannerCleanupEnabled = await getScannerCleanupEnabled(); return [ { @@ -751,10 +760,10 @@ export async function getSystemSchedules(): Promise { type: 'system_cleanup' as const, name: 'Scanner cache cleanup', description: 'Removes scanner vulnerability database cache to reclaim disk space', - cronExpression: '0 3 * * 0', - nextRun: getNextRun('0 3 * * 0')?.toISOString() ?? null, + cronExpression: scannerCleanupCron, + nextRun: scannerCleanupEnabled ? getNextRun(scannerCleanupCron)?.toISOString() ?? null : null, isSystem: true, - enabled: true + enabled: scannerCleanupEnabled } ]; } diff --git a/src/lib/server/scheduler/tasks/system-cleanup.ts b/src/lib/server/scheduler/tasks/system-cleanup.ts index 715c4ca..47a1703 100644 --- a/src/lib/server/scheduler/tasks/system-cleanup.ts +++ b/src/lib/server/scheduler/tasks/system-cleanup.ts @@ -11,6 +11,7 @@ import { getEventRetentionDays, getScheduleCleanupEnabled, getEventCleanupEnabled, + getScannerCleanupEnabled, createScheduleExecution, updateScheduleExecution, appendScheduleExecutionLog @@ -210,6 +211,14 @@ export async function runScannerCacheCleanupJob( triggeredBy: ScheduleTrigger = 'cron', cleanupFn?: () => Promise<{ volumes: string[]; dirs: string[] }> ): Promise { + // Check if cleanup is enabled (skip check if manually triggered) + if (triggeredBy === 'cron') { + const enabled = await getScannerCleanupEnabled(); + if (!enabled) { + return; // Skip execution if disabled + } + } + const startTime = Date.now(); const execution = await createScheduleExecution({ diff --git a/src/lib/server/stack-scanner.ts b/src/lib/server/stack-scanner.ts index fdf1557..100b743 100644 --- a/src/lib/server/stack-scanner.ts +++ b/src/lib/server/stack-scanner.ts @@ -60,19 +60,20 @@ async function isComposeFile(filePath: string): Promise { } /** - * Count the number of services defined in a compose file - * Parses YAML to reliably count top-level keys under 'services:' section + * Parse compose file metadata: top-level `name` property and service count. + * The `name` property (if present) should be used as the stack name instead of the directory name, + * matching Docker Compose's behavior with `com.docker.compose.project`. */ -async function countServices(filePath: string): Promise { +function parseComposeMetadata(filePath: string): { name: string | null; serviceCount: number } { try { const content = readFileSync(filePath, 'utf-8'); const doc = yaml.load(content) as Record | null; - if (doc?.services && typeof doc.services === 'object') { - return Object.keys(doc.services).length; - } - return 0; + const name = typeof doc?.name === 'string' ? doc.name.trim() : null; + const serviceCount = doc?.services && typeof doc.services === 'object' + ? Object.keys(doc.services).length : 0; + return { name, serviceCount }; } catch { - return 0; + return { name: null, serviceCount: 0 }; } } @@ -122,13 +123,12 @@ async function scanPath(basePath: string): Promise<{ stacks: DiscoveredStack[]; for (const pattern of COMPOSE_PATTERNS) { const composePath = join(currentPath, pattern); if (existsSync(composePath)) { - // Found a stack! Stack name = directory name - const stackName = normalizeStackName(basename(currentPath)); + // Found a stack! Use compose name property if defined, otherwise directory name + const { name: composeName, serviceCount } = parseComposeMetadata(composePath); + const stackName = normalizeStackName(composeName || basename(currentPath)); if (stackName) { // Check for .env file const envPath = join(currentPath, '.env'); - // Count services in compose file - const serviceCount = await countServices(composePath); discovered.push({ name: stackName, composePath, @@ -166,14 +166,13 @@ async function scanPath(basePath: string): Promise<{ stacks: DiscoveredStack[]; if (lowerName.endsWith('.yml') || lowerName.endsWith('.yaml')) { // Validate it's actually a compose file if (await isComposeFile(entryPath)) { + const { name: composeName, serviceCount } = parseComposeMetadata(entryPath); const stackName = normalizeStackName( - entry.name.replace(/\.(yml|yaml)$/i, '') + composeName || entry.name.replace(/\.(yml|yaml)$/i, '') ); if (stackName) { // Check for .env file in same directory const envPath = join(currentPath, '.env'); - // Count services in compose file - const serviceCount = await countServices(entryPath); discovered.push({ name: stackName, composePath: entryPath, @@ -214,8 +213,18 @@ export async function adoptStack( return { success: false, error: 'Already adopted' }; } + // If the compose file has a top-level `name:` property, prefer it over the passed name. + // This ensures Docker's project name (from the label) matches Dockhand's stack name. + let stackNameSource = stack.name; + if (stack.composePath && existsSync(stack.composePath)) { + const { name: composeName } = parseComposeMetadata(stack.composePath); + if (composeName) { + stackNameSource = composeName; + } + } + // Check for name conflict within the same environment - let finalName = normalizeStackName(stack.name); + let finalName = normalizeStackName(stackNameSource); const existingNames = new Set( existingSources .filter((s) => s.environmentId === environmentId) @@ -224,11 +233,12 @@ export async function adoptStack( if (existingNames.has(finalName)) { // Append suffix to make unique + const baseName = finalName; let suffix = 1; - while (existingNames.has(`${stack.name}-${suffix}`)) { + while (existingNames.has(`${baseName}-${suffix}`)) { suffix++; } - finalName = `${stack.name}-${suffix}`; + finalName = `${baseName}-${suffix}`; } // Create stack source record - use 'internal' since we know the file paths diff --git a/src/lib/server/stacks.ts b/src/lib/server/stacks.ts index dd77beb..5cbf54c 100644 --- a/src/lib/server/stacks.ts +++ b/src/lib/server/stacks.ts @@ -102,6 +102,7 @@ export interface DeployStackOptions { sourceDir?: string; // Directory to copy all files from (for git stacks) forceRecreate?: boolean; build?: boolean; // Build images before starting (--build) + noBuildCache?: boolean; // Disable build cache (--no-cache, requires --build) pullPolicy?: string; // Pull policy: 'always' | 'missing' | 'never' composePath?: string; // Custom compose file path (for adopted/imported stacks) envPath?: string; // Custom env file path (for adopted/imported stacks) @@ -259,23 +260,14 @@ async function readDirFilesAsMap(dirPath: string): Promise): Record { - const masked: Record = {}; - const secretPatterns = /password|secret|token|key|api_key|apikey|auth|credential|private/i; - for (const [key, value] of Object.entries(vars)) { - if (secretPatterns.test(key)) { - masked[key] = '***'; - } else if (value.length > 50) { - // Truncate long values that might be secrets - masked[key] = value.substring(0, 10) + '...(truncated)'; - } else { - masked[key] = value; - } +function redactEnvVarsForLog(vars: Record): Record { + const redacted: Record = {}; + for (const key of Object.keys(vars)) { + redacted[key] = '***'; } - return masked; + return redacted; } // ============================================================================= @@ -794,6 +786,7 @@ interface ComposeCommandOptions { envId?: number | null; forceRecreate?: boolean; build?: boolean; // Build images before starting (--build) + noBuildCache?: boolean; // Disable build cache (--no-cache, requires --build) pullPolicy?: string; // Pull policy: 'always' | 'missing' | 'never' removeVolumes?: boolean; stackFiles?: Record; // All files to send to Hawser @@ -857,6 +850,7 @@ async function executeLocalCompose( useOverrideFile?: boolean, serviceName?: string, build?: boolean, + noBuildCache?: boolean, pullPolicy?: string ): Promise { const logPrefix = `[Stack:${stackName}]`; @@ -1050,6 +1044,7 @@ async function executeLocalCompose( args.push('up', '-d', '--remove-orphans'); if (forceRecreate) args.push('--force-recreate'); if (build) args.push('--build'); + if (build && noBuildCache) args.push('--no-cache'); if (pullPolicy) args.push('--pull', pullPolicy); // If targeting a specific service, only update that service if (serviceName) { @@ -1094,7 +1089,7 @@ async function executeLocalCompose( console.log(`${logPrefix} Service name:`, serviceName ?? '(all services)'); console.log(`${logPrefix} Env vars count:`, envVars ? Object.keys(envVars).length : 0); if (envVars && Object.keys(envVars).length > 0) { - console.log(`${logPrefix} Env vars being injected (masked):`, JSON.stringify(maskSecrets(envVars), null, 2)); + console.log(`${logPrefix} Env vars being injected (masked):`, JSON.stringify(redactEnvVarsForLog(envVars), null, 2)); } // Login to registries before pulling images @@ -1226,6 +1221,7 @@ async function executeComposeViaHawser( serviceName?: string, composeFileName?: string, build?: boolean, + noBuildCache?: boolean, pullPolicy?: string ): Promise { const logPrefix = `[Stack:${stackName}]`; @@ -1249,7 +1245,7 @@ async function executeComposeViaHawser( console.log(`${logPrefix} Non-secret env vars count:`, envVars ? Object.keys(envVars).length : 0); console.log(`${logPrefix} Secret env vars count:`, secretCount); if (allEnvVars && Object.keys(allEnvVars).length > 0) { - console.log(`${logPrefix} All env vars being sent (masked):`, JSON.stringify(maskSecrets(allEnvVars), null, 2)); + console.log(`${logPrefix} All env vars being sent (masked):`, JSON.stringify(redactEnvVarsForLog(allEnvVars), null, 2)); } console.log(`${logPrefix} Compose content length:`, composeContent.length, 'chars'); console.log(`${logPrefix} Stack files count:`, stackFiles ? Object.keys(stackFiles).length : 0); @@ -1300,6 +1296,7 @@ async function executeComposeViaHawser( forceRecreate: forceRecreate || false, removeVolumes: removeVolumes || false, build: build || false, + noBuildCache: (build && noBuildCache) || false, pullPolicy: pullPolicy || '', registries, // Registry credentials for docker login serviceName // Target specific service only (with --no-deps) @@ -1368,7 +1365,7 @@ async function executeComposeCommand( envVars?: Record, secretVars?: Record ): Promise { - const { stackName, envId, forceRecreate, build, pullPolicy, removeVolumes, stackFiles, workingDir, composePath, envPath, useOverrideFile, serviceName, composeFileName } = options; + const { stackName, envId, forceRecreate, build, noBuildCache, pullPolicy, removeVolumes, stackFiles, workingDir, composePath, envPath, useOverrideFile, serviceName, composeFileName } = options; // Get environment configuration const env = envId ? await getEnvironment(envId) : null; @@ -1392,6 +1389,7 @@ async function executeComposeCommand( useOverrideFile, serviceName, build, + noBuildCache, pullPolicy ); } @@ -1456,6 +1454,7 @@ async function executeComposeCommand( serviceName, composeFileName, build, + noBuildCache, pullPolicy ); } @@ -1489,6 +1488,7 @@ async function executeComposeCommand( useOverrideFile, serviceName, build, + noBuildCache, pullPolicy ); } @@ -1512,6 +1512,7 @@ async function executeComposeCommand( useOverrideFile, serviceName, build, + noBuildCache, pullPolicy ); } @@ -2175,7 +2176,7 @@ export async function removeStack( * Uses stack locking to prevent concurrent deployments. */ export async function deployStack(options: DeployStackOptions): Promise { - const { name, compose, envId, sourceDir, forceRecreate, build, pullPolicy, composePath, envPath, composeFileName, envFileName } = options; + const { name, compose, envId, sourceDir, forceRecreate, build, noBuildCache, pullPolicy, composePath, envPath, composeFileName, envFileName } = options; const logPrefix = `[Stack:${name}]`; console.log(`${logPrefix} ========================================`); @@ -2253,7 +2254,11 @@ export async function deployStack(options: DeployStackOptions): Promise !src.includes('/.git/') && !src.endsWith('/.git') + }); console.log(`${logPrefix} Copied ${sourceDir} -> ${workingDir}`); } else { // Internal stack: check if a custom path exists in DB (adopted/imported stacks) @@ -2318,6 +2323,7 @@ export async function deployStack(options: DeployStackOptions): Promise= 12 ? 'PM' : 'AM'; - const hour12 = hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum; - const timeStr = `${hour12}:${minNum.toString().padStart(2, '0')} ${ampm}`; + const is12Hour = get(appSettings).timeFormat === '12h'; + + let timeStr: string; + if (is12Hour) { + const ampm = hourNum >= 12 ? 'PM' : 'AM'; + const hour12 = hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum; + timeStr = `${hour12}:${minNum.toString().padStart(2, '0')} ${ampm}`; + } else { + timeStr = `${hourNum.toString().padStart(2, '0')}:${minNum.toString().padStart(2, '0')}`; + } if (scheduleType === 'daily' || dow === '*') { return { label: 'daily', tooltip: `Daily at ${timeStr}` }; diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts index b5cd9e5..ef0b7a2 100644 --- a/src/lib/stores/settings.ts +++ b/src/lib/stores/settings.ts @@ -22,6 +22,8 @@ export interface AppSettings { eventCleanupCron: string; scheduleCleanupEnabled: boolean; eventCleanupEnabled: boolean; + scannerCleanupCron: string; + scannerCleanupEnabled: boolean; logBufferSizeKb: number; defaultTimezone: string; eventCollectionMode: EventCollectionMode; @@ -52,6 +54,8 @@ const DEFAULT_SETTINGS: AppSettings = { eventCleanupCron: '30 3 * * *', scheduleCleanupEnabled: true, eventCleanupEnabled: true, + scannerCleanupCron: '0 3 * * 0', + scannerCleanupEnabled: true, logBufferSizeKb: 500, defaultTimezone: 'UTC', eventCollectionMode: 'stream', @@ -113,6 +117,8 @@ function createSettingsStore() { eventCleanupCron: settings.eventCleanupCron ?? DEFAULT_SETTINGS.eventCleanupCron, scheduleCleanupEnabled: settings.scheduleCleanupEnabled ?? DEFAULT_SETTINGS.scheduleCleanupEnabled, eventCleanupEnabled: settings.eventCleanupEnabled ?? DEFAULT_SETTINGS.eventCleanupEnabled, + scannerCleanupCron: settings.scannerCleanupCron ?? DEFAULT_SETTINGS.scannerCleanupCron, + scannerCleanupEnabled: settings.scannerCleanupEnabled ?? DEFAULT_SETTINGS.scannerCleanupEnabled, logBufferSizeKb: settings.logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb, defaultTimezone: settings.defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone, eventCollectionMode: settings.eventCollectionMode ?? DEFAULT_SETTINGS.eventCollectionMode, @@ -160,6 +166,8 @@ function createSettingsStore() { eventCleanupCron: updatedSettings.eventCleanupCron ?? DEFAULT_SETTINGS.eventCleanupCron, scheduleCleanupEnabled: updatedSettings.scheduleCleanupEnabled ?? DEFAULT_SETTINGS.scheduleCleanupEnabled, eventCleanupEnabled: updatedSettings.eventCleanupEnabled ?? DEFAULT_SETTINGS.eventCleanupEnabled, + scannerCleanupCron: updatedSettings.scannerCleanupCron ?? DEFAULT_SETTINGS.scannerCleanupCron, + scannerCleanupEnabled: updatedSettings.scannerCleanupEnabled ?? DEFAULT_SETTINGS.scannerCleanupEnabled, logBufferSizeKb: updatedSettings.logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb, defaultTimezone: updatedSettings.defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone, eventCollectionMode: updatedSettings.eventCollectionMode ?? DEFAULT_SETTINGS.eventCollectionMode, @@ -297,6 +305,20 @@ function createSettingsStore() { return newSettings; }); }, + setScannerCleanupCron: (value: string) => { + update((current) => { + const newSettings = { ...current, scannerCleanupCron: value }; + saveSettings({ scannerCleanupCron: value }); + return newSettings; + }); + }, + setScannerCleanupEnabled: (value: boolean) => { + update((current) => { + const newSettings = { ...current, scannerCleanupEnabled: value }; + saveSettings({ scannerCleanupEnabled: value }); + return newSettings; + }); + }, setLogBufferSizeKb: (value: number) => { update((current) => { const newSettings = { ...current, logBufferSizeKb: value }; diff --git a/src/routes/api/containers/pending-updates/+server.ts b/src/routes/api/containers/pending-updates/+server.ts index 23ffdf0..eb278a4 100644 --- a/src/routes/api/containers/pending-updates/+server.ts +++ b/src/routes/api/containers/pending-updates/+server.ts @@ -1,7 +1,7 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { authorize } from '$lib/server/authorize'; -import { getPendingContainerUpdates, removePendingContainerUpdate } from '$lib/server/db'; +import { getPendingContainerUpdates, removePendingContainerUpdate, clearPendingContainerUpdates } from '$lib/server/db'; /** * Get pending container updates for an environment. @@ -48,8 +48,8 @@ export const DELETE: RequestHandler = async ({ url, cookies }) => { const containerId = url.searchParams.get('containerId'); const envIdNum = envId ? parseInt(envId) : undefined; - if (!envIdNum || !containerId) { - return json({ error: 'Environment ID and container ID required' }, { status: 400 }); + if (!envIdNum) { + return json({ error: 'Environment ID required' }, { status: 400 }); } // Need manage permission to delete @@ -58,7 +58,11 @@ export const DELETE: RequestHandler = async ({ url, cookies }) => { } try { - await removePendingContainerUpdate(envIdNum, containerId); + if (containerId) { + await removePendingContainerUpdate(envIdNum, containerId); + } else { + await clearPendingContainerUpdates(envIdNum); + } return json({ success: true }); } catch (error: any) { console.error('Error removing pending update:', error); diff --git a/src/routes/api/git/stacks/+server.ts b/src/routes/api/git/stacks/+server.ts index 35574fe..d067db6 100644 --- a/src/routes/api/git/stacks/+server.ts +++ b/src/routes/api/git/stacks/+server.ts @@ -7,7 +7,8 @@ import { getGitRepository, createGitRepository, upsertStackSource, - setStackEnvVars + setStackEnvVars, + getStackSource } from '$lib/server/db'; import { deployGitStack } from '$lib/server/git'; import { authorize } from '$lib/server/authorize'; @@ -61,6 +62,12 @@ export const POST: RequestHandler = async (event) => { return json({ error: 'Stack name must start with a letter or number, and contain only letters, numbers, hyphens, and underscores' }, { status: 400 }); } + // Check for name conflicts with existing stacks (regular/external/git) + const existing = await getStackSource(trimmedStackName, data.environmentId || null); + if (existing) { + return json({ error: 'A stack with this name already exists on this environment' }, { status: 409 }); + } + // Either repositoryId or new repo details (url, branch) must be provided let repositoryId = data.repositoryId; @@ -120,7 +127,9 @@ export const POST: RequestHandler = async (event) => { autoUpdateCron: data.autoUpdateCron || '0 3 * * *', webhookEnabled: data.webhookEnabled || false, webhookSecret: webhookSecret, + contextDir: data.contextDir || null, buildOnDeploy: data.buildOnDeploy ?? false, + noBuildCache: data.noBuildCache ?? false, repullImages: data.repullImages ?? false, forceRedeploy: data.forceRedeploy ?? false }); diff --git a/src/routes/api/git/stacks/[id]/+server.ts b/src/routes/api/git/stacks/[id]/+server.ts index 45223a5..ba29c6e 100644 --- a/src/routes/api/git/stacks/[id]/+server.ts +++ b/src/routes/api/git/stacks/[id]/+server.ts @@ -73,7 +73,9 @@ export const PUT: RequestHandler = async (event) => { autoUpdateCron: data.autoUpdateCron, webhookEnabled: data.webhookEnabled, webhookSecret: data.webhookSecret, + contextDir: data.contextDir, buildOnDeploy: data.buildOnDeploy, + noBuildCache: data.noBuildCache, repullImages: data.repullImages, forceRedeploy: data.forceRedeploy }); diff --git a/src/routes/api/registry/catalog/+server.ts b/src/routes/api/registry/catalog/+server.ts index e754f45..7c8c6f3 100644 --- a/src/routes/api/registry/catalog/+server.ts +++ b/src/routes/api/registry/catalog/+server.ts @@ -46,8 +46,8 @@ export const GET: RequestHandler = async ({ url }) => { }); if (!response.ok) { - if (response.status === 401) { - return json({ error: 'Authentication failed. Please check your credentials.' }, { status: 401 }); + if (response.status === 401 || response.status === 403) { + return json({ error: 'Catalog listing not available. This registry may not support the _catalog endpoint (common with GitLab and Harbor). Try searching for images by name instead.' }, { status: response.status }); } if (response.status === 404) { return json({ error: 'Registry does not support V2 catalog API' }, { status: 404 }); diff --git a/src/routes/api/registry/search/+server.ts b/src/routes/api/registry/search/+server.ts index 3f1a2f3..583803a 100644 --- a/src/routes/api/registry/search/+server.ts +++ b/src/routes/api/registry/search/+server.ts @@ -1,7 +1,7 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { getRegistry } from '$lib/server/db'; -import { getRegistryAuth } from '$lib/server/docker'; +import { getRegistryAuth, parseRegistryUrl } from '$lib/server/docker'; interface SearchResult { name: string; @@ -46,28 +46,50 @@ async function searchDockerHub(term: string, limit: number): Promise { const results: string[] = []; + const { orgPath } = parseRegistryUrl(registry.url); + const orgPrefix = orgPath ? orgPath.replace(/^\//, '') : ''; - // Strategy 1: If term looks like an image name (contains /), try direct lookup first - // This is much faster than iterating through catalog for large registries like ghcr.io + // Strategy 1: Direct image lookup — try the exact term and org-prefixed variants + // This uses per-repository auth scope which works on all V2 registries (GitLab, Harbor, etc.) + const directCandidates: string[] = []; if (term.includes('/')) { - const directResult = await tryDirectImageLookup(registry, term); - if (directResult) { - results.push(term); + directCandidates.push(term); + } + // If registry URL has an org path (e.g., https://registry.example.com/group), + // try prepending it to the search term + if (orgPrefix && !term.startsWith(orgPrefix + '/')) { + directCandidates.push(`${orgPrefix}/${term}`); + } + + for (const candidate of directCandidates) { + if (results.length >= limit) break; + const exists = await tryDirectImageLookup(registry, candidate); + if (exists && !results.includes(candidate)) { + results.push(candidate); } } - // Strategy 2: Fall back to catalog search for partial matches or if direct lookup failed + // Strategy 2: Fall back to catalog search for partial/fuzzy matches + // Some registries (GitLab, Harbor) don't support _catalog for deploy tokens, + // so catch errors gracefully and return whatever we have from direct lookup if (results.length < limit) { - const catalogResults = await searchCatalog(registry, term, limit - results.length); - // Add catalog results, avoiding duplicates - for (const name of catalogResults) { - if (!results.includes(name)) { - results.push(name); + try { + const catalogResults = await searchCatalog(registry, term, limit - results.length); + for (const name of catalogResults) { + if (!results.includes(name)) { + results.push(name); + } + } + } catch (e: any) { + // Catalog not supported but we have direct lookup results — that's fine + if (results.length > 0) { + console.warn(`[Registry] Catalog search failed (using direct lookup results): ${e.message}`); + } else { + throw e; } } } - // Return results in the same format as Docker Hub return results.map((name: string) => ({ name, description: '', @@ -136,8 +158,8 @@ async function searchCatalog(registry: any, term: string, limit: number): Promis }); if (!response.ok) { - if (response.status === 401) { - throw new Error('Authentication failed'); + if (response.status === 401 || response.status === 403) { + throw new Error('Authentication failed. This registry may not support catalog listing (common with GitLab and Harbor deploy tokens).'); } throw new Error(`Registry returned error: ${response.status}`); } diff --git a/src/routes/api/registry/tags/+server.ts b/src/routes/api/registry/tags/+server.ts index 6e76915..33609d4 100644 --- a/src/routes/api/registry/tags/+server.ts +++ b/src/routes/api/registry/tags/+server.ts @@ -93,8 +93,8 @@ async function fetchRegistryTags(registry: any, imageName: string): Promise { const auth = await authorize(cookies); @@ -32,6 +36,11 @@ export const POST: RequestHandler = async ({ params, cookies }) => { const currentEnabled = await getEventCleanupEnabled(); await setEventCleanupEnabled(!currentEnabled); return json({ success: true, enabled: !currentEnabled }); + } else if (systemId === SYSTEM_SCANNER_CLEANUP_ID) { + const currentEnabled = await getScannerCleanupEnabled(); + await setScannerCleanupEnabled(!currentEnabled); + await refreshSystemJobs(); + return json({ success: true, enabled: !currentEnabled }); } else { return json({ error: 'Unknown system schedule' }, { status: 400 }); } diff --git a/src/routes/api/settings/general/+server.ts b/src/routes/api/settings/general/+server.ts index c522b51..894ac02 100644 --- a/src/routes/api/settings/general/+server.ts +++ b/src/routes/api/settings/general/+server.ts @@ -14,6 +14,10 @@ import { setScheduleCleanupEnabled, getEventCleanupEnabled, setEventCleanupEnabled, + getScannerCleanupCron, + setScannerCleanupCron, + getScannerCleanupEnabled, + setScannerCleanupEnabled, getDefaultTimezone, setDefaultTimezone, getEventCollectionMode, @@ -52,6 +56,8 @@ export interface GeneralSettings { eventCleanupCron: string; scheduleCleanupEnabled: boolean; eventCleanupEnabled: boolean; + scannerCleanupCron: string; + scannerCleanupEnabled: boolean; logBufferSizeKb: number; defaultTimezone: string; // Background monitoring settings @@ -83,7 +89,7 @@ export interface GeneralSettings { labelFilterMode: 'any' | 'all'; } -const DEFAULT_SETTINGS: Omit = { +const DEFAULT_SETTINGS: Omit = { confirmDestructive: true, showStoppedContainers: true, highlightUpdates: true, @@ -165,6 +171,8 @@ export const GET: RequestHandler = async ({ cookies }) => { eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, + scannerCleanupCron, + scannerCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, @@ -200,6 +208,8 @@ export const GET: RequestHandler = async ({ cookies }) => { getEventCleanupCron(), getScheduleCleanupEnabled(), getEventCleanupEnabled(), + getScannerCleanupCron(), + getScannerCleanupEnabled(), getSetting('log_buffer_size_kb'), getDefaultTimezone(), getEventCollectionMode(), @@ -237,6 +247,8 @@ export const GET: RequestHandler = async ({ cookies }) => { eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, + scannerCleanupCron, + scannerCleanupEnabled, logBufferSizeKb: logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb, defaultTimezone: defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone, eventCollectionMode: (eventCollectionMode ?? DEFAULT_SETTINGS.eventCollectionMode) as EventCollectionMode, @@ -274,7 +286,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { try { const body = await request.json(); - const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode } = body; + const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode } = body; if (confirmDestructive !== undefined) { await setSetting('confirm_destructive', confirmDestructive); @@ -318,6 +330,14 @@ export const POST: RequestHandler = async ({ request, cookies }) => { if (eventCleanupEnabled !== undefined && typeof eventCleanupEnabled === 'boolean') { await setEventCleanupEnabled(eventCleanupEnabled); } + if (scannerCleanupCron !== undefined && typeof scannerCleanupCron === 'string') { + await setScannerCleanupCron(scannerCleanupCron); + await refreshSystemJobs(); + } + if (scannerCleanupEnabled !== undefined && typeof scannerCleanupEnabled === 'boolean') { + await setScannerCleanupEnabled(scannerCleanupEnabled); + await refreshSystemJobs(); + } if (logBufferSizeKb !== undefined && typeof logBufferSizeKb === 'number') { // Clamp to reasonable range: 100KB - 5000KB (5MB) await setSetting('log_buffer_size_kb', Math.max(100, Math.min(5000, logBufferSizeKb))); @@ -416,6 +436,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => { eventCleanupCronVal, scheduleCleanupEnabledVal, eventCleanupEnabledVal, + scannerCleanupCronVal, + scannerCleanupEnabledVal, logBufferSizeKbVal, defaultTimezoneVal, eventCollectionModeVal, @@ -451,6 +473,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => { getEventCleanupCron(), getScheduleCleanupEnabled(), getEventCleanupEnabled(), + getScannerCleanupCron(), + getScannerCleanupEnabled(), getSetting('log_buffer_size_kb'), getDefaultTimezone(), getEventCollectionMode(), @@ -488,6 +512,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => { eventCleanupCron: eventCleanupCronVal, scheduleCleanupEnabled: scheduleCleanupEnabledVal, eventCleanupEnabled: eventCleanupEnabledVal, + scannerCleanupCron: scannerCleanupCronVal, + scannerCleanupEnabled: scannerCleanupEnabledVal, logBufferSizeKb: logBufferSizeKbVal ?? DEFAULT_SETTINGS.logBufferSizeKb, defaultTimezone: defaultTimezoneVal ?? DEFAULT_SETTINGS.defaultTimezone, eventCollectionMode: (eventCollectionModeVal ?? DEFAULT_SETTINGS.eventCollectionMode) as EventCollectionMode, diff --git a/src/routes/api/volumes/[name]/export/+server.ts b/src/routes/api/volumes/[name]/export/+server.ts index 9042d2b..0e5259f 100644 --- a/src/routes/api/volumes/[name]/export/+server.ts +++ b/src/routes/api/volumes/[name]/export/+server.ts @@ -1,7 +1,7 @@ import { gzipSync } from 'node:zlib'; import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { getVolumeArchive } from '$lib/server/docker'; +import { getVolumeArchive, releaseVolumeHelperContainer } from '$lib/server/docker'; import { authorize } from '$lib/server/authorize'; import { validateDockerIdParam } from '$lib/server/docker-validation'; @@ -36,15 +36,33 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => { let body: ReadableStream | Uint8Array = response.body!; if (format === 'tar.gz') { - // Compress with gzip + // Compress with gzip — fully consumes the archive stream const tarData = new Uint8Array(await response.arrayBuffer()); body = gzipSync(tarData); contentType = 'application/gzip'; extension = '.tar.gz'; - } - // Note: Helper container is cached and reused for subsequent requests. - // Cache TTL handles cleanup automatically. + // Data fully read, release helper container immediately + releaseVolumeHelperContainer(params.name, envIdNum).catch(() => {}); + } else { + // For streaming tar, wrap the stream to release on completion + const reader = body.getReader(); + body = new ReadableStream({ + async pull(controller) { + const { done, value } = await reader.read(); + if (done) { + controller.close(); + releaseVolumeHelperContainer(params.name, envIdNum).catch(() => {}); + } else { + controller.enqueue(value); + } + }, + cancel() { + reader.cancel(); + releaseVolumeHelperContainer(params.name, envIdNum).catch(() => {}); + } + }); + } const headers: Record = { 'Content-Type': contentType, @@ -66,6 +84,9 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => { } catch (error: any) { console.error('Failed to export volume:', error); + // Best-effort cleanup on error + releaseVolumeHelperContainer(params.name, envIdNum).catch(() => {}); + if (error.message?.includes('No such file or directory')) { return json({ error: 'Path not found' }, { status: 404 }); } diff --git a/src/routes/containers/+page.svelte b/src/routes/containers/+page.svelte index 1bed158..92dec2b 100644 --- a/src/routes/containers/+page.svelte +++ b/src/routes/containers/+page.svelte @@ -473,7 +473,7 @@ // Unlock button width if (updateCheckBtnEl) updateCheckBtnEl.style.minWidth = ''; - const containersWithUpdates = data.results.filter((r: any) => r.hasUpdate && !r.systemContainer); + const containersWithUpdates = data.results.filter((r: any) => r.hasUpdate && !r.systemContainer && !r.updateDisabled && !r.isLocalImage); const failed = data.results.filter((r: any) => r.error && !r.hasUpdate); failedUpdateChecks = failed.map((r: any) => ({ containerName: r.containerName, @@ -542,6 +542,19 @@ showBatchUpdateModal = true; } + async function dismissPendingUpdates() { + const envId = $currentEnvironment?.id; + if (!envId) return; + try { + const response = await fetch(`/api/containers/pending-updates?env=${envId}`, { method: 'DELETE' }); + if (response.ok) { + containerStore.setPendingUpdates([], new Map()); + } + } catch { + toast.error('Failed to clear update indicators'); + } + } + function updateAllContainers() { if (batchUpdateContainerIds.length === 0) return; @@ -1442,6 +1455,12 @@ > Update all ({updatableContainersCount}) + {/if} {#if $canAccess('containers', 'remove')} diff --git a/src/routes/containers/BatchUpdateModal.svelte b/src/routes/containers/BatchUpdateModal.svelte index adedc3d..db19849 100644 --- a/src/routes/containers/BatchUpdateModal.svelte +++ b/src/routes/containers/BatchUpdateModal.svelte @@ -87,8 +87,12 @@ let forceUpdating = $state>(new Set()); // Track containers being force-updated let filterMode = $state<'updated' | 'failed'>('updated'); + let showFilterButtons = $derived( + summary && (summary.failed > 0 || summary.blocked > 0) && summary.success > 0 + ); + let filteredProgress = $derived( - !summary + !summary || !showFilterButtons ? progress : filterMode === 'failed' ? progress.filter(p => p.step === 'failed' || p.step === 'blocked') @@ -475,7 +479,7 @@ const severityOrder: Record = { critical: 0, high: 1, medium: 2, {#if progress.length > 0} - {#if summary && (summary.failed > 0 || summary.blocked > 0) && summary.success > 0} + {#if showFilterButtons}
+ +
+ {#if isExpanded && networkConfigs[networkName]} +
+
+
+ + + {#if validateIpv4(networkConfigs[networkName].ipv4Address)} +

{validateIpv4(networkConfigs[networkName].ipv4Address)}

+ {/if} +
+
+ + + {#if validateIpv6(networkConfigs[networkName].ipv6Address)} +

{validateIpv6(networkConfigs[networkName].ipv6Address)}

+ {/if} +
+
+
+ + +
+
{/if} - - +
{/each}
{/if} + + +
+ + + {#if validateMac(macAddress)} +

{validateMac(macAddress)}

+ {/if} +
+ {#if mode === 'edit'}

Container will be connected to selected networks in addition to the network mode above

{/if} @@ -851,7 +987,7 @@ size="icon" variant="ghost" onclick={() => removeLabel(index)} - disabled={labels.length === 1} + disabled={labels.length <= 1 && !labels[0]?.key} class="h-9 w-9 text-muted-foreground hover:text-destructive" > diff --git a/src/routes/containers/CreateContainerModal.svelte b/src/routes/containers/CreateContainerModal.svelte index d39e416..c7b5b07 100644 --- a/src/routes/containers/CreateContainerModal.svelte +++ b/src/routes/containers/CreateContainerModal.svelte @@ -112,6 +112,8 @@ } let availableNetworks = $state([]); let selectedNetworks = $state([]); + let networkConfigs = $state>({}); + let macAddress = $state(''); // Auto-update settings let autoUpdateEnabled = $state(false); @@ -431,6 +433,17 @@ deviceRequests = [dr]; } + // Build per-network configs for the API + const netConfigs: Record = {}; + for (const [netName, cfg] of Object.entries(networkConfigs)) { + if (!selectedNetworks.includes(netName)) continue; + const entry: { ipv4Address?: string; ipv6Address?: string; aliases?: string[] } = {}; + if (cfg.ipv4Address) entry.ipv4Address = cfg.ipv4Address; + if (cfg.ipv6Address) entry.ipv6Address = cfg.ipv6Address; + if (cfg.aliases) entry.aliases = cfg.aliases.split(',').map(a => a.trim()).filter(Boolean); + if (Object.keys(entry).length > 0) netConfigs[netName] = entry; + } + const payload = { name: name.trim(), image: image.trim(), @@ -443,6 +456,8 @@ restartMaxRetries: restartPolicy === 'on-failure' && restartMaxRetries !== '' ? Number(restartMaxRetries) : undefined, networkMode, networks: selectedNetworks.length > 0 ? selectedNetworks : undefined, + networkConfigs: Object.keys(netConfigs).length > 0 ? netConfigs : undefined, + macAddress: macAddress.trim() || undefined, startAfterCreate, user: containerUser.trim() || undefined, privileged: privilegedMode || undefined, @@ -530,6 +545,8 @@ envVars = [{ key: '', value: '' }]; labels = [{ key: '', value: '' }]; selectedNetworks = []; + networkConfigs = {}; + macAddress = ''; autoUpdateEnabled = false; autoUpdateCronExpression = '0 3 * * *'; vulnerabilityCriteria = 'never'; @@ -723,6 +740,8 @@ bind:labels {availableNetworks} bind:selectedNetworks + bind:networkConfigs + bind:macAddress bind:containerUser bind:privilegedMode bind:healthcheckEnabled diff --git a/src/routes/containers/EditContainerModal.svelte b/src/routes/containers/EditContainerModal.svelte index 9fbbdd4..e2d818b 100644 --- a/src/routes/containers/EditContainerModal.svelte +++ b/src/routes/containers/EditContainerModal.svelte @@ -113,6 +113,8 @@ } let availableNetworks = $state([]); let selectedNetworks = $state([]); + let networkConfigs = $state>({}); + let macAddress = $state(''); // User/Group let containerUser = $state(''); @@ -182,6 +184,8 @@ envVars: typeof envVars; labels: typeof labels; selectedNetworks: string[]; + networkConfigs: Record; + macAddress: string; // Advanced options containerUser: string; privilegedMode: boolean; @@ -428,6 +432,24 @@ const networks = data.NetworkSettings?.Networks || {}; selectedNetworks = Object.keys(networks); + // Parse per-network IP/alias config from NetworkSettings + const parsedNetConfigs: Record = {}; + for (const [netName, netData] of Object.entries(networks) as [string, any][]) { + const ipam = netData.IPAMConfig || {}; + const aliases = netData.Aliases || []; + if (ipam.IPv4Address || ipam.IPv6Address || aliases.length > 0) { + parsedNetConfigs[netName] = { + ipv4Address: ipam.IPv4Address || '', + ipv6Address: ipam.IPv6Address || '', + aliases: aliases.join(', ') + }; + } + } + networkConfigs = parsedNetConfigs; + + // Parse MAC address + macAddress = data.Config?.MacAddress || ''; + // Parse advanced options - User containerUser = data.Config.User || ''; @@ -561,6 +583,8 @@ envVars: JSON.parse(JSON.stringify(envVars)), labels: JSON.parse(JSON.stringify(labels)), selectedNetworks: [...selectedNetworks], + networkConfigs: JSON.parse(JSON.stringify(networkConfigs)), + macAddress, // Advanced options containerUser, privilegedMode, @@ -631,6 +655,8 @@ if (JSON.stringify(currentLabels) !== JSON.stringify(originalLabels)) return true; if (JSON.stringify([...selectedNetworks].sort()) !== JSON.stringify([...originalConfig.selectedNetworks].sort())) return true; + if (JSON.stringify(networkConfigs) !== JSON.stringify(originalConfig.networkConfigs)) return true; + if (macAddress !== originalConfig.macAddress) return true; // Advanced options - User & Security if (containerUser !== originalConfig.containerUser) return true; @@ -701,7 +727,9 @@ volumeMappings: volumeMappings.filter(v => v.hostPath && v.containerPath), envVars: envVars.filter(e => e.key), labels: labels.filter(l => l.key), - selectedNetworks: [...selectedNetworks].sort() + selectedNetworks: [...selectedNetworks].sort(), + networkConfigs, + macAddress }); } @@ -716,7 +744,9 @@ volumeMappings: originalConfig.volumeMappings.filter(v => v.hostPath && v.containerPath), envVars: originalConfig.envVars.filter(e => e.key), labels: originalConfig.labels.filter(l => l.key), - selectedNetworks: [...originalConfig.selectedNetworks].sort() + selectedNetworks: [...originalConfig.selectedNetworks].sort(), + networkConfigs: originalConfig.networkConfigs, + macAddress: originalConfig.macAddress }); } @@ -901,18 +931,31 @@ deviceRequests = [dr]; } + // Build per-network configs for the API + const netConfigs: Record = {}; + for (const [netName, cfg] of Object.entries(networkConfigs)) { + if (!selectedNetworks.includes(netName)) continue; + const entry: { ipv4Address?: string; ipv6Address?: string; aliases?: string[] } = {}; + if (cfg.ipv4Address) entry.ipv4Address = cfg.ipv4Address; + if (cfg.ipv6Address) entry.ipv6Address = cfg.ipv6Address; + if (cfg.aliases) entry.aliases = cfg.aliases.split(',').map(a => a.trim()).filter(Boolean); + if (Object.keys(entry).length > 0) netConfigs[netName] = entry; + } + const payload = { name: name.trim(), image: image.trim(), ports: Object.keys(ports).length > 0 ? ports : null, volumeBinds: volumeBinds.length > 0 ? volumeBinds : null, env: env.length > 0 ? env : undefined, - labels: Object.keys(labelsObj).length > 0 ? labelsObj : undefined, + labels: labelsObj, cmd, restartPolicy, restartMaxRetries: restartPolicy === 'on-failure' && restartMaxRetries !== '' ? Number(restartMaxRetries) : undefined, networkMode, networks: selectedNetworks.length > 0 ? selectedNetworks : undefined, + networkConfigs: Object.keys(netConfigs).length > 0 ? netConfigs : undefined, + macAddress: macAddress.trim() || undefined, startAfterUpdate, repullImage, user: containerUser.trim() || null, @@ -1091,6 +1134,8 @@ bind:labels {availableNetworks} bind:selectedNetworks + bind:networkConfigs + bind:macAddress bind:containerUser bind:privilegedMode bind:healthcheckEnabled diff --git a/src/routes/dashboard/EnvironmentTile.svelte b/src/routes/dashboard/EnvironmentTile.svelte index 0a80a15..c91fa4f 100644 --- a/src/routes/dashboard/EnvironmentTile.svelte +++ b/src/routes/dashboard/EnvironmentTile.svelte @@ -338,7 +338,7 @@ - + {#if showOffline} {:else} @@ -439,7 +439,7 @@ - + {#if showOffline} {:else} @@ -543,7 +543,7 @@ - + {#if showOffline} {:else} @@ -586,7 +586,7 @@ /> - + {#if showOffline} {:else} @@ -632,7 +632,7 @@ /> - + {#if showOffline} {:else} @@ -684,7 +684,7 @@ /> - + {#if showOffline} {:else} diff --git a/src/routes/settings/about/AboutTab.svelte b/src/routes/settings/about/AboutTab.svelte index 490680a..eac52ca 100644 --- a/src/routes/settings/about/AboutTab.svelte +++ b/src/routes/settings/about/AboutTab.svelte @@ -90,18 +90,27 @@ } function formatChangelogDate(dateStr: string): string { + if (!dateStr) return 'Unreleased'; try { const date = new Date(dateStr); + if (isNaN(date.getTime())) return 'Unreleased'; return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); } catch { - return dateStr; + return 'Unreleased'; } } + function sortedChanges(changes: ChangelogChange[]): ChangelogChange[] { + return [...changes].sort((a, b) => { + if (a.type === b.type) return 0; + return a.type === 'feature' ? -1 : 1; + }); + } + // Build info - injected at build time declare const __BUILD_BRANCH__: string | null; const BUILD_DATE = __BUILD_DATE__ ?? null; @@ -867,7 +876,7 @@ {#if isExpanded}
    - {#each release.changes as change} + {#each sortedChanges(release.changes) as change}
  • {#if change.type === 'feature'} diff --git a/src/routes/settings/environments/EnvironmentModal.svelte b/src/routes/settings/environments/EnvironmentModal.svelte index c16bdc3..ee15eba 100644 --- a/src/routes/settings/environments/EnvironmentModal.svelte +++ b/src/routes/settings/environments/EnvironmentModal.svelte @@ -751,7 +751,7 @@ labels: formLabels, connectionType: formConnectionType, hawserToken: formHawserToken || undefined, - publicIp: formConnectionType !== 'hawser-edge' ? (stripHostProtocol(formPublicIp.trim()) || undefined) : undefined + publicIp: stripHostProtocol(formPublicIp.trim()) || undefined }) }); @@ -869,7 +869,7 @@ labels: formLabels, connectionType: formConnectionType, hawserToken: formHawserToken || undefined, - publicIp: formConnectionType !== 'hawser-edge' ? (stripHostProtocol(formPublicIp.trim()) || null) : null + publicIp: stripHostProtocol(formPublicIp.trim()) || null }) }); @@ -2160,31 +2160,29 @@
{/if} - - {#if formConnectionType !== 'hawser-edge'} -
-
- - - - - - -

IP address or hostname where container ports are accessible from your browser. For local Docker, use the server's LAN IP.

-
-
-
- -

- Used for clickable port links on the containers page -

+ +
+
+ + + + + + +

IP address or hostname where container ports are accessible from your browser. For local Docker, use the server's LAN IP.

+
+
- {/if} + +

+ Used for clickable port links on the containers page +

+
diff --git a/src/routes/settings/general/GeneralTab.svelte b/src/routes/settings/general/GeneralTab.svelte index d407c60..3af8f27 100644 --- a/src/routes/settings/general/GeneralTab.svelte +++ b/src/routes/settings/general/GeneralTab.svelte @@ -74,6 +74,8 @@ services: let eventCleanupCron = $derived($appSettings.eventCleanupCron); let scheduleCleanupEnabled = $derived($appSettings.scheduleCleanupEnabled); let eventCleanupEnabled = $derived($appSettings.eventCleanupEnabled); + let scannerCleanupCron = $derived($appSettings.scannerCleanupCron); + let scannerCleanupEnabled = $derived($appSettings.scannerCleanupEnabled); let logBufferSizeKb = $derived($appSettings.logBufferSizeKb); let formatLogTimestamps = $derived($appSettings.formatLogTimestamps); let defaultTimezone = $derived($appSettings.defaultTimezone); @@ -146,6 +148,17 @@ services: toast.success(newState ? 'Event cleanup enabled' : 'Event cleanup disabled'); } + function handleScannerCleanupCronChange(cron: string) { + appSettings.setScannerCleanupCron(cron); + toast.success('Scanner cleanup cron updated'); + } + + function handleScannerCleanupEnabledChange() { + const newState = !scannerCleanupEnabled; + appSettings.setScannerCleanupEnabled(newState); + toast.success(newState ? 'Scanner cleanup enabled' : 'Scanner cleanup disabled'); + } + function handleGrypeImageBlur(e: Event) { const value = (e.target as HTMLInputElement).value.trim(); if (value && value !== defaultGrypeImage) { @@ -758,6 +771,26 @@ services: Runs every 30 minutes and on startup.

+
+
+ + +
+

Remove cached vulnerability databases to reclaim disk space

+
+
+ +
+
+
diff --git a/src/routes/settings/notifications/NotificationModal.svelte b/src/routes/settings/notifications/NotificationModal.svelte index e364d81..376681d 100644 --- a/src/routes/settings/notifications/NotificationModal.svelte +++ b/src/routes/settings/notifications/NotificationModal.svelte @@ -422,6 +422,8 @@ mmost://hostname/webhook-token tgram://bot_token/chat_id tgram://bot_token/chat_id:topic_id ntfy://my-topic +ntfy://host/topic?auth=base64token +ntfys://host/topic?auth=base64token pushover://user_key/api_token workflows://hostname/workflow/signature jsons://hostname/webhook/path" diff --git a/src/routes/stacks/GitStackModal.svelte b/src/routes/stacks/GitStackModal.svelte index 559535f..7ec3dac 100644 --- a/src/routes/stacks/GitStackModal.svelte +++ b/src/routes/stacks/GitStackModal.svelte @@ -7,7 +7,7 @@ import { Badge } from '$lib/components/ui/badge'; import { Input } from '$lib/components/ui/input'; import { TogglePill } from '$lib/components/ui/toggle-pill'; - import { Loader2, GitBranch, RefreshCw, Webhook, Rocket, RefreshCcw, Copy, Check, XCircle, FolderGit2, Github, Key, KeyRound, Lock, FileText, HelpCircle, GripVertical, X, Download, Hammer, ArrowDownToLine, Zap } from 'lucide-svelte'; + import { Loader2, GitBranch, RefreshCw, Webhook, Rocket, RefreshCcw, Copy, Check, XCircle, FolderGit2, Github, Key, KeyRound, Lock, FileText, HelpCircle, GripVertical, X, Download, Hammer, ArrowDownToLine, Zap, FolderOpen, Ban, TriangleAlert } from 'lucide-svelte'; import * as Tooltip from '$lib/components/ui/tooltip'; import { copyToClipboard } from '$lib/utils/clipboard'; import CronEditor from '$lib/components/cron-editor.svelte'; @@ -58,7 +58,9 @@ autoUpdateCron: string; webhookEnabled: boolean; webhookSecret: string | null; + contextDir: string | null; buildOnDeploy: boolean; + noBuildCache: boolean; repullImages: boolean; forceRedeploy: boolean; } @@ -91,12 +93,15 @@ let formAutoUpdateCron = $state('0 3 * * *'); let formWebhookEnabled = $state(false); let formWebhookSecret = $state(''); + let formContextDir = $state(null); let formBuildOnDeploy = $state(false); + let formNoBuildCache = $state(false); let formRepullImages = $state(false); let formForceRedeploy = $state(false); let formDeployNow = $state(false); let formError = $state(''); let formSaving = $state(false); + let showExistsWarning = $state(false); let errors = $state<{ stackName?: string; repository?: string; repoName?: string; repoUrl?: string }>({}); // Stack name validation: must start with alphanumeric, can contain alphanumeric, hyphens, underscores @@ -364,7 +369,9 @@ formAutoUpdateCron = gitStack.autoUpdateCron || '0 3 * * *'; formWebhookEnabled = gitStack.webhookEnabled; formWebhookSecret = gitStack.webhookSecret || ''; + formContextDir = gitStack.contextDir ?? null; formBuildOnDeploy = gitStack.buildOnDeploy ?? false; + formNoBuildCache = gitStack.noBuildCache ?? false; formRepullImages = gitStack.repullImages ?? false; formForceRedeploy = gitStack.forceRedeploy ?? false; formDeployNow = false; @@ -391,7 +398,9 @@ formAutoUpdateCron = '0 3 * * *'; formWebhookEnabled = false; formWebhookSecret = ''; + formContextDir = null; formBuildOnDeploy = false; + formNoBuildCache = false; formRepullImages = false; formForceRedeploy = false; formDeployNow = false; @@ -428,6 +437,25 @@ if (hasErrors) return; + // Check if stack already exists (only for new stacks) + if (!gitStack) { + try { + const stacksResponse = await fetch(`/api/stacks?env=${environmentId}`); + if (stacksResponse.ok) { + const stacks = await stacksResponse.json(); + const existingStack = stacks.find((s: { name: string }) => + s.name.toLowerCase() === formStackName.trim().toLowerCase() + ); + if (existingStack) { + showExistsWarning = true; + return; + } + } + } catch (e) { + console.warn('Failed to check for existing stacks:', e); + } + } + formSaving = true; formError = ''; @@ -450,7 +478,9 @@ autoUpdateCron: formAutoUpdateCron, webhookEnabled: formWebhookEnabled, webhookSecret: formWebhookEnabled ? formWebhookSecret : null, + contextDir: formContextDir || null, buildOnDeploy: formBuildOnDeploy, + noBuildCache: formNoBuildCache, repullImages: formRepullImages, forceRedeploy: formForceRedeploy, deployNow: deployAfterSave, @@ -793,6 +823,32 @@

Additional env file to pass to Docker Compose

+ +
+
+ + + + + + +
+

Working directory for Docker Compose, relative to the repository root. All files in this directory will be available for volume mounts and build contexts.

+

Use . for the repository root when your compose file references files in sibling directories.

+

Defaults to the compose file's parent directory.

+
+
+
+
+ { const v = (e.target as HTMLInputElement).value; formContextDir = v.trim() || null; }} + placeholder="Defaults to compose file's directory" + /> +

Relative to repository root, e.g. . for root

+
+
@@ -922,6 +978,18 @@

Run --build to build images from Dockerfiles before starting containers.

+ {#if formBuildOnDeploy} +
+
+ + +
+ +
+

+ Pass --no-cache to force a clean build without using cached layers. +

+ {/if}
@@ -1057,3 +1125,23 @@ + + + + + + + + Stack already exists + + + A stack named "{formStackName}" already exists. Please choose a different name. + + +
+ +
+
+
diff --git a/src/routes/stacks/ImportStackModal.svelte b/src/routes/stacks/ImportStackModal.svelte index 85a779d..606ce99 100644 --- a/src/routes/stacks/ImportStackModal.svelte +++ b/src/routes/stacks/ImportStackModal.svelte @@ -48,6 +48,7 @@ let previewFile = $state(null); let previewContent = $state(null); let previewServiceCount = $state(0); + let previewComposeName = $state(null); let loadingPreview = $state(false); // Use current environment from store @@ -72,6 +73,7 @@ previewFile = null; previewContent = null; previewServiceCount = 0; + previewComposeName = null; } }); @@ -81,15 +83,19 @@ loadingPreview = true; previewContent = null; previewServiceCount = 0; + previewComposeName = null; try { const res = await fetch(`/api/system/files/content?path=${encodeURIComponent(entry.path)}`); if (res.ok) { const data = await res.json(); previewContent = data.content || ''; - // Count services in the compose file + // Parse compose metadata (name + service count) try { const doc = yaml.load(previewContent) as Record | null; + if (typeof doc?.name === 'string' && doc.name.trim()) { + previewComposeName = doc.name.trim(); + } if (doc?.services && typeof doc.services === 'object') { previewServiceCount = Object.keys(doc.services).length; } @@ -191,7 +197,8 @@ try { const parentDir = entry.path.replace(/\/[^/]+$/, ''); - const rawName = parentDir.split('/').pop() || 'adopted-stack'; + // Use compose `name:` property if available, otherwise fall back to directory name + const rawName = previewComposeName || parentDir.split('/').pop() || 'adopted-stack'; const stackName = rawName.toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'adopted-stack'; const envFilePath = `${parentDir}/.env`; @@ -462,7 +469,7 @@
Stack: - {previewFile.path.replace(/\/[^/]+$/, '').split('/').pop() || 'unknown'} + {previewComposeName || previewFile.path.replace(/\/[^/]+$/, '').split('/').pop() || 'unknown'} {#if previewServiceCount > 0} {previewServiceCount} service{previewServiceCount !== 1 ? 's' : ''} diff --git a/src/routes/stacks/StackModal.svelte b/src/routes/stacks/StackModal.svelte index f9fb617..bb1ac8f 100644 --- a/src/routes/stacks/StackModal.svelte +++ b/src/routes/stacks/StackModal.svelte @@ -887,7 +887,8 @@ error = null; // Prepare env vars for creating - syncs variables and rawContent - const prepared = envVarsPanelRef?.prepareForSave() || { rawContent: '', variables: [] }; + // If env panel is unmounted (e.g. graph tab active), use bound state directly + const prepared = envVarsPanelRef?.prepareForSave() || { rawContent: rawEnvContent, variables: envVars }; let response: Response | undefined; try { @@ -1011,7 +1012,8 @@ error = null; // Prepare env vars for saving - syncs variables and rawContent - const prepared = envVarsPanelRef?.prepareForSave() || { rawContent: '', variables: [] }; + // If env panel is unmounted (e.g. graph tab active), use bound state directly + const prepared = envVarsPanelRef?.prepareForSave() || { rawContent: rawEnvContent, variables: envVars }; // Resolve env path (use working or suggested) const envPathToSave = workingEnvPath.trim() || suggestedEnvPath || '';