From cd816d2c82b29f5850e6cb90faf0b39ed8947d57 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Thu, 20 Mar 2025 14:47:48 +0100 Subject: [PATCH 0001/1717] feat: add signoz template --- public/svgs/signoz.png | Bin 0 -> 173112 bytes templates/compose/signoz.yaml | 1743 +++++++++++++++++++++++++++++++++ 2 files changed, 1743 insertions(+) create mode 100644 public/svgs/signoz.png create mode 100644 templates/compose/signoz.yaml diff --git a/public/svgs/signoz.png b/public/svgs/signoz.png new file mode 100644 index 0000000000000000000000000000000000000000..a681188c7e6f71f779058786f9f153dd1d015d79 GIT binary patch literal 173112 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 uR>*j_t+{YD)grye+ik0uoL;nZT+%RAO literal 0 HcmV?d00001 diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml new file mode 100644 index 000000000..d491ce662 --- /dev/null +++ b/templates/compose/signoz.yaml @@ -0,0 +1,1743 @@ +# documentation: https://github.com/SigNoz/signoz +# slogan: An observability platform native to OpenTelemetry with logs, traces and metrics. +# tags: telemetry, server, applications, interface, logs, monitoring, traces, metrics +# logo: svgs/signoz.png +# port: 8080 + +services: + init-clickhouse: + image: clickhouse/clickhouse-server:24.1.2-alpine + container_name: signoz-init-clickhouse + command: + - bash + - -c + - | + version="v0.0.1" + node_os=$$(uname -s | tr '[:upper:]' '[:lower:]') + node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) + echo "Fetching histogram-binary for $${node_os}/$${node_arch}" + cd /tmp + wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz" + tar -xvzf histogram-quantile.tar.gz + mkdir -p /var/lib/clickhouse/user_scripts/histogramQuantile + mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile + restart: on-failure + networks: + - signoz-net + logging: + options: + max-size: 50m + max-file: "3" + + zookeeper-1: + image: bitnami/zookeeper:3.7.1 + container_name: signoz-zookeeper-1 + user: root + healthcheck: + test: + - CMD-SHELL + - curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null + interval: 30s + timeout: 5s + retries: 3 + networks: + - signoz-net + restart: unless-stopped + logging: + options: + max-size: 50m + max-file: "3" + + # ports: + # - "2181:2181" + # - "2888:2888" + # - "3888:3888" + volumes: + - zookeeper-1:/bitnami/zookeeper + environment: + - ZOO_SERVER_ID=1 + - ALLOW_ANONYMOUS_LOGIN=yes + - ZOO_AUTOPURGE_INTERVAL=1 + - ZOO_ENABLE_PROMETHEUS_METRICS=yes + - ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141 + + clickhouse: + # addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab + image: clickhouse/clickhouse-server:24.1.2-alpine + container_name: signoz-clickhouse + tty: true + depends_on: + init-clickhouse: + condition: service_completed_successfully + zookeeper-1: + condition: service_healthy + healthcheck: + test: + - CMD + - wget + - --spider + - -q + - 0.0.0.0:8123/ping + interval: 30s + timeout: 5s + retries: 3 + ulimits: + nproc: 65535 + nofile: + soft: 262144 + hard: 262144 + networks: + - signoz-net + restart: unless-stopped + logging: + options: + max-size: 50m + max-file: "3" + # ports: + # - "9000:9000" + # - "8123:8123" + # - "9181:9181" + volumes: + - type: bind + source: ./clickhouse/config.xml + target: /etc/clickhouse-server/config.xml + content: | + + + + + + information + + json + + /var/log/clickhouse-server/clickhouse-server.log + /var/log/clickhouse-server/clickhouse-server.err.log + + 1000M + 10 + + + + + + + + + + + + + + + + + + 8123 + + + 9000 + + + 9004 + + + 9005 + + + + + + + + + + + + 9009 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4096 + + + 3 + + + + + false + + + /path/to/ssl_cert_file + /path/to/ssl_key_file + + + false + + + /path/to/ssl_ca_cert_file + + + none + + + 0 + + + -1 + -1 + + + false + + + + + + + + + + + none + true + true + sslv2,sslv3 + true + + + + true + true + sslv2,sslv3 + true + + + + RejectCertificateHandler + + + + + + + + + 100 + + + 0 + + + + 10000 + + + + + + 0.9 + + + 4194304 + + + 0 + + + + + + 8589934592 + + + 5368709120 + + + + 1000 + + + 134217728 + + + 10000 + + + /var/lib/clickhouse/ + + + /var/lib/clickhouse/tmp/ + + + + ` + + + + + + /var/lib/clickhouse/user_files/ + + + + + + + + + + + + + users.xml + + + + /var/lib/clickhouse/access/ + + + + + + + default + + + + + + + + + + + + default + + + + + + + + + true + + + false + + ' | sed -e 's|.*>\(.*\)<.*|\1|') + wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v$PKG_VER/clickhouse-jdbc-bridge_$PKG_VER-1_all.deb + apt install --no-install-recommends -f ./clickhouse-jdbc-bridge_$PKG_VER-1_all.deb + clickhouse-jdbc-bridge & + + * [CentOS/RHEL] + export MVN_URL=https://repo1.maven.org/maven2/ru/yandex/clickhouse/clickhouse-jdbc-bridge + export PKG_VER=$(curl -sL $MVN_URL/maven-metadata.xml | grep '' | sed -e 's|.*>\(.*\)<.*|\1|') + wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v$PKG_VER/clickhouse-jdbc-bridge-$PKG_VER-1.noarch.rpm + yum localinstall -y clickhouse-jdbc-bridge-$PKG_VER-1.noarch.rpm + clickhouse-jdbc-bridge & + + Please refer to https://github.com/ClickHouse/clickhouse-jdbc-bridge#usage for more information. + ]]> + + + + + + + + + + + + + + + 01 + example01-01-1 + + + + + + 3600 + + + + 3600 + + + 60 + + + + + + + + + + /metrics + 9363 + + true + true + true + true + + + + + + system + query_log
+ + toYYYYMM(event_date) + + + + + + 7500 +
+ + + + system + trace_log
+ + toYYYYMM(event_date) + 7500 +
+ + + + system + query_thread_log
+ toYYYYMM(event_date) + 7500 +
+ + + + system + query_views_log
+ toYYYYMM(event_date) + 7500 +
+ + + + system + part_log
+ toYYYYMM(event_date) + 7500 +
+ + + + + + system + metric_log
+ 7500 + 1000 +
+ + + + system + asynchronous_metric_log
+ + 7000 +
+ + + + + + engine MergeTree + partition by toYYYYMM(finish_date) + order by (finish_date, finish_time_us, trace_id) + + system + opentelemetry_span_log
+ 7500 +
+ + + + + system + crash_log
+ + + 1000 +
+ + + + + + + system + processors_profile_log
+ + toYYYYMM(event_date) + 7500 +
+ + + + + + + + + *_dictionary.xml + + + *function.xml + /var/lib/clickhouse/user_scripts/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /clickhouse/task_queue/ddl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + click_cost + any + + 0 + 3600 + + + 86400 + 60 + + + + max + + 0 + 60 + + + 3600 + 300 + + + 86400 + 3600 + + + + + + /var/lib/clickhouse/format_schemas/ + + + + + hide encrypt/decrypt arguments + ((?:aes_)?(?:encrypt|decrypt)(?:_mysql)?)\s*\(\s*(?:'(?:\\'|.)+'|.*?)\s*\) + + \1(???) + + + + + + + + + + false + + false + + + https://6f33034cfe684dd7a3ab9875e57b1c8d@o388870.ingest.sentry.io/5226277 + + + + + + + + + + + 268435456 + true + +
+ - type: bind + source: ./clickhouse/users.xml + target: /etc/clickhouse-server/users.xml + content: | + + + + + + + + + + 10000000000 + + + random + + + + + 1 + + + + + + + + + + + + + ::/0 + + + + default + + + default + + + + + + + + + + + + + + 3600 + + + 0 + 0 + 0 + 0 + 0 + + + + + - type: bind + source: ./clickhouse/custom-function.xml + target: /etc/clickhouse-server/custom-function.xml + content: | + + + executable + histogramQuantile + Float64 + + Array(Float64) + buckets + + + Array(Float64) + counts + + + Float64 + quantile + + CSV + ./histogramQuantile + + + - type: bind + source: ./clickhouse/cluster.xml + target: /etc/clickhouse-server/config.d/cluster.xml + content: | + + + + + + zookeeper-1 + 2181 + + + + + + + + + + + + + + + + clickhouse + 9000 + + + + + + + + + - type: volume + source: clickhouse + target: /var/lib/clickhouse/ + + signoz: + image: signoz/signoz:${DOCKER_TAG:-v0.76.2} + container_name: signoz + depends_on: + clickhouse: + condition: service_healthy + schema-migrator-sync: + condition: service_completed_successfully + networks: + - signoz-net + restart: unless-stopped + logging: + options: + max-size: 50m + max-file: "3" + command: + - --config=/root/config/prometheus.yml + - --use-logs-new-schema=true + - --use-trace-new-schema=true + volumes: + - type: bind + source: ./prometheus.yml + target: /root/config/prometheus.yml + content: | + # my global config + global: + scrape_interval: 5s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. + # scrape_timeout is set to the global default (10s). + + # Alertmanager configuration + alerting: + alertmanagers: + - static_configs: + - targets: + - alertmanager:9093 + + # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. + rule_files: [] + # - "first_rules.yml" + # - "second_rules.yml" + # - 'alerts.yml' + + # A scrape configuration containing exactly one endpoint to scrape: + # Here it's Prometheus itself. + scrape_configs: [] + + remote_read: + - url: tcp://clickhouse:9000/signoz_metrics + - type: volume + source: sqlite + target: /var/lib/signoz/ + environment: + - SERVICE_FQDN_SIGNOZ_8080 + - SIGNOZ_ALERTMANAGER_PROVIDER=signoz + - SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000 + - SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db + - DASHBOARDS_PATH=/root/config/dashboards + - STORAGE=clickhouse + - GODEBUG=netdns=go + - TELEMETRY_ENABLED=true + - DEPLOYMENT_TYPE=docker-standalone-amd + healthcheck: + test: + - CMD + - wget + - --spider + - -q + - localhost:8080/api/v1/health + interval: 30s + timeout: 5s + retries: 3 + + otel-collector: + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.34} + container_name: signoz-otel-collector + depends_on: + clickhouse: + condition: service_healthy + schema-migrator-sync: + condition: service_completed_successfully + signoz: + condition: service_healthy + networks: + - signoz-net + restart: unless-stopped + logging: + options: + max-size: 50m + max-file: "3" + ports: + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + command: + - --config=/etc/otel-collector-config.yaml + - --manager-config=/etc/manager-config.yaml + - --copy-path=/var/tmp/collector-config.yaml + - --feature-gates=-pkg.translator.prometheus.NormalizeName + volumes: + - type: bind + source: ./otel-collector-config.yaml + target: /etc/otel-collector-config.yaml + content: | + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + prometheus: + config: + global: + scrape_interval: 60s + scrape_configs: + - job_name: otel-collector + static_configs: + - targets: + - localhost:8888 + labels: + job_name: otel-collector + processors: + batch: + send_batch_size: 10000 + send_batch_max_size: 11000 + timeout: 10s + resourcedetection: + # Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels. + detectors: [env, system] + timeout: 2s + signozspanmetrics/delta: + metrics_exporter: clickhousemetricswrite + metrics_flush_interval: 60s + latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ] + dimensions_cache_size: 100000 + aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA + enable_exp_histogram: true + dimensions: + - name: service.namespace + default: default + - name: deployment.environment + default: default + # This is added to ensure the uniqueness of the timeseries + # Otherwise, identical timeseries produced by multiple replicas of + # collectors result in incorrect APM metrics + - name: signoz.collector.id + - name: service.version + - name: browser.platform + - name: browser.mobile + - name: k8s.cluster.name + - name: k8s.node.name + - name: k8s.namespace.name + - name: host.name + - name: host.type + - name: container.name + extensions: + health_check: + endpoint: 0.0.0.0:13133 + pprof: + endpoint: 0.0.0.0:1777 + exporters: + clickhousetraces: + datasource: tcp://clickhouse:9000/signoz_traces + low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING} + use_new_schema: true + clickhousemetricswrite: + endpoint: tcp://clickhouse:9000/signoz_metrics + resource_to_telemetry_conversion: + enabled: true + clickhousemetricswrite/prometheus: + endpoint: tcp://clickhouse:9000/signoz_metrics + signozclickhousemetrics: + dsn: tcp://clickhouse:9000/signoz_metrics + clickhouselogsexporter: + dsn: tcp://clickhouse:9000/signoz_logs + timeout: 10s + use_new_schema: true + # debug: {} + service: + telemetry: + logs: + encoding: json + metrics: + address: 0.0.0.0:8888 + extensions: + - health_check + - pprof + pipelines: + traces: + receivers: [otlp] + processors: [signozspanmetrics/delta, batch] + exporters: [clickhousetraces] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [clickhousemetricswrite, signozclickhousemetrics] + metrics/prometheus: + receivers: [prometheus] + processors: [batch] + exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics] + logs: + receivers: [otlp] + processors: [batch] + exporters: [clickhouselogsexporter] + - type: bind + source: ./otel-collector-opamp-config.yaml + target: /etc/manager-config.yaml + content: | + server_endpoint: ws://signoz:4320/v1/opamp + environment: + - OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux + - LOW_CARDINAL_EXCEPTION_GROUPING=false + + schema-migrator-sync: + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34} + container_name: schema-migrator-sync + command: + - sync + - --dsn=tcp://clickhouse:9000 + - --up= + depends_on: + clickhouse: + condition: service_healthy + restart: on-failure + networks: + - signoz-net + logging: + options: + max-size: 50m + max-file: "3" + + schema-migrator-async: + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34} + container_name: schema-migrator-async + depends_on: + clickhouse: + condition: service_healthy + schema-migrator-sync: + condition: service_completed_successfully + networks: + - signoz-net + logging: + options: + max-size: 50m + max-file: "3" + command: + - async + - --dsn=tcp://clickhouse:9000 + - --up= + restart: on-failure + +networks: + signoz-net: + name: signoz-net + +volumes: + clickhouse: + name: signoz-clickhouse + sqlite: + name: signoz-sqlite + zookeeper-1: + name: signoz-zookeeper-1 \ No newline at end of file From 508d0a06e4d62ba9acab0ae46f6424a915b8297d Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Thu, 20 Mar 2025 15:27:44 +0100 Subject: [PATCH 0002/1717] feat(signoz): replace png icon by svg icon --- public/svgs/signoz.png | Bin 173112 -> 0 bytes public/svgs/signoz.svg | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) delete mode 100644 public/svgs/signoz.png create mode 100644 public/svgs/signoz.svg diff --git a/public/svgs/signoz.png b/public/svgs/signoz.png deleted file mode 100644 index a681188c7e6f71f779058786f9f153dd1d015d79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173112 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 uR>*j_t+{YD)grye+ik0uoL;nZT+%RAO diff --git a/public/svgs/signoz.svg b/public/svgs/signoz.svg new file mode 100644 index 000000000..ac47e1c93 --- /dev/null +++ b/public/svgs/signoz.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fe489faa39c04f1452d3c2d4d4ab053d32ed5487 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Thu, 20 Mar 2025 15:28:24 +0100 Subject: [PATCH 0003/1717] feat(signoz): remove explicit 'networks' setting --- templates/compose/signoz.yaml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index d491ce662..fdcc7cfcb 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -1,7 +1,7 @@ # documentation: https://github.com/SigNoz/signoz # slogan: An observability platform native to OpenTelemetry with logs, traces and metrics. # tags: telemetry, server, applications, interface, logs, monitoring, traces, metrics -# logo: svgs/signoz.png +# logo: svgs/signoz.svg # port: 8080 services: @@ -22,8 +22,6 @@ services: mkdir -p /var/lib/clickhouse/user_scripts/histogramQuantile mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile restart: on-failure - networks: - - signoz-net logging: options: max-size: 50m @@ -40,8 +38,6 @@ services: interval: 30s timeout: 5s retries: 3 - networks: - - signoz-net restart: unless-stopped logging: options: @@ -86,8 +82,6 @@ services: nofile: soft: 262144 hard: 262144 - networks: - - signoz-net restart: unless-stopped logging: options: @@ -1487,8 +1481,6 @@ services: condition: service_healthy schema-migrator-sync: condition: service_completed_successfully - networks: - - signoz-net restart: unless-stopped logging: options: @@ -1562,8 +1554,6 @@ services: condition: service_completed_successfully signoz: condition: service_healthy - networks: - - signoz-net restart: unless-stopped logging: options: @@ -1703,8 +1693,6 @@ services: clickhouse: condition: service_healthy restart: on-failure - networks: - - signoz-net logging: options: max-size: 50m @@ -1718,8 +1706,6 @@ services: condition: service_healthy schema-migrator-sync: condition: service_completed_successfully - networks: - - signoz-net logging: options: max-size: 50m @@ -1730,10 +1716,6 @@ services: - --up= restart: on-failure -networks: - signoz-net: - name: signoz-net - volumes: clickhouse: name: signoz-clickhouse From fa61a80a528ceab9e74d457f3dcb3153f8c0315e Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Fri, 21 Mar 2025 10:29:36 +0100 Subject: [PATCH 0004/1717] chore(signoz): remove unused ports --- templates/compose/signoz.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index fdcc7cfcb..1d9a134ef 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -43,11 +43,6 @@ services: options: max-size: 50m max-file: "3" - - # ports: - # - "2181:2181" - # - "2888:2888" - # - "3888:3888" volumes: - zookeeper-1:/bitnami/zookeeper environment: @@ -87,10 +82,6 @@ services: options: max-size: 50m max-file: "3" - # ports: - # - "9000:9000" - # - "8123:8123" - # - "9181:9181" volumes: - type: bind source: ./clickhouse/config.xml From 5d60eabff937f6492433c1274613d3867f05e98f Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Fri, 21 Mar 2025 10:31:02 +0100 Subject: [PATCH 0005/1717] feat(signoz): add predefined environment variables to configure Telemetry, SMTP and email sending for Alert Manager --- templates/compose/signoz.yaml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 1d9a134ef..f4cb79ce9 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -26,7 +26,7 @@ services: options: max-size: 50m max-file: "3" - + zookeeper-1: image: bitnami/zookeeper:3.7.1 container_name: signoz-zookeeper-1 @@ -51,7 +51,7 @@ services: - ZOO_AUTOPURGE_INTERVAL=1 - ZOO_ENABLE_PROMETHEUS_METRICS=yes - ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141 - + clickhouse: # addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab image: clickhouse/clickhouse-server:24.1.2-alpine @@ -1229,7 +1229,7 @@ services: true - - type: bind + - type: bind source: ./clickhouse/users.xml target: /etc/clickhouse-server/users.xml content: | @@ -1522,8 +1522,18 @@ services: - DASHBOARDS_PATH=/root/config/dashboards - STORAGE=clickhouse - GODEBUG=netdns=go - - TELEMETRY_ENABLED=true - DEPLOYMENT_TYPE=docker-standalone-amd + - TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true} + - SMTP_ENABLED=${SMTP_ENABLED:-false} + - SMTP_FROM=${SMTP_FROM} + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_USERNAME=${SMTP_USERNAME} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__PASSWORD=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__PASSWORD} + - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__USERNAME=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__USERNAME} + - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__FROM=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__FROM} + - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__SMARTHOST=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__SMARTHOST} healthcheck: test: - CMD @@ -1713,4 +1723,4 @@ volumes: sqlite: name: signoz-sqlite zookeeper-1: - name: signoz-zookeeper-1 \ No newline at end of file + name: signoz-zookeeper-1 From 043e32bb8bb21570042ee7676751076b35edce1f Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Fri, 21 Mar 2025 10:32:05 +0100 Subject: [PATCH 0006/1717] feat(signoz): generate URLs for `otel-collector` service --- templates/compose/signoz.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index f4cb79ce9..9c3dc9260 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -1680,6 +1680,8 @@ services: content: | server_endpoint: ws://signoz:4320/v1/opamp environment: + - SERVICE_FQDN_OTELCOLLECTORGRPC_4317 + - SERVICE_FQDN_OTELCOLLECTORHTTP_4318 - OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux - LOW_CARDINAL_EXCEPTION_GROUPING=false From fa967abbc12f4d43b08eebadb5a7b41b2dafb092 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Fri, 21 Mar 2025 11:27:24 +0100 Subject: [PATCH 0007/1717] feat(signoz): update documentation link --- templates/compose/signoz.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 9c3dc9260..77657bdd6 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -1,4 +1,4 @@ -# documentation: https://github.com/SigNoz/signoz +# documentation: https://signoz.io/docs/introduction/ # slogan: An observability platform native to OpenTelemetry with logs, traces and metrics. # tags: telemetry, server, applications, interface, logs, monitoring, traces, metrics # logo: svgs/signoz.svg From d39e6ecc07625b757c50f7460f9d7f18f19c39bd Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Fri, 21 Mar 2025 18:08:40 +0100 Subject: [PATCH 0008/1717] feat(signoz): add healthcheck to otel-collector service --- templates/compose/signoz.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 77657bdd6..6d18fe242 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -1684,6 +1684,11 @@ services: - SERVICE_FQDN_OTELCOLLECTORHTTP_4318 - OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux - LOW_CARDINAL_EXCEPTION_GROUPING=false + healthcheck: + test: bash -c "exec 6<> /dev/tcp/localhost/13133" + interval: 30s + timeout: 5s + retries: 3 schema-migrator-sync: image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34} From 7e271dfcdb5984848283605abf457c196953e944 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Mon, 24 Mar 2025 10:15:16 +0100 Subject: [PATCH 0009/1717] fix(signoz): remove example secrets to avoid triggering GitGuardian --- templates/compose/signoz.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 6d18fe242..9f74e8a16 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -1274,11 +1274,11 @@ services: Password could be empty. If you want to specify SHA256, place it in 'password_sha256_hex' element. - Example: 65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5 + Example: **PASSWORD HASHED WITH SHA256** Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019). If you want to specify double SHA1, place it in 'password_double_sha1_hex' element. - Example: e395796d6546b1b65db9d665cd43f0e858dd4303 + Example: **PASSWORD HASHED WITH SHA1** If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication, place its name in 'server' element inside 'ldap' element. From 55765908f6976821973dfe45b3696c571fe97a8e Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Mon, 7 Apr 2025 14:39:05 +0200 Subject: [PATCH 0010/1717] chore(signoz): bump version to 0.77.0 --- templates/compose/signoz.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 9f74e8a16..1d9719396 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -1465,7 +1465,7 @@ services: target: /var/lib/clickhouse/ signoz: - image: signoz/signoz:${DOCKER_TAG:-v0.76.2} + image: signoz/signoz:${VERSION:-v0.77.0} container_name: signoz depends_on: clickhouse: @@ -1546,7 +1546,7 @@ services: retries: 3 otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.34} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.37} container_name: signoz-otel-collector depends_on: clickhouse: @@ -1691,7 +1691,7 @@ services: retries: 3 schema-migrator-sync: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.37} container_name: schema-migrator-sync command: - sync @@ -1707,7 +1707,7 @@ services: max-file: "3" schema-migrator-async: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.34} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.37} container_name: schema-migrator-async depends_on: clickhouse: From 4db6a523b4a5e2c978d90eb0183bc6adcab35ad6 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Tue, 15 Apr 2025 10:03:42 +0200 Subject: [PATCH 0011/1717] chore(signoz): bump version to 0.78.1 --- templates/compose/signoz.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 1d9719396..288f67cb8 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -1465,7 +1465,7 @@ services: target: /var/lib/clickhouse/ signoz: - image: signoz/signoz:${VERSION:-v0.77.0} + image: signoz/signoz:${VERSION:-v0.78.1} container_name: signoz depends_on: clickhouse: @@ -1546,7 +1546,7 @@ services: retries: 3 otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.37} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.38} container_name: signoz-otel-collector depends_on: clickhouse: @@ -1691,7 +1691,7 @@ services: retries: 3 schema-migrator-sync: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.37} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38} container_name: schema-migrator-sync command: - sync @@ -1707,7 +1707,7 @@ services: max-file: "3" schema-migrator-async: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.37} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38} container_name: schema-migrator-async depends_on: clickhouse: From 2e0d4328867e312e70e3c204fe112b640b60838e Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Wed, 23 Apr 2025 15:56:34 +0200 Subject: [PATCH 0012/1717] add backup config info to --- app/Http/Controllers/Api/DatabasesController.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 504665f6a..452e24837 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -11,6 +11,7 @@ use App\Http\Controllers\Controller; use App\Jobs\DeleteResourceJob; use App\Models\Project; +use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use Illuminate\Http\Request; use OpenApi\Attributes as OA; @@ -78,7 +79,17 @@ public function databases(Request $request) foreach ($projects as $project) { $databases = $databases->merge($project->databases()); } - $databases = $databases->map(function ($database) { + + $backupConfig = ScheduledDatabaseBackup::with('latest_log')->get(); + $databases = $databases->map(function ($database) use ($backupConfig) { + $databaseBackupConfig = $backupConfig->where('database_id', $database->id)->first(); + + if ($databaseBackupConfig) { + $database->backup_configs = $databaseBackupConfig; + } else { + $database->backup_configs = null; + } + return $this->removeSensitiveData($database); }); From da487f609acfd8966ff8393e3c77dba64f358858 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Wed, 23 Apr 2025 20:59:20 +0200 Subject: [PATCH 0013/1717] implmenet `Get /database/:uuid/backups` api --- .../Controllers/Api/DatabasesController.php | 63 +++++++++++++++++++ routes/api.php | 5 ++ 2 files changed, 68 insertions(+) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 452e24837..de8daa43e 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -96,6 +96,69 @@ public function databases(Request $request) return response()->json($databases); } + #[OA\Get( + summary: 'Get', + description: 'Get database by UUID.', + path: '/databases/{uuid}/backups', + operationId: 'get-database-backups-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get all backups for a database', + content: new OA\JsonContent( + type: 'string', + example: 'Content is very complex. Will be implemented later.', + ), + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function database_backup_details_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->first(); + + return response()->json($this->removeSensitiveData($backupConfig)); + } + #[OA\Get( summary: 'Get', description: 'Get database by UUID.', diff --git a/routes/api.php b/routes/api.php index 8ac8aef14..409dd393f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,6 +23,10 @@ }); Route::post('/feedback', [OtherController::class, 'feedback']); +Route::get('/test', function () { + return response()->json(['message' => 'test']); +}); + Route::group([ 'middleware' => ['auth:sanctum', 'api.ability:write'], 'prefix' => 'v1', @@ -110,6 +114,7 @@ Route::post('/databases/keydb', [DatabasesController::class, 'create_database_keydb'])->middleware(['api.ability:write']); Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']); + Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_uuid'])->middleware(['api.ability:read']); Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); From 5dff22d3455146c7a46901da823d6c8a8c3c8d06 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Thu, 24 Apr 2025 16:48:08 +0200 Subject: [PATCH 0014/1717] implement backup config via api --- .../Controllers/Api/DatabasesController.php | 61 ++++++++++++++++++- routes/api.php | 3 - 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index de8daa43e..ab0191581 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -288,6 +288,19 @@ public function database_by_uuid(Request $request) 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], + // WIP + 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], + 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], + 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], + 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], + 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], + 'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'], + 'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'], + 'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'], + 'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], + 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], + 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], ], ), ) @@ -313,12 +326,14 @@ public function database_by_uuid(Request $request) )] public function update_by_uuid(Request $request) { + $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); } + // this check if the request is a valid json $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; @@ -336,6 +351,18 @@ public function update_by_uuid(Request $request) 'limits_cpus' => 'string', 'limits_cpuset' => 'string|nullable', 'limits_cpu_shares' => 'numeric', + 'save_s3' => 'boolean', + 'enabled' => 'boolean', + 'dump_all' => 'boolean', + 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', + 'databases_to_backup' => 'string', + 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', + 'database_backup_retention_amount_locally' => 'integer|min:0', + 'database_backup_retention_days_locally' => 'integer|min:0', + 'database_backup_retention_max_storage_locally' => 'integer|min:0', + 'database_backup_retention_amount_s3' => 'integer|min:0', + 'database_backup_retention_days_s3' => 'integer|min:0', + 'database_backup_retention_max_storage_s3' => 'integer|min:0', ]); if ($validator->fails()) { @@ -347,6 +374,7 @@ public function update_by_uuid(Request $request) $uuid = $request->uuid; removeUnnecessaryFieldsFromRequest($request); $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); + $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id)->first(); if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } @@ -545,7 +573,7 @@ public function update_by_uuid(Request $request) } break; } - $extraFields = array_diff(array_keys($request->all()), $allowedFields); + $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); if (! empty($extraFields)) { @@ -567,7 +595,36 @@ public function update_by_uuid(Request $request) $whatToDoWithDatabaseProxy = 'start'; } - $database->update($request->all()); + $backupPayload = $request->only($allowedBackupConfigsFields); + $databasePayload = $request->only($allowedFields); + + if ($databasePayload) { + $database->update($databasePayload); + } + + if ($backupPayload && ! $backupConfig) { + if ($database->type() === 'standalone-postgresql') { + $backupPayload['databases_to_backup'] = $database->postgres_db; + } elseif ($database->type() === 'standalone-mysql') { + $backupPayload['databases_to_backup'] = $database->mysql_database; + } elseif ($database->type() === 'standalone-mariadb') { + $backupPayload['databases_to_backup'] = $database->mariadb_database; + } elseif ($database->type() === 'standalone-mongodbs') { + $backupPayload['databases_to_backup'] = $database->mongo_initdb_database; + } + + $backupConfig = ScheduledDatabaseBackup::create([ + 'database_id' => $database->id, + 'database_type' => $database->getMorphClass(), + 'team_id' => $teamId, + 's3_storage_id' => $backupPayload['s3_storage_id'] ?? 1, + ...$backupPayload, + ]); + } + + if ($backupPayload && $backupConfig) { + $backupConfig->update($backupPayload); + } if ($whatToDoWithDatabaseProxy === 'start') { StartDatabaseProxy::dispatch($database); diff --git a/routes/api.php b/routes/api.php index 409dd393f..326399f30 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,9 +23,6 @@ }); Route::post('/feedback', [OtherController::class, 'feedback']); -Route::get('/test', function () { - return response()->json(['message' => 'test']); -}); Route::group([ 'middleware' => ['auth:sanctum', 'api.ability:write'], From 2a06a392d5174f278f20cf9533644d1e7fd2c747 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 11:46:02 +0200 Subject: [PATCH 0015/1717] Implement backup delete --- .../Controllers/Api/DatabasesController.php | 95 +++++++++++++++++++ routes/api.php | 1 + 2 files changed, 96 insertions(+) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index ab0191581..a25b07bf2 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1750,6 +1750,101 @@ public function delete_by_uuid(Request $request) ]); } + #[OA\Delete( + summary: 'Delete backup', + description: 'Deletes a backup by its database UUID and backup ID.', + path: '/databases/{uuid}/backups/{backup_id}', + operationId: 'delete-backup-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['backups'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + required: true, + description: 'UUID of the database to delete', + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'backup_id', + in: 'path', + required: true, + description: 'ID of the backup to delete', + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'delete_s3', + in: 'query', + required: false, + description: 'Whether to delete the backup from S3', + schema: new OA\Schema(type: 'boolean', default: false) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Backup deleted.', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'message' => new OA\Schema(type: 'string', example: 'Backup deleted.'), + ] + ) + ), + new OA\Response( + response: 404, + description: 'Backup not found.', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'message' => new OA\Schema(type: 'string', example: 'Backup not found.'), + ] + ) + ), + ] + )] + public function delete_backup_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + $backup = ScheduledDatabaseBackup::where('database_id', $database->id)->first(); + if (! $backup) { + return response()->json(['message' => 'Backup not found.'], 404); + } + $execution = $backup->executions()->where('id', $request->backup_id)->first(); + if (! $execution) { + return response()->json(['message' => 'Execution not found.'], 404); + } + + $deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN); + + try { + if ($execution->filename) { + deleteBackupsLocally($execution->filename, $database->destination->server); + + if ($deleteS3 && $backup->s3) { + deleteBackupsS3($execution->filename, $backup->s3); + } + } + + $execution->delete(); + + return response()->json([ + 'message' => 'Backup deleted.', + ]); + } catch (\Exception $e) { + return response()->json(['message' => 'Failed to delete backup: '.$e->getMessage()], 500); + } + } + #[OA\Get( summary: 'Start', description: 'Start database. `Post` request is also accepted.', diff --git a/routes/api.php b/routes/api.php index 326399f30..1a1990513 100644 --- a/routes/api.php +++ b/routes/api.php @@ -114,6 +114,7 @@ Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_uuid'])->middleware(['api.ability:read']); Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); + Route::delete('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']); Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:write']); Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:write']); From 81180af27d4f5870bd7e4253c7fd3804eeac2afb Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 15:49:14 +0200 Subject: [PATCH 0016/1717] add ability to get backup now and get all schedule backup --- .../Controllers/Api/DatabasesController.php | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index a25b07bf2..9d007939d 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -9,6 +9,7 @@ use App\Actions\Database\StopDatabaseProxy; use App\Enums\NewDatabaseTypes; use App\Http\Controllers\Controller; +use App\Jobs\DatabaseBackupJob; use App\Jobs\DeleteResourceJob; use App\Models\Project; use App\Models\ScheduledDatabaseBackup; @@ -80,12 +81,11 @@ public function databases(Request $request) $databases = $databases->merge($project->databases()); } - $backupConfig = ScheduledDatabaseBackup::with('latest_log')->get(); - $databases = $databases->map(function ($database) use ($backupConfig) { - $databaseBackupConfig = $backupConfig->where('database_id', $database->id)->first(); + $databases = $databases->map(function ($database) { + $backupConfig = ScheduledDatabaseBackup::with('latest_log')->where('database_id', $database->id)->get(); - if ($databaseBackupConfig) { - $database->backup_configs = $databaseBackupConfig; + if ($backupConfig) { + $database->backup_configs = $backupConfig; } else { $database->backup_configs = null; } @@ -98,7 +98,7 @@ public function databases(Request $request) #[OA\Get( summary: 'Get', - description: 'Get database by UUID.', + description: 'Get backups details by database UUID.', path: '/databases/{uuid}/backups', operationId: 'get-database-backups-by-uuid', security: [ @@ -291,6 +291,7 @@ public function database_by_uuid(Request $request) // WIP 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], + 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], @@ -326,7 +327,7 @@ public function database_by_uuid(Request $request) )] public function update_by_uuid(Request $request) { - $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; + $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -352,6 +353,7 @@ public function update_by_uuid(Request $request) 'limits_cpuset' => 'string|nullable', 'limits_cpu_shares' => 'numeric', 'save_s3' => 'boolean', + 'backup_now' => 'boolean|nullable', 'enabled' => 'boolean', 'dump_all' => 'boolean', 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', @@ -573,7 +575,7 @@ public function update_by_uuid(Request $request) } break; } - $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields); + $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields, ['backup_now']); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); if (! empty($extraFields)) { @@ -620,10 +622,18 @@ public function update_by_uuid(Request $request) 's3_storage_id' => $backupPayload['s3_storage_id'] ?? 1, ...$backupPayload, ]); + + if ($request->backup_now) { + DatabaseBackupJob::dispatch($backupConfig); + } } if ($backupPayload && $backupConfig) { $backupConfig->update($backupPayload); + + if ($request->backup_now) { + DatabaseBackupJob::dispatch($backupConfig); + } } if ($whatToDoWithDatabaseProxy === 'start') { From 71ff19e746e59619ed2975877ea0754ada07b5cb Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 15:53:23 +0200 Subject: [PATCH 0017/1717] get all of the backups --- app/Http/Controllers/Api/DatabasesController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 9d007939d..9c04d1d42 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -154,7 +154,7 @@ public function database_backup_details_uuid(Request $request) return response()->json(['message' => 'Database not found.'], 404); } - $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->first(); + $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->get(); return response()->json($this->removeSensitiveData($backupConfig)); } From b4119fe012052f5d083c0d849d2f2942eca02f40 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 16:43:05 +0200 Subject: [PATCH 0018/1717] change the order of update --- .../Controllers/Api/DatabasesController.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 9c04d1d42..389983920 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -604,6 +604,15 @@ public function update_by_uuid(Request $request) $database->update($databasePayload); } + if ($backupPayload && $backupConfig) { + $backupConfig->update($backupPayload); + + if ($request->backup_now) { + dd('test'); + DatabaseBackupJob::dispatch($backupConfig); + } + } + if ($backupPayload && ! $backupConfig) { if ($database->type() === 'standalone-postgresql') { $backupPayload['databases_to_backup'] = $database->postgres_db; @@ -628,14 +637,6 @@ public function update_by_uuid(Request $request) } } - if ($backupPayload && $backupConfig) { - $backupConfig->update($backupPayload); - - if ($request->backup_now) { - DatabaseBackupJob::dispatch($backupConfig); - } - } - if ($whatToDoWithDatabaseProxy === 'start') { StartDatabaseProxy::dispatch($database); } elseif ($whatToDoWithDatabaseProxy === 'stop') { From 166e5ad2271479b8ea6d8d7ea1a849fed85d0aad Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 17:20:48 +0200 Subject: [PATCH 0019/1717] remove dd --- app/Http/Controllers/Api/DatabasesController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 389983920..4f62da8bf 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -608,7 +608,6 @@ public function update_by_uuid(Request $request) $backupConfig->update($backupPayload); if ($request->backup_now) { - dd('test'); DatabaseBackupJob::dispatch($backupConfig); } } From be104cd612cdf3e13523c0077bb4273cb95687a5 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Thu, 22 May 2025 14:36:14 +0200 Subject: [PATCH 0020/1717] feat(api): add endpoint to update backup configuration by UUID and backup ID; modify response to include backup id --- .../Controllers/Api/DatabasesController.php | 153 +++++++++++++++++- routes/api.php | 1 + 2 files changed, 152 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 4f62da8bf..7172e5aae 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -156,7 +156,7 @@ public function database_backup_details_uuid(Request $request) $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->get(); - return response()->json($this->removeSensitiveData($backupConfig)); + return response()->json($backupConfig); } #[OA\Get( @@ -288,7 +288,6 @@ public function database_by_uuid(Request $request) 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], - // WIP 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], @@ -647,6 +646,156 @@ public function update_by_uuid(Request $request) ]); } + #[OA\Patch( + summary: 'Update', + description: 'Update a specific backup configuration for a given database, identified by its UUID and the backup ID', + path: '/databases/{uuid}/backups/{backup_id}', + operationId: 'update-database-backup-config-by-uuid-and-backup-id', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter( + name: 'backup_id', + in: 'path', + description: 'ID of the backup configuration.', + required: true, + schema: new OA\Schema( + type: 'integer', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Database backup configuration data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], + 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], + 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], + 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], + 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], + 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], + 'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'], + 'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'], + 'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'], + 'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], + 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], + 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database backup configuration updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_backup_config_by_uuid_and_backup_id(Request $request) + { + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + // this check if the request is a valid json + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'save_s3' => 'boolean', + 'backup_now' => 'boolean|nullable', + 'enabled' => 'boolean', + 'dump_all' => 'boolean', + 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', + 'databases_to_backup' => 'string', + 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', + 'database_backup_retention_amount_locally' => 'integer|min:0', + 'database_backup_retention_days_locally' => 'integer|min:0', + 'database_backup_retention_max_storage_locally' => 'integer|min:0', + 'database_backup_retention_amount_s3' => 'integer|min:0', + 'database_backup_retention_days_s3' => 'integer|min:0', + 'database_backup_retention_max_storage_s3' => 'integer|min:0', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $uuid = $request->uuid; + removeUnnecessaryFieldsFromRequest($request); + $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id) + ->where('id', $request->backup_id) + ->first(); + if (! $backupConfig) { + return response()->json(['message' => 'Backup config not found.'], 404); + } + + $extraFields = array_diff(array_keys($request->all()), $backupConfigFields, ['backup_now']); + if (! empty($extraFields)) { + $errors = $validator->errors(); + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + $backupConfig->update($request->only($backupConfigFields)); + + if ($request->backup_now) { + DatabaseBackupJob::dispatch($backupConfig); + } + + return response()->json([ + 'message' => 'Database backup configuration updated', + ]); + } + #[OA\Post( summary: 'Create (PostgreSQL)', description: 'Create a new PostgreSQL database.', diff --git a/routes/api.php b/routes/api.php index 1a1990513..a5abe4b98 100644 --- a/routes/api.php +++ b/routes/api.php @@ -113,6 +113,7 @@ Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']); Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_uuid'])->middleware(['api.ability:read']); Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']); + Route::patch('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'update_backup_config_by_uuid_and_backup_id'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']); From 2bf6a9cb2c324715b19d87e88babfba1ebc7ca30 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Thu, 22 May 2025 14:39:36 +0200 Subject: [PATCH 0021/1717] undo changes to update_by_uuid method --- .../Controllers/Api/DatabasesController.php | 56 +------------------ 1 file changed, 2 insertions(+), 54 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 7172e5aae..4fa42c37d 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -326,7 +326,6 @@ public function database_by_uuid(Request $request) )] public function update_by_uuid(Request $request) { - $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -351,19 +350,6 @@ public function update_by_uuid(Request $request) 'limits_cpus' => 'string', 'limits_cpuset' => 'string|nullable', 'limits_cpu_shares' => 'numeric', - 'save_s3' => 'boolean', - 'backup_now' => 'boolean|nullable', - 'enabled' => 'boolean', - 'dump_all' => 'boolean', - 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', - 'databases_to_backup' => 'string', - 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', - 'database_backup_retention_amount_locally' => 'integer|min:0', - 'database_backup_retention_days_locally' => 'integer|min:0', - 'database_backup_retention_max_storage_locally' => 'integer|min:0', - 'database_backup_retention_amount_s3' => 'integer|min:0', - 'database_backup_retention_days_s3' => 'integer|min:0', - 'database_backup_retention_max_storage_s3' => 'integer|min:0', ]); if ($validator->fails()) { @@ -375,7 +361,6 @@ public function update_by_uuid(Request $request) $uuid = $request->uuid; removeUnnecessaryFieldsFromRequest($request); $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); - $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id)->first(); if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } @@ -574,7 +559,7 @@ public function update_by_uuid(Request $request) } break; } - $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields, ['backup_now']); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); if (! empty($extraFields)) { @@ -596,44 +581,7 @@ public function update_by_uuid(Request $request) $whatToDoWithDatabaseProxy = 'start'; } - $backupPayload = $request->only($allowedBackupConfigsFields); - $databasePayload = $request->only($allowedFields); - - if ($databasePayload) { - $database->update($databasePayload); - } - - if ($backupPayload && $backupConfig) { - $backupConfig->update($backupPayload); - - if ($request->backup_now) { - DatabaseBackupJob::dispatch($backupConfig); - } - } - - if ($backupPayload && ! $backupConfig) { - if ($database->type() === 'standalone-postgresql') { - $backupPayload['databases_to_backup'] = $database->postgres_db; - } elseif ($database->type() === 'standalone-mysql') { - $backupPayload['databases_to_backup'] = $database->mysql_database; - } elseif ($database->type() === 'standalone-mariadb') { - $backupPayload['databases_to_backup'] = $database->mariadb_database; - } elseif ($database->type() === 'standalone-mongodbs') { - $backupPayload['databases_to_backup'] = $database->mongo_initdb_database; - } - - $backupConfig = ScheduledDatabaseBackup::create([ - 'database_id' => $database->id, - 'database_type' => $database->getMorphClass(), - 'team_id' => $teamId, - 's3_storage_id' => $backupPayload['s3_storage_id'] ?? 1, - ...$backupPayload, - ]); - - if ($request->backup_now) { - DatabaseBackupJob::dispatch($backupConfig); - } - } + $database->update($request->all()); if ($whatToDoWithDatabaseProxy === 'start') { StartDatabaseProxy::dispatch($database); From 07cdb4ddcc523b91b3997a6c7ebb03094ca0743e Mon Sep 17 00:00:00 2001 From: alexbaron-dev Date: Thu, 22 May 2025 18:03:39 +0200 Subject: [PATCH 0022/1717] Create opnform.yaml Add opnform.yaml as template to deploy OpnForm app --- templates/compose/opnform.yaml | 232 +++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 templates/compose/opnform.yaml diff --git a/templates/compose/opnform.yaml b/templates/compose/opnform.yaml new file mode 100644 index 000000000..1fe9644b6 --- /dev/null +++ b/templates/compose/opnform.yaml @@ -0,0 +1,232 @@ +# documentation: https://docs.opnform.com/introduction +# slogan: OpnForm is an open-source form builder that lets you create beautiful forms and share them anywhere. It's super fast, you don't need to know how to code +# tags: opnform, form, survey, cloud, open-source, self-hosted, docker, no-code, embeddable +# logo: svg/opnform.svg +# port: 80 + +x-shared-env: &shared-api-env + APP_NAME: "OpnForm" + APP_ENV: production + APP_KEY: ${SERVICE_BASE64_APIKEY} + APP_DEBUG: ${APP_DEBUG:-false} + APP_URL: ${SERVICE_FQDN_NGINX} + SELF_HOSTED: ${SELF_HOSTED:-true} + LOG_CHANNEL: errorlog + LOG_LEVEL: ${LOG_LEVEL:-debug} + FILESYSTEM_DRIVER: ${FILESYSTEM_DRIVER:-local} + LOCAL_FILESYSTEM_VISIBILITY: public + CACHE_DRIVER: redis + QUEUE_CONNECTION: redis + SESSION_DRIVER: redis + SESSION_LIFETIME: 120 + MAIL_MAILER: ${MAIL_MAILER:-log} + MAIL_HOST: ${MAIL_HOST} + MAIL_PORT: ${MAIL_PORT} + MAIL_USERNAME: ${MAIL_USERNAME:-your@email.com} + MAIL_PASSWORD: ${MAIL_PASSWORD} + MAIL_ENCRYPTION: ${MAIL_ENCRYPTION} + MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS:-your@email.com} + MAIL_FROM_NAME: ${MAIL_FROM_NAME:-OpnForm} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-east-1} + AWS_BUCKET: ${AWS_BUCKET} + JWT_TTL: ${JWT_TTL:-1440} + JWT_SECRET: ${SERVICE_PASSWORD_JWTSECRET} + JWT_SKIP_IP_UA_VALIDATION: ${JWT_SKIP_IP_UA_VALIDATION:-true} + OPEN_AI_API_KEY: ${OPEN_AI_API_KEY} + H_CAPTCHA_SITE_KEY: ${H_CAPTCHA_SITE_KEY} + H_CAPTCHA_SECRET_KEY: ${H_CAPTCHA_SECRET_KEY} + RE_CAPTCHA_SITE_KEY: ${RE_CAPTCHA_SITE_KEY} + RE_CAPTCHA_SECRET_KEY: ${RE_CAPTCHA_SECRET_KEY} + TELEGRAM_BOT_ID: ${TELEGRAM_BOT_ID} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} + SHOW_OFFICIAL_TEMPLATES: ${SHOW_OFFICIAL_TEMPLATES:-true} + REDIS_HOST: redis + REDIS_PASSWORD: ${SERVICE_PASSWORD_64_REDIS} + # Database settings + DB_HOST: postgresql + DB_DATABASE: ${POSTGRESQL_DATABASE:-opnform} + DB_USERNAME: ${SERVICE_USER_POSTGRESQL} + DB_PASSWORD: ${SERVICE_PASSWORD_POSTGRESQL} + DB_CONNECTION: pgsql + # PHP Configuration + PHP_MEMORY_LIMIT: "1G" + PHP_MAX_EXECUTION_TIME: "600" + PHP_UPLOAD_MAX_FILESIZE: "64M" + PHP_POST_MAX_SIZE: "64M" + +services: + opnform-api: + image: jhumanj/opnform-api:latest + volumes: &api-environment-volumes + - api-storage:/usr/share/nginx/html/storage:rw + environment: + # Use the shared environment variables. + <<: *shared-api-env + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan about || exit 1"] + interval: 30s + timeout: 15s + retries: 3 + start_period: 60s + + api-worker: + image: jhumanj/opnform-api:latest + volumes: *api-environment-volumes + environment: + # Use the shared environment variables. + <<: *shared-api-env + command: ["php", "artisan", "queue:work"] + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pgrep -f 'php artisan queue:work' > /dev/null || exit 1"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 30s + + api-scheduler: + image: jhumanj/opnform-api:latest + volumes: *api-environment-volumes + environment: + # Use the shared environment variables. + <<: *shared-api-env + command: ["php", "artisan", "schedule:work"] + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan app:scheduler-status --mode=check --max-minutes=3 || exit 1"] + interval: 60s + timeout: 30s + retries: 3 + start_period: 70s # Allow time for first scheduled run and cache write + + opnform-ui: + image: jhumanj/opnform-client:latest + environment: + - NUXT_PUBLIC_APP_URL=/ + - NUXT_PUBLIC_API_BASE=/api + - NUXT_PRIVATE_API_BASE=http://nginx/api + - NUXT_PUBLIC_ENV=${NUXT_PUBLIC_ENV:-production} + - NUXT_PUBLIC_H_CAPTCHA_SITE_KEY=${H_CAPTCHA_SITE_KEY} + - NUXT_PUBLIC_RE_CAPTCHA_SITE_KEY=${RE_CAPTCHA_SITE_KEY} + - NUXT_PUBLIC_ROOT_REDIRECT_URL=${NUXT_PUBLIC_ROOT_REDIRECT_URL} + healthcheck: + test: ["CMD-SHELL", "wget --spider -q http://opnform-ui:3000/login || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 45s + + postgresql: + image: postgres:16 + volumes: + - opnform-postgresql-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: ${SERVICE_USER_POSTGRESQL} + POSTGRES_PASSWORD: ${SERVICE_PASSWORD_POSTGRESQL} + POSTGRES_DB: ${POSTGRESQL_DATABASE:-opnform} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 + + redis: + image: redis:7 + environment: + REDIS_PASSWORD: ${SERVICE_PASSWORD_64_REDIS} + volumes: + - redis-data:/data + command: ["redis-server", "--requirepass", "${SERVICE_PASSWORD_64_REDIS}"] + healthcheck: + test: ["CMD", "redis-cli", "-a", "${SERVICE_PASSWORD_64_REDIS}", "PING"] + interval: 10s + timeout: 30s + retries: 3 + + # The nginx reverse proxy. + # used for reverse proxying the API service and Web service. + nginx: + image: nginx:latest + volumes: + - type: bind + source: ./nginx/nginx.conf.template + target: /etc/nginx/conf.d/opnform.conf + read_only: true + content: | + map $request_uri $api_uri { + ~^/api(/.*$) $1; + default $request_uri; + } + + server { + listen 80 default_server; + root /usr/share/nginx/html/public; + + access_log /dev/stdout; + error_log /dev/stderr error; + + index index.html index.htm index.php; + + location / { + proxy_http_version 1.1; + proxy_pass http://opnform-ui:3000; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location ~/(api|open|local\/temp|forms\/assets)/ { + set $original_uri $uri; + try_files $uri $uri/ /index.php$is_args$args; + } + + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass opnform-api:9000; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + fastcgi_param REQUEST_URI $api_uri; + } + + # Deny access to . files + location ~ /\. { + deny all; + } + } + environment: + - SERVICE_FQDN_NGINX + depends_on: + - opnform-api + - opnform-ui + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + api-storage: + driver: local + opnform-postgresql-data: + driver: local + redis-data: + driver: local From c4cb0f70367e168d228ca55858bb4f2366ab9419 Mon Sep 17 00:00:00 2001 From: alexbaron-dev Date: Thu, 22 May 2025 18:08:24 +0200 Subject: [PATCH 0023/1717] Add opnform logo into svgs directory --- public/svgs/opnform.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/svgs/opnform.svg diff --git a/public/svgs/opnform.svg b/public/svgs/opnform.svg new file mode 100644 index 000000000..70562a4bf --- /dev/null +++ b/public/svgs/opnform.svg @@ -0,0 +1 @@ + \ No newline at end of file From 934126778370673472a1907747108d32af1b4cbb Mon Sep 17 00:00:00 2001 From: alexbaron-dev Date: Fri, 23 May 2025 18:32:26 +0200 Subject: [PATCH 0024/1717] Update opnform.yaml - split environment variables per usage (api, worker and scheduler) - remove volume anchor - remove volumes definition --- templates/compose/opnform.yaml | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/templates/compose/opnform.yaml b/templates/compose/opnform.yaml index 1fe9644b6..1b658bca9 100644 --- a/templates/compose/opnform.yaml +++ b/templates/compose/opnform.yaml @@ -31,17 +31,9 @@ x-shared-env: &shared-api-env AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-east-1} AWS_BUCKET: ${AWS_BUCKET} - JWT_TTL: ${JWT_TTL:-1440} - JWT_SECRET: ${SERVICE_PASSWORD_JWTSECRET} - JWT_SKIP_IP_UA_VALIDATION: ${JWT_SKIP_IP_UA_VALIDATION:-true} OPEN_AI_API_KEY: ${OPEN_AI_API_KEY} - H_CAPTCHA_SITE_KEY: ${H_CAPTCHA_SITE_KEY} - H_CAPTCHA_SECRET_KEY: ${H_CAPTCHA_SECRET_KEY} - RE_CAPTCHA_SITE_KEY: ${RE_CAPTCHA_SITE_KEY} - RE_CAPTCHA_SECRET_KEY: ${RE_CAPTCHA_SECRET_KEY} TELEGRAM_BOT_ID: ${TELEGRAM_BOT_ID} TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} - SHOW_OFFICIAL_TEMPLATES: ${SHOW_OFFICIAL_TEMPLATES:-true} REDIS_HOST: redis REDIS_PASSWORD: ${SERVICE_PASSWORD_64_REDIS} # Database settings @@ -59,11 +51,19 @@ x-shared-env: &shared-api-env services: opnform-api: image: jhumanj/opnform-api:latest - volumes: &api-environment-volumes - - api-storage:/usr/share/nginx/html/storage:rw + volumes: + - api-storage:/usr/share/nginx/html/storage environment: # Use the shared environment variables. <<: *shared-api-env + JWT_TTL: ${JWT_TTL:-1440} + JWT_SECRET: ${SERVICE_PASSWORD_JWTSECRET} + JWT_SKIP_IP_UA_VALIDATION: ${JWT_SKIP_IP_UA_VALIDATION:-true} + H_CAPTCHA_SITE_KEY: ${H_CAPTCHA_SITE_KEY} + H_CAPTCHA_SECRET_KEY: ${H_CAPTCHA_SECRET_KEY} + RE_CAPTCHA_SITE_KEY: ${RE_CAPTCHA_SITE_KEY} + RE_CAPTCHA_SECRET_KEY: ${RE_CAPTCHA_SECRET_KEY} + SHOW_OFFICIAL_TEMPLATES: ${SHOW_OFFICIAL_TEMPLATES:-true} depends_on: postgresql: condition: service_healthy @@ -78,7 +78,8 @@ services: api-worker: image: jhumanj/opnform-api:latest - volumes: *api-environment-volumes + volumes: + - api-storage:/usr/share/nginx/html/storage environment: # Use the shared environment variables. <<: *shared-api-env @@ -97,7 +98,8 @@ services: api-scheduler: image: jhumanj/opnform-api:latest - volumes: *api-environment-volumes + volumes: + - api-storage:/usr/share/nginx/html/storage environment: # Use the shared environment variables. <<: *shared-api-env @@ -222,11 +224,3 @@ services: timeout: 10s retries: 3 start_period: 40s - -volumes: - api-storage: - driver: local - opnform-postgresql-data: - driver: local - redis-data: - driver: local From d3c4a4fa1363f16ca0e4780db2717e1b8664fe84 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Tue, 17 Jun 2025 09:41:49 +0200 Subject: [PATCH 0025/1717] feat(signoz): use latest tag instead of hardcoded versions --- templates/compose/signoz.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 288f67cb8..f4209b01f 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -1465,7 +1465,7 @@ services: target: /var/lib/clickhouse/ signoz: - image: signoz/signoz:${VERSION:-v0.78.1} + image: signoz/signoz:latest container_name: signoz depends_on: clickhouse: @@ -1546,7 +1546,7 @@ services: retries: 3 otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.38} + image: signoz/signoz-otel-collector:latest container_name: signoz-otel-collector depends_on: clickhouse: @@ -1691,7 +1691,7 @@ services: retries: 3 schema-migrator-sync: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38} + image: signoz/signoz-schema-migrator:latest container_name: schema-migrator-sync command: - sync @@ -1707,7 +1707,7 @@ services: max-file: "3" schema-migrator-async: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38} + image: signoz/signoz-schema-migrator:latest container_name: schema-migrator-async depends_on: clickhouse: From 5b92d9945bbbd9ce4370857ba4a9857352bc1f02 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Tue, 17 Jun 2025 11:10:05 +0200 Subject: [PATCH 0026/1717] feat(signoz): remove redundant users.xml volume from clickhouse container --- templates/compose/signoz.yaml | 127 ---------------------------------- 1 file changed, 127 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index f4209b01f..6877ef9e5 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -1229,133 +1229,6 @@ services: true - - type: bind - source: ./clickhouse/users.xml - target: /etc/clickhouse-server/users.xml - content: | - - - - - - - - - - 10000000000 - - - random - - - - - 1 - - - - - - - - - - - - - ::/0 - - - - default - - - default - - - - - - - - - - - - - - 3600 - - - 0 - 0 - 0 - 0 - 0 - - - - - type: bind source: ./clickhouse/custom-function.xml target: /etc/clickhouse-server/custom-function.xml From 10b4126aa1256a14bb3854c6707c0f6d4c244a53 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Tue, 17 Jun 2025 11:37:38 +0200 Subject: [PATCH 0027/1717] feat(signoz): replace clickhouse' config.xml volume with simpler configuration --- templates/compose/signoz.yaml | 1155 +-------------------------------- 1 file changed, 13 insertions(+), 1142 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 6877ef9e5..fb2217ae0 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -84,1150 +84,21 @@ services: max-file: "3" volumes: - type: bind - source: ./clickhouse/config.xml - target: /etc/clickhouse-server/config.xml + source: ./clickhouse/config.d/config.xml + target: /etc/clickhouse-server/config.d/config.xml content: | - - - - - information - - json - - /var/log/clickhouse-server/clickhouse-server.log - /var/log/clickhouse-server/clickhouse-server.err.log - - 1000M - 10 - - - - - - - - - - - - - - - - - - 8123 - - - 9000 - - - 9004 - - - 9005 - - - - - - - - - - - - 9009 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4096 - - - 3 - - - - - false - - - /path/to/ssl_cert_file - /path/to/ssl_key_file - - - false - - - /path/to/ssl_ca_cert_file - - - none - - - 0 - - - -1 - -1 - - - false - - - - - - - - - - - none - true - true - sslv2,sslv3 - true - - - - true - true - sslv2,sslv3 - true - - - - RejectCertificateHandler - - - - - - - - - 100 - - - 0 - - - - 10000 - - - - - - 0.9 - - - 4194304 - - - 0 - - - - - - 8589934592 - - - 5368709120 - - - - 1000 - - - 134217728 - - - 10000 - - - /var/lib/clickhouse/ - - - /var/lib/clickhouse/tmp/ - - - - ` - - - - - - /var/lib/clickhouse/user_files/ - - - - - - - - - - - - - users.xml - - - - /var/lib/clickhouse/access/ - - - - - - - default - - - - - - - - - - - - default - - - - - - - - - true - - - false - - ' | sed -e 's|.*>\(.*\)<.*|\1|') - wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v$PKG_VER/clickhouse-jdbc-bridge_$PKG_VER-1_all.deb - apt install --no-install-recommends -f ./clickhouse-jdbc-bridge_$PKG_VER-1_all.deb - clickhouse-jdbc-bridge & - - * [CentOS/RHEL] - export MVN_URL=https://repo1.maven.org/maven2/ru/yandex/clickhouse/clickhouse-jdbc-bridge - export PKG_VER=$(curl -sL $MVN_URL/maven-metadata.xml | grep '' | sed -e 's|.*>\(.*\)<.*|\1|') - wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v$PKG_VER/clickhouse-jdbc-bridge-$PKG_VER-1.noarch.rpm - yum localinstall -y clickhouse-jdbc-bridge-$PKG_VER-1.noarch.rpm - clickhouse-jdbc-bridge & - - Please refer to https://github.com/ClickHouse/clickhouse-jdbc-bridge#usage for more information. - ]]> - - - - - - - - - - - - - - - 01 - example01-01-1 - - - - - - 3600 - - - - 3600 - - - 60 - - - - - - - - - - /metrics - 9363 - - true - true - true - true - - - - - - system - query_log
- - toYYYYMM(event_date) - - - - - - 7500 -
- - - - system - trace_log
- - toYYYYMM(event_date) - 7500 -
- - - - system - query_thread_log
- toYYYYMM(event_date) - 7500 -
- - - - system - query_views_log
- toYYYYMM(event_date) - 7500 -
- - - - system - part_log
- toYYYYMM(event_date) - 7500 -
- - - - - - system - metric_log
- 7500 - 1000 -
- - - - system - asynchronous_metric_log
- - 7000 -
- - - - - - engine MergeTree - partition by toYYYYMM(finish_date) - order by (finish_date, finish_time_us, trace_id) - - system - opentelemetry_span_log
- 7500 -
- - - - - system - crash_log
- - - 1000 -
- - - - - - - system - processors_profile_log
- - toYYYYMM(event_date) - 7500 -
- - - - - - - - - *_dictionary.xml - - - *function.xml - /var/lib/clickhouse/user_scripts/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /clickhouse/task_queue/ddl - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - click_cost - any - - 0 - 3600 - - - 86400 - 60 - - - - max - - 0 - 60 - - - 3600 - 300 - - - 86400 - 3600 - - - - - - /var/lib/clickhouse/format_schemas/ - - - - - hide encrypt/decrypt arguments - ((?:aes_)?(?:encrypt|decrypt)(?:_mysql)?)\s*\(\s*(?:'(?:\\'|.)+'|.*?)\s*\) - - \1(???) - - - - - - - - - - false - - false - - - https://6f33034cfe684dd7a3ab9875e57b1c8d@o388870.ingest.sentry.io/5226277 - - - - - - - - - - - 268435456 - true - + + information + + json + + + + 01 + example01-01-1 + + *function.xml
- type: bind source: ./clickhouse/custom-function.xml From ade1a72804a628a8e239721c5704acd96e44d9a9 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Tue, 17 Jun 2025 11:40:25 +0200 Subject: [PATCH 0028/1717] feat(signoz): remove deprecated parameters of signoz container --- templates/compose/signoz.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index fb2217ae0..8cd7f90a3 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -223,8 +223,6 @@ services: max-file: "3" command: - --config=/root/config/prometheus.yml - - --use-logs-new-schema=true - - --use-trace-new-schema=true volumes: - type: bind source: ./prometheus.yml From d5e85c2657b043cb3f998fd6e04b5e8607cf0788 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Wed, 18 Jun 2025 18:36:40 +0200 Subject: [PATCH 0029/1717] feat(signoz): remove volumes from signoz.yaml --- templates/compose/signoz.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 8cd7f90a3..cf77b6f5a 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -465,11 +465,3 @@ services: - --dsn=tcp://clickhouse:9000 - --up= restart: on-failure - -volumes: - clickhouse: - name: signoz-clickhouse - sqlite: - name: signoz-sqlite - zookeeper-1: - name: signoz-zookeeper-1 From b6a43338e31f2724fa9708111d1841aae175ea56 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Wed, 18 Jun 2025 18:38:11 +0200 Subject: [PATCH 0030/1717] feat(signoz): assume there is a single zookeeper container --- templates/compose/signoz.yaml | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index cf77b6f5a..6492037dd 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -27,9 +27,9 @@ services: max-size: 50m max-file: "3" - zookeeper-1: + zookeeper: image: bitnami/zookeeper:3.7.1 - container_name: signoz-zookeeper-1 + container_name: signoz-zookeeper user: root healthcheck: test: @@ -44,13 +44,12 @@ services: max-size: 50m max-file: "3" volumes: - - zookeeper-1:/bitnami/zookeeper + - zookeeper:/bitnami/zookeeper environment: - - ZOO_SERVER_ID=1 - - ALLOW_ANONYMOUS_LOGIN=yes - - ZOO_AUTOPURGE_INTERVAL=1 - - ZOO_ENABLE_PROMETHEUS_METRICS=yes - - ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141 + - ALLOW_ANONYMOUS_LOGIN=${ZOO_ALLOW_ANONYMOUS_LOGIN:-yes} + - ZOO_AUTOPURGE_INTERVAL=${ZOO_AUTOPURGE_INTERVAL:-1} + - ZOO_ENABLE_PROMETHEUS_METRICS=${ZOO_ENABLE_PROMETHEUS_METRICS:-yes} + - ZOO_PROMETHEUS_METRICS_PORT_NUMBER=${ZOO_PROMETHEUS_METRICS_PORT_NUMBER:-9141} clickhouse: # addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab @@ -60,7 +59,7 @@ services: depends_on: init-clickhouse: condition: service_completed_successfully - zookeeper-1: + zookeeper: condition: service_healthy healthcheck: test: @@ -129,7 +128,6 @@ services: source: ./clickhouse/cluster.xml target: /etc/clickhouse-server/config.d/cluster.xml content: | - - zookeeper-1 + zookeeper 2181 - + + + + + + + 10000000000 + + + random + + + + + 1 + + + + + + + + + + + + + ::/0 + + + + default + + + default + + + + + + + + + + + + + + 3600 + + + 0 + 0 + 0 + 0 + 0 + + + + + - type: bind + source: ./clickhouse/config.xml + target: /etc/clickhouse-server/config.xml + content: | + + + 4096 + 3 + 100 + 5368709120 + 1000 + 134217728 + 10000 + + *_dictionary.xml + *function.xml + /var/lib/clickhouse/user_scripts/ + 8123 + 9000 + 9004 + 9005 + 9009 + + information + + json + + + + 01 + example01-01-1 + + + /metrics + 9363 + true + true + true + true + + + engine MergeTree + partition by toYYYYMM(finish_date) + order by (finish_date, finish_time_us, trace_id) + + + + hide encrypt/decrypt arguments + ((?:aes_)?(?:encrypt|decrypt)(?:_mysql)?)\s*\(\s*(?:'(?:\\'|.)+'|.*?)\s*\) + \1(???) + + + + false + false + https://6f33034cfe684dd7a3ab9875e57b1c8d@o388870.ingest.sentry.io/5226277 + + + 268435456 + true + + + + + users.xml + + + + /var/lib/clickhouse/access/ + + + default + + + /clickhouse/task_queue/ddl + + signoz: image: signoz/signoz:latest From 5301ec2b459bebc56780049c2f46150ad25fd558 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Wed, 2 Jul 2025 11:38:06 +0200 Subject: [PATCH 0035/1717] feat(signoz): update otel-collector configuration to match upstream --- templates/compose/signoz.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 48fbe4870..ca3cde268 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -521,7 +521,7 @@ services: detectors: [env, system] timeout: 2s signozspanmetrics/delta: - metrics_exporter: clickhousemetricswrite + metrics_exporter: clickhousemetricswrite, signozclickhousemetrics metrics_flush_interval: 60s latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ] dimensions_cache_size: 100000 @@ -557,10 +557,12 @@ services: use_new_schema: true clickhousemetricswrite: endpoint: tcp://clickhouse:9000/signoz_metrics + disable_v2: true resource_to_telemetry_conversion: enabled: true clickhousemetricswrite/prometheus: endpoint: tcp://clickhouse:9000/signoz_metrics + disable_v2: true signozclickhousemetrics: dsn: tcp://clickhouse:9000/signoz_metrics clickhouselogsexporter: From ea98e4c29a1c28f8ac0e73256aa796401cdd962c Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Wed, 2 Jul 2025 14:39:53 +0200 Subject: [PATCH 0036/1717] feat(signoz): fix otel-collector config for version v0.128.0 --- templates/compose/signoz.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index ca3cde268..9ffbbec61 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -569,13 +569,10 @@ services: dsn: tcp://clickhouse:9000/signoz_logs timeout: 10s use_new_schema: true - # debug: {} service: telemetry: logs: encoding: json - metrics: - address: 0.0.0.0:8888 extensions: - health_check - pprof From 9cf09a187643b5d764ad0f77de0bec7451f2164a Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Thu, 3 Jul 2025 14:51:47 +0200 Subject: [PATCH 0037/1717] feat(signoz): remove unecessary port mapping for otel-collector --- templates/compose/signoz.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 9ffbbec61..6ea1ec76e 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -480,9 +480,6 @@ services: options: max-size: 50m max-file: "3" - ports: - - "4317:4317" # OTLP gRPC receiver - - "4318:4318" # OTLP HTTP receiver command: - --config=/etc/otel-collector-config.yaml - --manager-config=/etc/manager-config.yaml From 129b55ee298044899485108eb60bf05572fe2681 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Thu, 3 Jul 2025 15:45:25 +0200 Subject: [PATCH 0038/1717] feat(signoz): add SIGNOZ_JWT_SECRET env var generation --- templates/compose/signoz.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 6ea1ec76e..c5adb9869 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -437,6 +437,7 @@ services: environment: - SERVICE_FQDN_SIGNOZ_8080 - SIGNOZ_ALERTMANAGER_PROVIDER=signoz + - SIGNOZ_JWT_SECRET=${SERVICE_REALBASE64_JWTSECRET} - SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000 - SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db - DASHBOARDS_PATH=/root/config/dashboards From 760d59c3681bb645688084d3f7c0647f3dcd043b Mon Sep 17 00:00:00 2001 From: Nageshbansal <76246968+Nageshbansal@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:46:06 +0530 Subject: [PATCH 0039/1717] add changes for release v0.89 --- templates/compose/signoz.yaml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index c5adb9869..3e56b17f8 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -455,6 +455,7 @@ services: - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__USERNAME=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__USERNAME} - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__FROM=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__FROM} - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__SMARTHOST=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__SMARTHOST} + - DOT_METRICS_ENABELD=true healthcheck: test: - CMD @@ -519,7 +520,7 @@ services: detectors: [env, system] timeout: 2s signozspanmetrics/delta: - metrics_exporter: clickhousemetricswrite, signozclickhousemetrics + metrics_exporter: signozclickhousemetrics metrics_flush_interval: 60s latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ] dimensions_cache_size: 100000 @@ -553,14 +554,6 @@ services: datasource: tcp://clickhouse:9000/signoz_traces low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING} use_new_schema: true - clickhousemetricswrite: - endpoint: tcp://clickhouse:9000/signoz_metrics - disable_v2: true - resource_to_telemetry_conversion: - enabled: true - clickhousemetricswrite/prometheus: - endpoint: tcp://clickhouse:9000/signoz_metrics - disable_v2: true signozclickhousemetrics: dsn: tcp://clickhouse:9000/signoz_metrics clickhouselogsexporter: @@ -582,11 +575,11 @@ services: metrics: receivers: [otlp] processors: [batch] - exporters: [clickhousemetricswrite, signozclickhousemetrics] + exporters: [signozclickhousemetrics] metrics/prometheus: receivers: [prometheus] processors: [batch] - exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics] + exporters: [signozclickhousemetrics] logs: receivers: [otlp] processors: [batch] From 2214099c90f03f4681cae3f2b012a4a2b00c0de9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:10:43 +0200 Subject: [PATCH 0040/1717] feat(scheduling): add frequency filter option for manual execution of scheduled jobs --- .../Commands/RunScheduledJobsManually.php | 60 ++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/app/Console/Commands/RunScheduledJobsManually.php b/app/Console/Commands/RunScheduledJobsManually.php index 1685d47cc..238bcbce3 100644 --- a/app/Console/Commands/RunScheduledJobsManually.php +++ b/app/Console/Commands/RunScheduledJobsManually.php @@ -13,6 +13,7 @@ class RunScheduledJobsManually extends Command { protected $signature = 'schedule:run-manual {--type=all : Type of jobs to run (all, backups, tasks)} + {--frequency= : Filter by frequency (daily, hourly, weekly, monthly, yearly, or cron expression)} {--chunk=5 : Number of jobs to process in each batch} {--delay=30 : Delay in seconds between batches} {--max= : Maximum number of jobs to process (useful for testing)} @@ -23,37 +24,52 @@ class RunScheduledJobsManually extends Command public function handle() { $type = $this->option('type'); + $frequency = $this->option('frequency'); $chunkSize = (int) $this->option('chunk'); $delay = (int) $this->option('delay'); $maxJobs = $this->option('max') ? (int) $this->option('max') : null; $dryRun = $this->option('dry-run'); $this->info('Starting manual execution of scheduled jobs...'.($dryRun ? ' (DRY RUN)' : '')); - $this->info("Type: {$type}, Chunk size: {$chunkSize}, Delay: {$delay}s".($maxJobs ? ", Max jobs: {$maxJobs}" : '').($dryRun ? ', Dry run: enabled' : '')); + $this->info("Type: {$type}".($frequency ? ", Frequency: {$frequency}" : '').", Chunk size: {$chunkSize}, Delay: {$delay}s".($maxJobs ? ", Max jobs: {$maxJobs}" : '').($dryRun ? ', Dry run: enabled' : '')); if ($dryRun) { $this->warn('DRY RUN MODE: No jobs will actually be dispatched'); } if ($type === 'all' || $type === 'backups') { - $this->runScheduledBackups($chunkSize, $delay, $maxJobs, $dryRun); + $this->runScheduledBackups($chunkSize, $delay, $maxJobs, $dryRun, $frequency); } if ($type === 'all' || $type === 'tasks') { - $this->runScheduledTasks($chunkSize, $delay, $maxJobs, $dryRun); + $this->runScheduledTasks($chunkSize, $delay, $maxJobs, $dryRun, $frequency); } $this->info('Completed manual execution of scheduled jobs.'.($dryRun ? ' (DRY RUN)' : '')); } - private function runScheduledBackups(int $chunkSize, int $delay, ?int $maxJobs = null, bool $dryRun = false): void + private function runScheduledBackups(int $chunkSize, int $delay, ?int $maxJobs = null, bool $dryRun = false, ?string $frequency = null): void { $this->info('Processing scheduled database backups...'); - $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get(); + $query = ScheduledDatabaseBackup::where('enabled', true); + + if ($frequency) { + $query->where(function ($q) use ($frequency) { + // Handle human-readable frequency strings + if (in_array($frequency, ['daily', 'hourly', 'weekly', 'monthly', 'yearly', 'every_minute'])) { + $q->where('frequency', $frequency); + } else { + // Handle cron expressions + $q->where('frequency', $frequency); + } + }); + } + + $scheduled_backups = $query->get(); if ($scheduled_backups->isEmpty()) { - $this->info('No enabled scheduled backups found.'); + $this->info('No enabled scheduled backups found'.($frequency ? " with frequency '{$frequency}'" : '').'.'); return; } @@ -96,7 +112,7 @@ private function runScheduledBackups(int $chunkSize, int $delay, ?int $maxJobs = $this->info("Limited to {$maxJobs} scheduled backups for testing"); } - $this->info("Found {$finalScheduledBackups->count()} valid scheduled backups to process"); + $this->info("Found {$finalScheduledBackups->count()} valid scheduled backups to process".($frequency ? " with frequency '{$frequency}'" : '')); $chunks = $finalScheduledBackups->chunk($chunkSize); foreach ($chunks as $index => $chunk) { @@ -105,10 +121,10 @@ private function runScheduledBackups(int $chunkSize, int $delay, ?int $maxJobs = foreach ($chunk as $scheduled_backup) { try { if ($dryRun) { - $this->info("🔍 Would dispatch backup job for: {$scheduled_backup->name} (ID: {$scheduled_backup->id})"); + $this->info("🔍 Would dispatch backup job for: {$scheduled_backup->name} (ID: {$scheduled_backup->id}, Frequency: {$scheduled_backup->frequency})"); } else { DatabaseBackupJob::dispatch($scheduled_backup); - $this->info("✓ Dispatched backup job for: {$scheduled_backup->name} (ID: {$scheduled_backup->id})"); + $this->info("✓ Dispatched backup job for: {$scheduled_backup->name} (ID: {$scheduled_backup->id}, Frequency: {$scheduled_backup->frequency})"); } } catch (\Exception $e) { $this->error("✗ Failed to dispatch backup job for {$scheduled_backup->id}: ".$e->getMessage()); @@ -123,14 +139,28 @@ private function runScheduledBackups(int $chunkSize, int $delay, ?int $maxJobs = } } - private function runScheduledTasks(int $chunkSize, int $delay, ?int $maxJobs = null, bool $dryRun = false): void + private function runScheduledTasks(int $chunkSize, int $delay, ?int $maxJobs = null, bool $dryRun = false, ?string $frequency = null): void { $this->info('Processing scheduled tasks...'); - $scheduled_tasks = ScheduledTask::where('enabled', true)->get(); + $query = ScheduledTask::where('enabled', true); + + if ($frequency) { + $query->where(function ($q) use ($frequency) { + // Handle human-readable frequency strings + if (in_array($frequency, ['daily', 'hourly', 'weekly', 'monthly', 'yearly', 'every_minute'])) { + $q->where('frequency', $frequency); + } else { + // Handle cron expressions + $q->where('frequency', $frequency); + } + }); + } + + $scheduled_tasks = $query->get(); if ($scheduled_tasks->isEmpty()) { - $this->info('No enabled scheduled tasks found.'); + $this->info('No enabled scheduled tasks found'.($frequency ? " with frequency '{$frequency}'" : '').'.'); return; } @@ -188,7 +218,7 @@ private function runScheduledTasks(int $chunkSize, int $delay, ?int $maxJobs = n $this->info("Limited to {$maxJobs} scheduled tasks for testing"); } - $this->info("Found {$finalScheduledTasks->count()} valid scheduled tasks to process"); + $this->info("Found {$finalScheduledTasks->count()} valid scheduled tasks to process".($frequency ? " with frequency '{$frequency}'" : '')); $chunks = $finalScheduledTasks->chunk($chunkSize); foreach ($chunks as $index => $chunk) { @@ -197,10 +227,10 @@ private function runScheduledTasks(int $chunkSize, int $delay, ?int $maxJobs = n foreach ($chunk as $scheduled_task) { try { if ($dryRun) { - $this->info("🔍 Would dispatch task job for: {$scheduled_task->name} (ID: {$scheduled_task->id})"); + $this->info("🔍 Would dispatch task job for: {$scheduled_task->name} (ID: {$scheduled_task->id}, Frequency: {$scheduled_task->frequency})"); } else { ScheduledTaskJob::dispatch($scheduled_task); - $this->info("✓ Dispatched task job for: {$scheduled_task->name} (ID: {$scheduled_task->id})"); + $this->info("✓ Dispatched task job for: {$scheduled_task->name} (ID: {$scheduled_task->id}, Frequency: {$scheduled_task->frequency})"); } } catch (\Exception $e) { $this->error("✗ Failed to dispatch task job for {$scheduled_task->id}: ".$e->getMessage()); From 36fe235bea66e64b93d97e69e586340ae293d885 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:10:55 +0200 Subject: [PATCH 0041/1717] feat(logging): implement scheduled logs command and enhance backup/task scheduling with cron checks --- app/Console/Commands/ViewScheduledLogs.php | 203 +++++++++++++++++++++ app/Console/Kernel.php | 50 +++++ config/logging.php | 14 ++ 3 files changed, 267 insertions(+) create mode 100644 app/Console/Commands/ViewScheduledLogs.php diff --git a/app/Console/Commands/ViewScheduledLogs.php b/app/Console/Commands/ViewScheduledLogs.php new file mode 100644 index 000000000..157afc7c1 --- /dev/null +++ b/app/Console/Commands/ViewScheduledLogs.php @@ -0,0 +1,203 @@ +option('date') ?: now()->format('Y-m-d'); + $logPaths = $this->getLogPaths($date); + + if (empty($logPaths)) { + $this->showAvailableLogFiles($date); + + return; + } + + $lines = $this->option('lines'); + $follow = $this->option('follow'); + + // Build grep filters + $filters = $this->buildFilters(); + $filterDescription = $this->getFilterDescription(); + $logTypeDescription = $this->getLogTypeDescription(); + + if ($follow) { + $this->info("Following {$logTypeDescription} logs for {$date}{$filterDescription} (Press Ctrl+C to stop)..."); + $this->line(''); + + if (count($logPaths) === 1) { + $logPath = $logPaths[0]; + if ($filters) { + passthru("tail -f {$logPath} | grep -E '{$filters}'"); + } else { + passthru("tail -f {$logPath}"); + } + } else { + // Multiple files - use multitail or tail with process substitution + $logPathsStr = implode(' ', $logPaths); + if ($filters) { + passthru("tail -f {$logPathsStr} | grep -E '{$filters}'"); + } else { + passthru("tail -f {$logPathsStr}"); + } + } + } else { + $this->info("Showing last {$lines} lines of {$logTypeDescription} logs for {$date}{$filterDescription}:"); + $this->line(''); + + if (count($logPaths) === 1) { + $logPath = $logPaths[0]; + if ($filters) { + passthru("tail -n {$lines} {$logPath} | grep -E '{$filters}'"); + } else { + passthru("tail -n {$lines} {$logPath}"); + } + } else { + // Multiple files - concatenate and sort by timestamp + $logPathsStr = implode(' ', $logPaths); + if ($filters) { + passthru("tail -n {$lines} {$logPathsStr} | sort | grep -E '{$filters}'"); + } else { + passthru("tail -n {$lines} {$logPathsStr} | sort"); + } + } + } + } + + private function getLogPaths(string $date): array + { + $paths = []; + + if ($this->option('errors')) { + // Error logs only + $errorPath = storage_path("logs/scheduled-errors-{$date}.log"); + if (File::exists($errorPath)) { + $paths[] = $errorPath; + } + } elseif ($this->option('all')) { + // Both normal and error logs + $normalPath = storage_path("logs/scheduled-{$date}.log"); + $errorPath = storage_path("logs/scheduled-errors-{$date}.log"); + + if (File::exists($normalPath)) { + $paths[] = $normalPath; + } + if (File::exists($errorPath)) { + $paths[] = $errorPath; + } + } else { + // Normal logs only (default) + $normalPath = storage_path("logs/scheduled-{$date}.log"); + if (File::exists($normalPath)) { + $paths[] = $normalPath; + } + } + + return $paths; + } + + private function showAvailableLogFiles(string $date): void + { + $logType = $this->getLogTypeDescription(); + $this->warn("No {$logType} logs found for date {$date}"); + + // Show available log files + $normalFiles = File::glob(storage_path('logs/scheduled-*.log')); + $errorFiles = File::glob(storage_path('logs/scheduled-errors-*.log')); + + if (! empty($normalFiles) || ! empty($errorFiles)) { + $this->info('Available scheduled log files:'); + + if (! empty($normalFiles)) { + $this->line(' Normal logs:'); + foreach ($normalFiles as $file) { + $basename = basename($file); + $this->line(" - {$basename}"); + } + } + + if (! empty($errorFiles)) { + $this->line(' Error logs:'); + foreach ($errorFiles as $file) { + $basename = basename($file); + $this->line(" - {$basename}"); + } + } + } + } + + private function getLogTypeDescription(): string + { + if ($this->option('errors')) { + return 'error'; + } elseif ($this->option('all')) { + return 'all'; + } else { + return 'normal'; + } + } + + private function buildFilters(): ?string + { + $filters = []; + + if ($taskName = $this->option('task-name')) { + $filters[] = '"task_name":"[^"]*'.preg_quote($taskName, '/').'[^"]*"'; + } + + if ($taskId = $this->option('task-id')) { + $filters[] = '"task_id":'.preg_quote($taskId, '/'); + } + + if ($backupName = $this->option('backup-name')) { + $filters[] = '"backup_name":"[^"]*'.preg_quote($backupName, '/').'[^"]*"'; + } + + if ($backupId = $this->option('backup-id')) { + $filters[] = '"backup_id":'.preg_quote($backupId, '/'); + } + + return empty($filters) ? null : implode('|', $filters); + } + + private function getFilterDescription(): string + { + $descriptions = []; + + if ($taskName = $this->option('task-name')) { + $descriptions[] = "task name: {$taskName}"; + } + + if ($taskId = $this->option('task-id')) { + $descriptions[] = "task ID: {$taskId}"; + } + + if ($backupName = $this->option('backup-name')) { + $descriptions[] = "backup name: {$backupName}"; + } + + if ($backupId = $this->option('backup-id')) { + $descriptions[] = "backup ID: {$backupId}"; + } + + return empty($descriptions) ? '' : ' (filtered by '.implode(', ', $descriptions).')'; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 395c58dee..190e46961 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -20,6 +20,7 @@ use App\Models\ScheduledTask; use App\Models\Server; use App\Models\Team; +use Cron\CronExpression; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Support\Carbon; @@ -197,6 +198,7 @@ private function checkResources(): void private function checkScheduledBackups(): void { $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get(); + if ($scheduled_backups->isEmpty()) { return; } @@ -238,10 +240,33 @@ private function checkScheduledBackups(): void $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; } $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); + + // Check if the backup should run now + $cron = new CronExpression($scheduled_backup->frequency); + $now = Carbon::now($serverTimezone); + + if ($cron->isDue($now)) { + Log::channel('scheduled')->info('Backup job running now', [ + 'backup_id' => $scheduled_backup->id, + 'backup_name' => $scheduled_backup->name ?? 'unnamed', + 'server_name' => $server->name, + 'frequency' => $scheduled_backup->frequency, + 'timezone' => $serverTimezone, + 'current_time' => $now->format('Y-m-d H:i:s T'), + ]); + } + $this->scheduleInstance->job(new DatabaseBackupJob( backup: $scheduled_backup ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer(); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Error scheduling backup', [ + 'backup_id' => $scheduled_backup->id, + 'backup_name' => $scheduled_backup->name ?? 'unnamed', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); Log::error('Error scheduling backup: '.$e->getMessage()); Log::error($e->getTraceAsString()); } @@ -251,6 +276,7 @@ private function checkScheduledBackups(): void private function checkScheduledTasks(): void { $scheduled_tasks = ScheduledTask::where('enabled', true)->get(); + if ($scheduled_tasks->isEmpty()) { return; } @@ -301,10 +327,34 @@ private function checkScheduledTasks(): void if (validate_timezone($serverTimezone) === false) { $serverTimezone = config('app.timezone'); } + + // Check if the task should run now + $cron = new CronExpression($scheduled_task->frequency); + $now = Carbon::now($serverTimezone); + + if ($cron->isDue($now)) { + Log::channel('scheduled')->info('Task job running now', [ + 'task_id' => $scheduled_task->id, + 'task_name' => $scheduled_task->name ?? 'unnamed', + 'server_name' => $server->name, + 'frequency' => $scheduled_task->frequency, + 'timezone' => $serverTimezone, + 'type' => $scheduled_task->service ? 'service' : 'application', + 'current_time' => $now->format('Y-m-d H:i:s T'), + ]); + } + $this->scheduleInstance->job(new ScheduledTaskJob( task: $scheduled_task ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer(); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Error scheduling task', [ + 'task_id' => $scheduled_task->id, + 'task_name' => $scheduled_task->name ?? 'unnamed', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); Log::error('Error scheduling task: '.$e->getMessage()); Log::error($e->getTraceAsString()); } diff --git a/config/logging.php b/config/logging.php index 4c3df4ce1..a804295fa 100644 --- a/config/logging.php +++ b/config/logging.php @@ -118,6 +118,20 @@ 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], + + 'scheduled' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/scheduled.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => 1, // Keep logs for 1 day only (truncated daily) + ], + + 'scheduled-errors' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/scheduled-errors.log'), + 'level' => 'error', + 'days' => 7, // Keep error logs for 7 days + ], ], ]; From e6720113126f291077fdb7b46f13a0fda653e6e0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:11:00 +0200 Subject: [PATCH 0042/1717] fix(versions): update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.6 and 4.0.0-beta.420.7 --- config/constants.php | 2 +- versions.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/constants.php b/config/constants.php index dde69362b..ae984aa18 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.420.5', + 'version' => '4.0.0-beta.420.6', 'helper_version' => '1.0.8', 'realtime_version' => '1.0.9', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/versions.json b/versions.json index 8a962b51d..7b12bd23e 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.420.5" + "version": "4.0.0-beta.420.6" }, "nightly": { - "version": "4.0.0-beta.420.6" + "version": "4.0.0-beta.420.7" }, "helper": { "version": "1.0.8" From 316f233eb8b550d4e59e3fed68857f946077b3da Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:26:42 +0200 Subject: [PATCH 0043/1717] feat(logging): add frequency filters for scheduled logs command to support hourly, daily, weekly, and monthly job views --- app/Console/Commands/ViewScheduledLogs.php | 77 +++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/app/Console/Commands/ViewScheduledLogs.php b/app/Console/Commands/ViewScheduledLogs.php index 157afc7c1..9ecf90716 100644 --- a/app/Console/Commands/ViewScheduledLogs.php +++ b/app/Console/Commands/ViewScheduledLogs.php @@ -16,7 +16,12 @@ class ViewScheduledLogs extends Command {--backup-name= : Filter by backup name (partial match)} {--backup-id= : Filter by backup ID} {--errors : View error logs only} - {--all : View both normal and error logs}'; + {--all : View both normal and error logs} + {--hourly : Filter hourly jobs} + {--daily : Filter daily jobs} + {--weekly : Filter weekly jobs} + {--monthly : Filter monthly jobs} + {--frequency= : Filter by specific cron expression}'; protected $description = 'View scheduled backups and tasks logs with optional filtering'; @@ -175,9 +180,58 @@ private function buildFilters(): ?string $filters[] = '"backup_id":'.preg_quote($backupId, '/'); } + // Frequency filters + if ($this->option('hourly')) { + $filters[] = $this->getFrequencyPattern('hourly'); + } + + if ($this->option('daily')) { + $filters[] = $this->getFrequencyPattern('daily'); + } + + if ($this->option('weekly')) { + $filters[] = $this->getFrequencyPattern('weekly'); + } + + if ($this->option('monthly')) { + $filters[] = $this->getFrequencyPattern('monthly'); + } + + if ($frequency = $this->option('frequency')) { + $filters[] = '"frequency":"'.preg_quote($frequency, '/').'"'; + } + return empty($filters) ? null : implode('|', $filters); } + private function getFrequencyPattern(string $type): string + { + $patterns = [ + 'hourly' => [ + '0 \* \* \* \*', // 0 * * * * + '@hourly', // @hourly + ], + 'daily' => [ + '0 0 \* \* \*', // 0 0 * * * + '@daily', // @daily + '@midnight', // @midnight + ], + 'weekly' => [ + '0 0 \* \* [0-6]', // 0 0 * * 0-6 (any day of week) + '@weekly', // @weekly + ], + 'monthly' => [ + '0 0 1 \* \*', // 0 0 1 * * (first of month) + '@monthly', // @monthly + ], + ]; + + $typePatterns = $patterns[$type] ?? []; + + // For grep, we need to match the frequency field in JSON + return '"frequency":"('.implode('|', $typePatterns).')"'; + } + private function getFilterDescription(): string { $descriptions = []; @@ -198,6 +252,27 @@ private function getFilterDescription(): string $descriptions[] = "backup ID: {$backupId}"; } + // Frequency filters + if ($this->option('hourly')) { + $descriptions[] = 'hourly jobs'; + } + + if ($this->option('daily')) { + $descriptions[] = 'daily jobs'; + } + + if ($this->option('weekly')) { + $descriptions[] = 'weekly jobs'; + } + + if ($this->option('monthly')) { + $descriptions[] = 'monthly jobs'; + } + + if ($frequency = $this->option('frequency')) { + $descriptions[] = "frequency: {$frequency}"; + } + return empty($descriptions) ? '' : ' (filtered by '.implode(', ', $descriptions).')'; } } From 18f2e5ea32e62b3348d06071e795fecd83a04b40 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:26:19 +0200 Subject: [PATCH 0044/1717] fix(scheduling): remove unnecessary padding from scheduled task form layout for improved UI consistency --- .../views/livewire/project/shared/scheduled-task/show.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php index c32659ad8..1ede7775a 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -11,7 +11,7 @@

-
+

Scheduled Task

Save From b08c6b5b333da390b77c5924cb752153ef6a8c33 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 12 Jul 2025 14:04:01 +0200 Subject: [PATCH 0045/1717] fix(horizon): update queue configuration to use environment variable for dynamic queue management --- config/horizon.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/horizon.php b/config/horizon.php index 6086b30da..9dd80b8e4 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -182,7 +182,7 @@ 'defaults' => [ 's6' => [ 'connection' => 'redis', - 'queue' => ['high', 'default'], + 'queue' => explode(',', env('HORIZON_QUEUES', 'high,default')), 'balance' => env('HORIZON_BALANCE', 'auto'), 'maxTime' => 0, 'maxJobs' => 0, From 0f5c98865868ba797dd008c86ea34d89a9a2b3ce Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 12 Jul 2025 14:44:32 +0200 Subject: [PATCH 0046/1717] fix(horizon): add silenced jobs --- app/Events/BackupCreated.php | 3 ++- app/Events/ServiceChecked.php | 3 ++- app/Jobs/PushServerUpdateJob.php | 3 ++- app/Jobs/ServerStorageCheckJob.php | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/Events/BackupCreated.php b/app/Events/BackupCreated.php index bc1ecee0d..9670f5c3c 100644 --- a/app/Events/BackupCreated.php +++ b/app/Events/BackupCreated.php @@ -7,8 +7,9 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Laravel\Horizon\Contracts\Silenced; -class BackupCreated implements ShouldBroadcast +class BackupCreated implements ShouldBroadcast, Silenced { use Dispatchable, InteractsWithSockets, SerializesModels; diff --git a/app/Events/ServiceChecked.php b/app/Events/ServiceChecked.php index 3f130a0fb..86a27a892 100644 --- a/app/Events/ServiceChecked.php +++ b/app/Events/ServiceChecked.php @@ -7,8 +7,9 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Laravel\Horizon\Contracts\Silenced; -class ServiceChecked implements ShouldBroadcast +class ServiceChecked implements ShouldBroadcast, Silenced { use Dispatchable, InteractsWithSockets, SerializesModels; diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index 61206da6f..3e3aa1eb7 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -21,8 +21,9 @@ use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; +use Laravel\Horizon\Contracts\Silenced; -class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue +class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php index 9a8d86be1..9d45491c6 100644 --- a/app/Jobs/ServerStorageCheckJob.php +++ b/app/Jobs/ServerStorageCheckJob.php @@ -11,8 +11,9 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\RateLimiter; +use Laravel\Horizon\Contracts\Silenced; -class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue +class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue, Silenced { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; From 80fae306e625d1274119b5ae7905c3a097257a41 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 12 Jul 2025 14:44:44 +0200 Subject: [PATCH 0047/1717] feat(scheduling): introduce ScheduledJobManager and ServerResourceManager for enhanced job scheduling and resource management --- app/Jobs/ScheduledJobManager.php | 255 +++++++++++++++++++++++++++++ app/Jobs/ServerResourceManager.php | 168 +++++++++++++++++++ config/logging.php | 8 +- 3 files changed, 427 insertions(+), 4 deletions(-) create mode 100644 app/Jobs/ScheduledJobManager.php create mode 100644 app/Jobs/ServerResourceManager.php diff --git a/app/Jobs/ScheduledJobManager.php b/app/Jobs/ScheduledJobManager.php new file mode 100644 index 000000000..572491c0d --- /dev/null +++ b/app/Jobs/ScheduledJobManager.php @@ -0,0 +1,255 @@ +onQueue($this->determineQueue()); + } + + private function determineQueue(): string + { + $preferredQueue = 'crons'; + $fallbackQueue = 'high'; + + $configuredQueues = explode(',', env('HORIZON_QUEUES', 'high,default')); + + return in_array($preferredQueue, $configuredQueues) ? $preferredQueue : $fallbackQueue; + } + + /** + * Get the middleware the job should pass through. + */ + public function middleware(): array + { + return [ + (new WithoutOverlapping('scheduled-job-manager')) + ->releaseAfter(60), // Release the lock after 60 seconds if job fails + ]; + } + + public function handle(): void + { + // Freeze the execution time at the start of the job + $this->executionTime = Carbon::now(); + + Log::channel('scheduled')->info('ScheduledJobManager started', [ + 'execution_time' => $this->executionTime->format('Y-m-d H:i:s T'), + ]); + + // Process backups - don't let failures stop task processing + try { + $this->processScheduledBackups(); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Failed to process scheduled backups', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } + + // Process tasks - don't let failures stop the job manager + try { + $this->processScheduledTasks(); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Failed to process scheduled tasks', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } + + Log::channel('scheduled')->info('ScheduledJobManager completed'); + } + + private function processScheduledBackups(): void + { + $backups = ScheduledDatabaseBackup::with(['database']) + ->where('enabled', true) + ->get(); + + foreach ($backups as $backup) { + try { + // Apply the same filtering logic as the original + if (! $this->shouldProcessBackup($backup)) { + continue; + } + + $server = $backup->server(); + $serverTimezone = data_get($server->settings, 'server_timezone', config('app.timezone')); + + if (validate_timezone($serverTimezone) === false) { + $serverTimezone = config('app.timezone'); + } + + $frequency = $backup->frequency; + if (isset(VALID_CRON_STRINGS[$frequency])) { + $frequency = VALID_CRON_STRINGS[$frequency]; + } + + if ($this->shouldRunNow($frequency, $serverTimezone)) { + Log::channel('scheduled')->info('Dispatching backup job', [ + 'backup_id' => $backup->id, + 'backup_name' => $backup->name ?? 'unnamed', + 'server_name' => $server->name, + 'frequency' => $frequency, + 'timezone' => $serverTimezone, + 'execution_time' => $this->executionTime?->format('Y-m-d H:i:s T'), + 'current_time' => Carbon::now()->format('Y-m-d H:i:s T'), + ]); + + DatabaseBackupJob::dispatch($backup); + } + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Error processing backup', [ + 'backup_id' => $backup->id, + 'error' => $e->getMessage(), + ]); + } + } + } + + private function processScheduledTasks(): void + { + $tasks = ScheduledTask::with(['service', 'application']) + ->where('enabled', true) + ->get(); + + foreach ($tasks as $task) { + try { + if (! $this->shouldProcessTask($task)) { + continue; + } + + $server = $task->server(); + $serverTimezone = data_get($server->settings, 'server_timezone', config('app.timezone')); + + if (validate_timezone($serverTimezone) === false) { + $serverTimezone = config('app.timezone'); + } + + $frequency = $task->frequency; + if (isset(VALID_CRON_STRINGS[$frequency])) { + $frequency = VALID_CRON_STRINGS[$frequency]; + } + + if ($this->shouldRunNow($frequency, $serverTimezone)) { + Log::channel('scheduled')->info('Dispatching task job', [ + 'task_id' => $task->id, + 'task_name' => $task->name ?? 'unnamed', + 'server_name' => $server->name, + 'frequency' => $frequency, + 'timezone' => $serverTimezone, + 'execution_time' => $this->executionTime?->format('Y-m-d H:i:s T'), + 'current_time' => Carbon::now()->format('Y-m-d H:i:s T'), + ]); + + ScheduledTaskJob::dispatch($task); + } + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Error processing task', [ + 'task_id' => $task->id, + 'error' => $e->getMessage(), + ]); + } + } + } + + private function shouldProcessBackup(ScheduledDatabaseBackup $backup): bool + { + if (blank(data_get($backup, 'database'))) { + $backup->delete(); + + return false; + } + + $server = $backup->server(); + if (blank($server)) { + $backup->delete(); + + return false; + } + + if ($server->isFunctional() === false) { + return false; + } + + if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) { + return false; + } + + return true; + } + + private function shouldProcessTask(ScheduledTask $task): bool + { + $service = $task->service; + $application = $task->application; + + $server = $task->server(); + if (blank($server)) { + $task->delete(); + + return false; + } + + if ($server->isFunctional() === false) { + return false; + } + + if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) { + return false; + } + + if (! $service && ! $application) { + $task->delete(); + + return false; + } + + if ($application && str($application->status)->contains('running') === false) { + return false; + } + + if ($service && str($service->status)->contains('running') === false) { + return false; + } + + return true; + } + + private function shouldRunNow(string $frequency, string $timezone): bool + { + $cron = new CronExpression($frequency); + + // Use the frozen execution time, not the current time + // Fallback to current time if execution time is not set (shouldn't happen) + $baseTime = $this->executionTime ?? Carbon::now(); + $executionTime = $baseTime->copy()->setTimezone($timezone); + + return $cron->isDue($executionTime); + } +} diff --git a/app/Jobs/ServerResourceManager.php b/app/Jobs/ServerResourceManager.php new file mode 100644 index 000000000..9059bc6cb --- /dev/null +++ b/app/Jobs/ServerResourceManager.php @@ -0,0 +1,168 @@ +onQueue('high'); + } + + /** + * Get the middleware the job should pass through. + */ + public function middleware(): array + { + return [ + (new WithoutOverlapping('server-resource-manager')) + ->releaseAfter(60), + ]; + } + + public function handle(): void + { + // Freeze the execution time at the start of the job + $this->executionTime = Carbon::now(); + + $this->settings = instanceSettings(); + $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone'); + + if (validate_timezone($this->instanceTimezone) === false) { + $this->instanceTimezone = config('app.timezone'); + } + + Log::channel('scheduled')->info('ServerResourceManager started', [ + 'execution_time' => $this->executionTime->format('Y-m-d H:i:s T'), + ]); + + // Process server checks - don't let failures stop the job + try { + $this->processServerChecks(); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Failed to process server checks', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } + + Log::channel('scheduled')->info('ServerResourceManager completed'); + } + + private function processServerChecks(): void + { + $servers = $this->getServers(); + + foreach ($servers as $server) { + try { + $this->processServer($server); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Error processing server', [ + 'server_id' => $server->id, + 'server_name' => $server->name, + 'error' => $e->getMessage(), + ]); + } + } + } + + private function getServers() + { + $allServers = Server::where('ip', '!=', '1.2.3.4'); + + if (isCloud()) { + $servers = $allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get(); + $own = Team::find(0)->servers; + + return $servers->merge($own); + } else { + return $allServers->get(); + } + } + + private function processServer(Server $server): void + { + $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); + if (validate_timezone($serverTimezone) === false) { + $serverTimezone = config('app.timezone'); + } + + // Sentinel check + $lastSentinelUpdate = $server->sentinel_updated_at; + if (Carbon::parse($lastSentinelUpdate)->isBefore($this->executionTime->subSeconds($server->waitBeforeDoingSshCheck()))) { + // Dispatch ServerCheckJob if due + $checkFrequency = isCloud() ? '*/5 * * * *' : '* * * * *'; // Every 5 min for cloud, every minute for self-hosted + if ($this->shouldRunNow($checkFrequency, $serverTimezone)) { + ServerCheckJob::dispatch($server); + } + + // Dispatch ServerStorageCheckJob if due + $serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *'); + if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) { + $serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency]; + } + if ($this->shouldRunNow($serverDiskUsageCheckFrequency, $serverTimezone)) { + ServerStorageCheckJob::dispatch($server); + } + } + + // Dispatch DockerCleanupJob if due + $dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *'); + if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) { + $dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency]; + } + if ($this->shouldRunNow($dockerCleanupFrequency, $serverTimezone)) { + DockerCleanupJob::dispatch($server); + } + + // Dispatch ServerPatchCheckJob if due (weekly) + if ($this->shouldRunNow('0 0 * * 0', $serverTimezone)) { // Weekly on Sunday at midnight + ServerPatchCheckJob::dispatch($server); + } + + // Dispatch Sentinel restart if due (daily for Sentinel-enabled servers) + if ($server->isSentinelEnabled() && $this->shouldRunNow('0 0 * * *', $serverTimezone)) { + dispatch(function () use ($server) { + $server->restartContainer('coolify-sentinel'); + }); + } + } + + private function shouldRunNow(string $frequency, string $timezone): bool + { + $cron = new CronExpression($frequency); + + // Use the frozen execution time, not the current time + $baseTime = $this->executionTime ?? Carbon::now(); + $executionTime = $baseTime->copy()->setTimezone($timezone); + + return $cron->isDue($executionTime); + } +} diff --git a/config/logging.php b/config/logging.php index a804295fa..488327414 100644 --- a/config/logging.php +++ b/config/logging.php @@ -122,15 +122,15 @@ 'scheduled' => [ 'driver' => 'daily', 'path' => storage_path('logs/scheduled.log'), - 'level' => env('LOG_LEVEL', 'debug'), - 'days' => 1, // Keep logs for 1 day only (truncated daily) + 'level' => 'debug', + 'days' => 1, ], 'scheduled-errors' => [ 'driver' => 'daily', 'path' => storage_path('logs/scheduled-errors.log'), - 'level' => 'error', - 'days' => 7, // Keep error logs for 7 days + 'level' => 'debug', + 'days' => 7, ], ], From fbe98cfd1189df3ec91710fa04de4db190d2ef60 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 12 Jul 2025 14:44:52 +0200 Subject: [PATCH 0048/1717] refactor(scheduling): replace deprecated job checks with ScheduledJobManager and ServerResourceManager for improved scheduling efficiency --- app/Console/Kernel.php | 249 ++--------------------------------------- 1 file changed, 8 insertions(+), 241 deletions(-) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 190e46961..eda2fca74 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -6,24 +6,16 @@ use App\Jobs\CheckForUpdatesJob; use App\Jobs\CheckHelperImageJob; use App\Jobs\CleanupInstanceStuffsJob; -use App\Jobs\DatabaseBackupJob; -use App\Jobs\DockerCleanupJob; use App\Jobs\PullTemplatesFromCDN; use App\Jobs\RegenerateSslCertJob; -use App\Jobs\ScheduledTaskJob; -use App\Jobs\ServerCheckJob; -use App\Jobs\ServerPatchCheckJob; -use App\Jobs\ServerStorageCheckJob; +use App\Jobs\ScheduledJobManager; +use App\Jobs\ServerResourceManager; use App\Jobs\UpdateCoolifyJob; use App\Models\InstanceSettings; -use App\Models\ScheduledDatabaseBackup; -use App\Models\ScheduledTask; use App\Models\Server; use App\Models\Team; -use Cron\CronExpression; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; -use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Log; class Kernel extends ConsoleKernel @@ -62,10 +54,10 @@ protected function schedule(Schedule $schedule): void $this->scheduleInstance->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer(); // Server Jobs - $this->checkResources(); + $this->scheduleInstance->job(new ServerResourceManager)->everyMinute()->onOneServer(); - $this->checkScheduledBackups(); - $this->checkScheduledTasks(); + // Scheduled Jobs (Backups & Tasks) + $this->scheduleInstance->job(new ScheduledJobManager)->everyMinute()->onOneServer(); $this->scheduleInstance->command('uploads:clear')->everyTwoMinutes(); @@ -80,12 +72,12 @@ protected function schedule(Schedule $schedule): void $this->scheduleUpdates(); // Server Jobs - $this->checkResources(); + $this->scheduleInstance->job(new ServerResourceManager)->everyMinute()->onOneServer(); $this->pullImages(); - $this->checkScheduledBackups(); - $this->checkScheduledTasks(); + // Scheduled Jobs (Backups & Tasks) + $this->scheduleInstance->job(new ScheduledJobManager)->everyMinute()->onOneServer(); $this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily(); @@ -136,231 +128,6 @@ private function scheduleUpdates(): void } } - private function checkResources(): void - { - if (isCloud()) { - $servers = $this->allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get(); - $own = Team::find(0)->servers; - $servers = $servers->merge($own); - } else { - $servers = $this->allServers->get(); - } - - foreach ($servers as $server) { - try { - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - if (validate_timezone($serverTimezone) === false) { - $serverTimezone = config('app.timezone'); - } - - // Sentinel check - $lastSentinelUpdate = $server->sentinel_updated_at; - if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { - // Check container status every minute if Sentinel does not activated - if (isCloud()) { - $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer(); - } else { - $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer(); - } - // $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer(); - - $serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *'); - if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) { - $serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency]; - } - $this->scheduleInstance->job(new ServerStorageCheckJob($server))->cron($serverDiskUsageCheckFrequency)->timezone($serverTimezone)->onOneServer(); - } - - $dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *'); - if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) { - $dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency]; - } - $this->scheduleInstance->job(new DockerCleanupJob($server))->cron($dockerCleanupFrequency)->timezone($serverTimezone)->onOneServer(); - - // Server patch check - weekly - $this->scheduleInstance->job(new ServerPatchCheckJob($server))->weekly()->timezone($serverTimezone)->onOneServer(); - - // Cleanup multiplexed connections every hour - // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer(); - - // Temporary solution until we have better memory management for Sentinel - if ($server->isSentinelEnabled()) { - $this->scheduleInstance->job(function () use ($server) { - $server->restartContainer('coolify-sentinel'); - })->daily()->onOneServer(); - } - } catch (\Exception $e) { - Log::error('Error checking resources: '.$e->getMessage()); - } - } - } - - private function checkScheduledBackups(): void - { - $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get(); - - if ($scheduled_backups->isEmpty()) { - return; - } - $finalScheduledBackups = collect(); - foreach ($scheduled_backups as $scheduled_backup) { - if (blank(data_get($scheduled_backup, 'database'))) { - $scheduled_backup->delete(); - - continue; - } - $server = $scheduled_backup->server(); - if (blank($server)) { - $scheduled_backup->delete(); - - continue; - } - if ($server->isFunctional() === false) { - continue; - } - if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) { - continue; - } - $finalScheduledBackups->push($scheduled_backup); - } - - foreach ($finalScheduledBackups as $scheduled_backup) { - try { - if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { - $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; - } - $server = $scheduled_backup->server(); - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - - if (validate_timezone($serverTimezone) === false) { - $serverTimezone = config('app.timezone'); - } - - if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { - $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; - } - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - - // Check if the backup should run now - $cron = new CronExpression($scheduled_backup->frequency); - $now = Carbon::now($serverTimezone); - - if ($cron->isDue($now)) { - Log::channel('scheduled')->info('Backup job running now', [ - 'backup_id' => $scheduled_backup->id, - 'backup_name' => $scheduled_backup->name ?? 'unnamed', - 'server_name' => $server->name, - 'frequency' => $scheduled_backup->frequency, - 'timezone' => $serverTimezone, - 'current_time' => $now->format('Y-m-d H:i:s T'), - ]); - } - - $this->scheduleInstance->job(new DatabaseBackupJob( - backup: $scheduled_backup - ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer(); - - } catch (\Exception $e) { - Log::channel('scheduled-errors')->error('Error scheduling backup', [ - 'backup_id' => $scheduled_backup->id, - 'backup_name' => $scheduled_backup->name ?? 'unnamed', - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - Log::error('Error scheduling backup: '.$e->getMessage()); - Log::error($e->getTraceAsString()); - } - } - } - - private function checkScheduledTasks(): void - { - $scheduled_tasks = ScheduledTask::where('enabled', true)->get(); - - if ($scheduled_tasks->isEmpty()) { - return; - } - $finalScheduledTasks = collect(); - foreach ($scheduled_tasks as $scheduled_task) { - $service = $scheduled_task->service; - $application = $scheduled_task->application; - - $server = $scheduled_task->server(); - if (blank($server)) { - $scheduled_task->delete(); - - continue; - } - - if ($server->isFunctional() === false) { - continue; - } - - if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) { - continue; - } - - if (! $service && ! $application) { - $scheduled_task->delete(); - - continue; - } - - if ($application && str($application->status)->contains('running') === false) { - continue; - } - if ($service && str($service->status)->contains('running') === false) { - continue; - } - - $finalScheduledTasks->push($scheduled_task); - } - - foreach ($finalScheduledTasks as $scheduled_task) { - try { - $server = $scheduled_task->server(); - if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { - $scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency]; - } - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - - if (validate_timezone($serverTimezone) === false) { - $serverTimezone = config('app.timezone'); - } - - // Check if the task should run now - $cron = new CronExpression($scheduled_task->frequency); - $now = Carbon::now($serverTimezone); - - if ($cron->isDue($now)) { - Log::channel('scheduled')->info('Task job running now', [ - 'task_id' => $scheduled_task->id, - 'task_name' => $scheduled_task->name ?? 'unnamed', - 'server_name' => $server->name, - 'frequency' => $scheduled_task->frequency, - 'timezone' => $serverTimezone, - 'type' => $scheduled_task->service ? 'service' : 'application', - 'current_time' => $now->format('Y-m-d H:i:s T'), - ]); - } - - $this->scheduleInstance->job(new ScheduledTaskJob( - task: $scheduled_task - ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer(); - - } catch (\Exception $e) { - Log::channel('scheduled-errors')->error('Error scheduling task', [ - 'task_id' => $scheduled_task->id, - 'task_name' => $scheduled_task->name ?? 'unnamed', - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - Log::error('Error scheduling task: '.$e->getMessage()); - Log::error($e->getTraceAsString()); - } - } - } - protected function commands(): void { $this->load(__DIR__.'/Commands'); From 0b847928717f8c19f2d084852757e0a5f5afe188 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 13 Jul 2025 12:36:53 +0200 Subject: [PATCH 0049/1717] feat(deployment): add pull request filtering and pagination to deployment and backup execution components fix(ui): make them more stylish yeah --- CLAUDE.md | 87 ++++++++ .../Project/Application/Deployment/Index.php | 105 +++++++--- .../Project/Database/BackupExecutions.php | 95 ++++++++- app/Models/Application.php | 7 +- app/Models/ScheduledDatabaseBackup.php | 12 ++ .../application/deployment/index.blade.php | 45 +++-- .../database/backup-executions.blade.php | 30 ++- .../database/scheduled-backups.blade.php | 185 ++++++++++++++++-- 8 files changed, 493 insertions(+), 73 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..a3bb31cee --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,87 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Coolify is an open-source, self-hostable platform for deploying applications and managing servers - an alternative to Heroku/Netlify/Vercel. It's built with Laravel (PHP) and uses Docker for containerization. + +## Development Commands + +### Frontend Development +- `npm run dev` - Start Vite development server for frontend assets +- `npm run build` - Build frontend assets for production + +### Backend Development +- `php artisan serve` - Start Laravel development server +- `php artisan migrate` - Run database migrations +- `php artisan queue:work` - Start queue worker for background jobs +- `php artisan horizon` - Start Laravel Horizon for queue monitoring +- `php artisan tinker` - Start interactive PHP REPL + +### Code Quality +- `./vendor/bin/pint` - Run Laravel Pint for code formatting +- `./vendor/bin/phpstan` - Run PHPStan for static analysis +- `./vendor/bin/pest` - Run Pest tests + +## Architecture Overview + +### Technology Stack +- **Backend**: Laravel 12 (PHP 8.4) +- **Frontend**: Livewire + Alpine.js + Tailwind CSS +- **Database**: PostgreSQL 15 +- **Cache/Queue**: Redis 7 +- **Real-time**: Soketi (WebSocket server) +- **Containerization**: Docker & Docker Compose + +### Key Components + +#### Core Models +- `Application` - Deployed applications with Git integration +- `Server` - Remote servers managed by Coolify +- `Service` - Docker Compose services +- `Database` - Standalone database instances (PostgreSQL, MySQL, MongoDB, Redis, etc.) +- `Team` - Multi-tenancy support +- `Project` - Grouping of environments and resources + +#### Job System +- Uses Laravel Horizon for queue management +- Key jobs: `ApplicationDeploymentJob`, `ServerCheckJob`, `DatabaseBackupJob` +- `ScheduledJobManager` and `ServerResourceManager` handle job scheduling + +#### Deployment Flow +1. Git webhook triggers deployment +2. `ApplicationDeploymentJob` handles build and deployment +3. Docker containers are managed on target servers +4. Proxy configuration (Nginx/Traefik) is updated + +#### Server Management +- SSH-based server communication via `ExecuteRemoteCommand` trait +- Docker installation and management +- Proxy configuration generation +- Resource monitoring and cleanup + +### Directory Structure +- `app/Actions/` - Domain-specific actions (Application, Database, Server, etc.) +- `app/Jobs/` - Background queue jobs +- `app/Livewire/` - Frontend components (full-stack with Livewire) +- `app/Models/` - Eloquent models +- `bootstrap/helpers/` - Helper functions for various domains +- `database/migrations/` - Database schema evolution + +## Development Guidelines + +### Code Organization +- Use Actions pattern for complex business logic +- Livewire components handle UI and user interactions +- Jobs handle asynchronous operations +- Traits provide shared functionality (e.g., `ExecuteRemoteCommand`) + +### Testing +- Uses Pest for testing framework +- Tests located in `tests/` directory + +### Deployment and Docker +- Applications are deployed using Docker containers +- Configuration generated dynamically based on application settings +- Supports multiple deployment targets and proxy configurations \ No newline at end of file diff --git a/app/Livewire/Project/Application/Deployment/Index.php b/app/Livewire/Project/Application/Deployment/Index.php index c957615ac..5b621cb95 100644 --- a/app/Livewire/Project/Application/Deployment/Index.php +++ b/app/Livewire/Project/Application/Deployment/Index.php @@ -18,11 +18,13 @@ class Index extends Component public int $skip = 0; - public int $default_take = 10; + public int $defaultTake = 10; - public bool $show_next = false; + public bool $showNext = false; - public bool $show_prev = false; + public bool $showPrev = false; + + public int $currentPage = 1; public ?string $pull_request_id = null; @@ -51,68 +53,111 @@ public function mount() if (! $application) { return redirect()->route('dashboard'); } - ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->default_take); + // Validate pull request ID from URL parameters + if ($this->pull_request_id !== null && $this->pull_request_id !== '') { + if (! is_numeric($this->pull_request_id) || (float) $this->pull_request_id <= 0 || (float) $this->pull_request_id != (int) $this->pull_request_id) { + $this->pull_request_id = null; + $this->dispatch('error', 'Invalid Pull Request ID in URL. Filter cleared.'); + } else { + // Ensure it's stored as a string representation of a positive integer + $this->pull_request_id = (string) (int) $this->pull_request_id; + } + } + + ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->defaultTake, $this->pull_request_id); $this->application = $application; $this->deployments = $deployments; $this->deployments_count = $count; $this->current_url = url()->current(); - $this->show_pull_request_only(); - $this->show_more(); + $this->updateCurrentPage(); + $this->showMore(); } - private function show_pull_request_only() - { - if ($this->pull_request_id) { - $this->deployments = $this->deployments->where('pull_request_id', $this->pull_request_id); - } - } - - private function show_more() + private function showMore() { if ($this->deployments->count() !== 0) { - $this->show_next = true; - if ($this->deployments->count() < $this->default_take) { - $this->show_next = false; + $this->showNext = true; + if ($this->deployments->count() < $this->defaultTake) { + $this->showNext = false; } return; } } - public function reload_deployments() + public function reloadDeployments() { - $this->load_deployments(); + $this->loadDeployments(); } - public function previous_page(?int $take = null) + public function previousPage(?int $take = null) { if ($take) { $this->skip = $this->skip - $take; } - $this->skip = $this->skip - $this->default_take; + $this->skip = $this->skip - $this->defaultTake; if ($this->skip < 0) { - $this->show_prev = false; + $this->showPrev = false; $this->skip = 0; } - $this->load_deployments(); + $this->updateCurrentPage(); + $this->loadDeployments(); } - public function next_page(?int $take = null) + public function nextPage(?int $take = null) { if ($take) { $this->skip = $this->skip + $take; } - $this->show_prev = true; - $this->load_deployments(); + $this->showPrev = true; + $this->updateCurrentPage(); + $this->loadDeployments(); } - public function load_deployments() + public function loadDeployments() { - ['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->default_take); + ['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->defaultTake, $this->pull_request_id); $this->deployments = $deployments; $this->deployments_count = $count; - $this->show_pull_request_only(); - $this->show_more(); + $this->showMore(); + } + + public function updatedPullRequestId($value) + { + // Sanitize and validate the pull request ID + if ($value !== null && $value !== '') { + // Check if it's numeric and positive + if (! is_numeric($value) || (float) $value <= 0 || (float) $value != (int) $value) { + $this->pull_request_id = null; + $this->dispatch('error', 'Invalid Pull Request ID. Please enter a valid positive number.'); + + return; + } + // Ensure it's stored as a string representation of a positive integer + $this->pull_request_id = (string) (int) $value; + } else { + $this->pull_request_id = null; + } + + // Reset pagination when filter changes + $this->skip = 0; + $this->showPrev = false; + $this->updateCurrentPage(); + $this->loadDeployments(); + } + + public function clearFilter() + { + $this->pull_request_id = null; + $this->skip = 0; + $this->showPrev = false; + $this->updateCurrentPage(); + $this->loadDeployments(); + } + + private function updateCurrentPage() + { + $this->currentPage = intval($this->skip / $this->defaultTake) + 1; } public function render() diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index f96ca9a6a..2f3aae8cf 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -4,6 +4,7 @@ use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Livewire\Component; @@ -14,7 +15,19 @@ class BackupExecutions extends Component public $database; - public $executions = []; + public ?Collection $executions; + + public int $executions_count = 0; + + public int $skip = 0; + + public int $defaultTake = 10; + + public bool $showNext = false; + + public bool $showPrev = false; + + public int $currentPage = 1; public $setDeletableBackup; @@ -40,6 +53,20 @@ public function cleanupFailed() } } + public function cleanupDeleted() + { + if ($this->backup) { + $deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count(); + if ($deletedCount > 0) { + $this->backup->executions()->where('local_storage_deleted', true)->delete(); + $this->refreshBackupExecutions(); + $this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage."); + } else { + $this->dispatch('info', 'No backup entries found that are deleted from local storage.'); + } + } + } + public function deleteBackup($executionId, $password) { if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { @@ -85,18 +112,74 @@ public function download_file($exeuctionId) public function refreshBackupExecutions(): void { - if ($this->backup && $this->backup->exists) { - $this->executions = $this->backup->executions()->get()->toArray(); - } else { - $this->executions = []; + $this->loadExecutions(); + } + + public function reloadExecutions() + { + $this->loadExecutions(); + } + + public function previousPage(?int $take = null) + { + if ($take) { + $this->skip = $this->skip - $take; } + $this->skip = $this->skip - $this->defaultTake; + if ($this->skip < 0) { + $this->showPrev = false; + $this->skip = 0; + } + $this->updateCurrentPage(); + $this->loadExecutions(); + } + + public function nextPage(?int $take = null) + { + if ($take) { + $this->skip = $this->skip + $take; + } + $this->showPrev = true; + $this->updateCurrentPage(); + $this->loadExecutions(); + } + + private function loadExecutions() + { + if ($this->backup && $this->backup->exists) { + ['executions' => $executions, 'count' => $count] = $this->backup->executionsPaginated($this->skip, $this->defaultTake); + $this->executions = $executions; + $this->executions_count = $count; + } else { + $this->executions = collect([]); + $this->executions_count = 0; + } + $this->showMore(); + } + + private function showMore() + { + if ($this->executions->count() !== 0) { + $this->showNext = true; + if ($this->executions->count() < $this->defaultTake) { + $this->showNext = false; + } + + return; + } + } + + private function updateCurrentPage() + { + $this->currentPage = intval($this->skip / $this->defaultTake) + 1; } public function mount(ScheduledDatabaseBackup $backup) { $this->backup = $backup; $this->database = $backup->database; - $this->refreshBackupExecutions(); + $this->updateCurrentPage(); + $this->loadExecutions(); } public function server() diff --git a/app/Models/Application.php b/app/Models/Application.php index f3f063d19..2464429e2 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -836,9 +836,14 @@ public function get_last_days_deployments() return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get(); } - public function deployments(int $skip = 0, int $take = 10) + public function deployments(int $skip = 0, int $take = 10, ?string $pullRequestId = null) { $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc'); + + if ($pullRequestId) { + $deployments = $deployments->where('pull_request_id', $pullRequestId); + } + $count = $deployments->count(); $deployments = $deployments->skip($skip)->take($take)->get(); diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 473fc7b4b..90204d8df 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -36,6 +36,18 @@ public function get_last_days_backup_status($days = 7) return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); } + public function executionsPaginated(int $skip = 0, int $take = 10) + { + $executions = $this->hasMany(ScheduledDatabaseBackupExecution::class)->orderBy('created_at', 'desc'); + $count = $executions->count(); + $executions = $executions->skip($skip)->take($take)->get(); + + return [ + 'count' => $count, + 'executions' => $executions, + ]; + } + public function server() { if ($this->database) { diff --git a/resources/views/livewire/project/application/deployment/index.blade.php b/resources/views/livewire/project/application/deployment/index.blade.php index 096af2878..f8881b736 100644 --- a/resources/views/livewire/project/application/deployment/index.blade.php +++ b/resources/views/livewire/project/application/deployment/index.blade.php @@ -3,31 +3,36 @@

Deployments

-
+

Deployments ({{ $deployments_count }})

@if ($deployments_count > 0) - - - - - - - - - - +
+ + + + + + + Page {{ $currentPage }} of {{ ceil($deployments_count / $defaultTake) }} + + + + + + +
@endif
- @if ($deployments_count > 0) - - - Filter - - @endif +
+ + Filter + @if ($pull_request_id) + Clear + @endif +
@forelse ($deployments as $deployment)
@isset($backup)
-

Executions

+

Executions ({{ $executions_count }})

+ @if ($executions_count > 0) +
+ + + + + + + Page {{ $currentPage }} of {{ ceil($executions_count / $defaultTake) }} + + + + + + +
+ @endif Cleanup Failed Backups +
-
+
@forelse($executions as $execution)
scheduledBackups as $backup) @if ($type == 'database') - + $backup->latest_log && + data_get($backup->latest_log, 'status') === 'running', + 'border-error' => + $backup->latest_log && + data_get($backup->latest_log, 'status') === 'failed', + 'border-success' => + $backup->latest_log && + data_get($backup->latest_log, 'status') === 'success', + 'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log, + ]) href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"> -
-
Frequency: {{ $backup->frequency }} - ({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }}) + @if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running') +
+
-
Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}
+ @endif +
+ @if ($backup->latest_log) + + data_get($backup->latest_log, 'status') === 'running', + 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' => + data_get($backup->latest_log, 'status') === 'failed', + 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' => + data_get($backup->latest_log, 'status') === 'success', + ])> + @php + $statusText = match (data_get($backup->latest_log, 'status')) { + 'success' => 'Success', + 'running' => 'In Progress', + 'failed' => 'Failed', + default => ucfirst(data_get($backup->latest_log, 'status')), + }; + @endphp + {{ $statusText }} + + @else + + No executions yet + + @endif +

{{ $backup->frequency }}

+
+
+ @if ($backup->latest_log) + Started: + {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }} + @if (data_get($backup->latest_log, 'status') !== 'running') +
Ended: + {{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }} +
Duration: + {{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }} +
Finished + {{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }} + @endif + @if ($backup->save_s3) +
S3 Storage: Enabled + @endif + @if (data_get($backup->latest_log, 'status') === 'success') + @php + $size = data_get($backup->latest_log, 'size', 0); + $sizeFormatted = + $size > 0 ? number_format($size / 1024 / 1024, 2) . ' MB' : 'Unknown'; + @endphp +
Last Backup Size: {{ $sizeFormatted }} + @endif + @else + Last Run: Never +
Total Executions: 0 + @if ($backup->save_s3) +
S3 Storage: Enabled + @endif + @endif
@else -
-
- data_get($backup, 'id') === data_get($selectedBackup, 'id'), - 'flex flex-col border-l-2 border-transparent', - ])> -
Frequency: {{ $backup->frequency }} - ({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }}) +
+ data_get($backup, 'id') === data_get($selectedBackup, 'id'), + 'border-blue-500/50 border-dashed' => + $backup->latest_log && + data_get($backup->latest_log, 'status') === 'running', + 'border-error' => + $backup->latest_log && + data_get($backup->latest_log, 'status') === 'failed', + 'border-success' => + $backup->latest_log && + data_get($backup->latest_log, 'status') === 'success', + 'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log, + 'border-coollabs' => + data_get($backup, 'id') === data_get($selectedBackup, 'id'), + ]) wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')"> + @if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running') +
+
-
Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}
+ @endif +
+ @if ($backup->latest_log) + + data_get($backup->latest_log, 'status') === 'running', + 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' => + data_get($backup->latest_log, 'status') === 'failed', + 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' => + data_get($backup->latest_log, 'status') === 'success', + ])> + @php + $statusText = match (data_get($backup->latest_log, 'status')) { + 'success' => 'Success', + 'running' => 'In Progress', + 'failed' => 'Failed', + default => ucfirst(data_get($backup->latest_log, 'status')), + }; + @endphp + {{ $statusText }} + + @else + + No executions yet + + @endif +

{{ $backup->frequency }} Backup

+
+
+ @if ($backup->latest_log) + Started: + {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }} + @if (data_get($backup->latest_log, 'status') !== 'running') +
Ended: + {{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }} +
Duration: + {{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }} +
Finished + {{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }} + @endif +

Total Executions: {{ $backup->executions()->count() }} + @if ($backup->save_s3) +
S3 Storage: Enabled + @endif + @php + $successCount = $backup->executions()->where('status', 'success')->count(); + $totalCount = $backup->executions()->count(); + $successRate = $totalCount > 0 ? round(($successCount / $totalCount) * 100) : 0; + @endphp + @if ($totalCount > 0) +
Success Rate: $successRate >= 80, + 'text-yellow-600' => $successRate >= 50 && $successRate < 80, + 'text-red-600' => $successRate < 50, + ])>{{ $successRate }}% + ({{ $successCount }}/{{ $totalCount }}) + @endif + @if (data_get($backup->latest_log, 'status') === 'success') + @php + $size = data_get($backup->latest_log, 'size', 0); + $sizeFormatted = + $size > 0 ? number_format($size / 1024 / 1024, 2) . ' MB' : 'Unknown'; + @endphp +
Last Backup Size: {{ $sizeFormatted }} + @endif + @else + Last Run: Never +
Total Executions: 0 + @if ($backup->save_s3) +
S3 Storage: Enabled + @endif + @endif
@endif From 3bb28c29be58cec16ba166073fe161ab14e48178 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Jul 2025 14:45:01 +0200 Subject: [PATCH 0050/1717] fix(application): sanitize service names for HTML form binding and ensure original names are stored in docker compose domains --- app/Livewire/Project/Application/General.php | 50 +++++++++++++++++-- .../project/application/general.blade.php | 2 +- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 74f47232c..3e89ad663 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -156,6 +156,14 @@ public function mount() $this->application->settings->save(); } $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; + // Convert service names with dots to use underscores for HTML form binding + $sanitizedDomains = []; + foreach ($this->parsedServiceDomains as $serviceName => $domain) { + $sanitizedKey = str($serviceName)->slug('_')->toString(); + $sanitizedDomains[$sanitizedKey] = $domain; + } + $this->parsedServiceDomains = $sanitizedDomains; + $this->ports_exposes = $this->application->ports_exposes; $this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled; $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; @@ -242,8 +250,26 @@ public function generateDomain(string $serviceName) { $uuid = new Cuid2; $domain = generateFqdn($this->application->destination->server, $uuid); - $this->parsedServiceDomains[$serviceName]['domain'] = $domain; - $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); + $sanitizedKey = str($serviceName)->slug('_')->toString(); + $this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain; + + // Convert back to original service names for storage + $originalDomains = []; + foreach ($this->parsedServiceDomains as $key => $value) { + // Find the original service name by checking parsed services + $originalServiceName = $key; + if (isset($this->parsedServices['services'])) { + foreach ($this->parsedServices['services'] as $originalName => $service) { + if (str($originalName)->slug('_')->toString() === $key) { + $originalServiceName = $originalName; + break; + } + } + } + $originalDomains[$originalServiceName] = $value; + } + + $this->application->docker_compose_domains = json_encode($originalDomains); $this->application->save(); $this->dispatch('success', 'Domain generated.'); if ($this->application->build_pack === 'dockercompose') { @@ -429,9 +455,25 @@ public function submit($showToaster = true) $this->application->publish_directory = rtrim($this->application->publish_directory, '/'); } if ($this->application->build_pack === 'dockercompose') { - $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); + // Convert sanitized service names back to original names for storage + $originalDomains = []; + foreach ($this->parsedServiceDomains as $key => $value) { + // Find the original service name by checking parsed services + $originalServiceName = $key; + if (isset($this->parsedServices['services'])) { + foreach ($this->parsedServices['services'] as $originalName => $service) { + if (str($originalName)->slug('_')->toString() === $key) { + $originalServiceName = $originalName; + break; + } + } + } + $originalDomains[$originalServiceName] = $value; + } - foreach ($this->parsedServiceDomains as $serviceName => $service) { + $this->application->docker_compose_domains = json_encode($originalDomains); + + foreach ($originalDomains as $serviceName => $service) { $domain = data_get($service, 'domain'); if ($domain) { if (! validate_dns_entry($domain, $this->application->destination->server)) { diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 39102c39b..03f26fd04 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -51,7 +51,7 @@ + id="parsedServiceDomains.{{ str($serviceName)->slug('_') }}.domain"> Generate Domain
From 76766017b04fa2a3b41db873fb1391d4efd4611a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Jul 2025 14:53:34 +0200 Subject: [PATCH 0051/1717] fix(previews): adjust padding for rate limit message in application previews --- resources/views/livewire/project/application/previews.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/application/previews.blade.php b/resources/views/livewire/project/application/previews.blade.php index 37f7e35b5..8bca6b4be 100644 --- a/resources/views/livewire/project/application/previews.blade.php +++ b/resources/views/livewire/project/application/previews.blade.php @@ -13,7 +13,7 @@ class="dark:text-warning">{{ $application->destination->server->name }}.<
@endif @isset($rate_limit_remaining) -
Requests remaining till rate limited by Git: {{ $rate_limit_remaining }}
+
Requests remaining till rate limited by Git: {{ $rate_limit_remaining }}
@endisset
@if ($pull_requests->count() > 0) From e5a0cdf3b71a8112c5cd099aed18ea101a8161be Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:22:02 +0200 Subject: [PATCH 0052/1717] fix(previews): order application previews by pull request ID in descending order --- app/Models/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Application.php b/app/Models/Application.php index 2464429e2..f304debad 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -798,7 +798,7 @@ public function environment() public function previews() { - return $this->hasMany(ApplicationPreview::class); + return $this->hasMany(ApplicationPreview::class)->orderBy('pull_request_id', 'desc'); } public function deployment_queue() From 98ceec3b7e6faecef6adc0d8ad2ee2fea8f31596 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:22:12 +0200 Subject: [PATCH 0053/1717] fix(previews): add unique wire keys for preview containers and services based on pull request ID --- .../views/livewire/project/application/previews.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/project/application/previews.blade.php b/resources/views/livewire/project/application/previews.blade.php index 8bca6b4be..4fec0f48f 100644 --- a/resources/views/livewire/project/application/previews.blade.php +++ b/resources/views/livewire/project/application/previews.blade.php @@ -66,7 +66,7 @@ class="dark:text-warning">{{ $application->destination->server->name }}.<

Deployments

@foreach (data_get($application, 'previews') as $previewName => $preview) -
+
PR #{{ data_get($preview, 'pull_request_id') }} | @if (str(data_get($preview, 'status'))->startsWith('running')) @@ -100,7 +100,7 @@ class="flex items-end gap-2 pt-4"> @else @foreach (collect(json_decode($preview->docker_compose_domains)) as $serviceName => $service) - @endforeach @endif From a9e542bc4beb5763c32ba86d2bb0eb455820c86d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:42:01 +0200 Subject: [PATCH 0054/1717] fix(previews): enhance domain generation logic for application previews, ensuring unique domains are created when none are set --- .../Project/Application/PreviewsCompose.php | 35 ++++++++++++++----- app/Models/ApplicationPreview.php | 29 +++++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index b3e838bb3..334d96cad 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -38,9 +38,25 @@ public function generate() $domain = $domains->first(function ($_, $key) { return $key === $this->serviceName; }); - if ($domain) { - $domain = data_get($domain, 'domain'); - $url = Url::fromString($domain); + + $domain_string = data_get($domain, 'domain'); + + // If no domain is set in the main application, generate a default domain + if (empty($domain_string)) { + $server = $this->preview->application->destination->server; + $template = $this->preview->application->preview_url_template; + $random = new Cuid2; + + // Generate a unique domain like main app services do + $generated_fqdn = generateFqdn($server, $random); + + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', str($generated_fqdn)->after('://'), $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); + $preview_fqdn = str($generated_fqdn)->before('://').'://'.$preview_fqdn; + } else { + // Use the existing domain from the main application + $url = Url::fromString($domain_string); $template = $this->preview->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); @@ -49,12 +65,15 @@ public function generate() $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); $preview_fqdn = "$schema://$preview_fqdn"; - $docker_compose_domains = data_get($this->preview, 'docker_compose_domains'); - $docker_compose_domains = json_decode($docker_compose_domains, true); - $docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn; - $this->preview->docker_compose_domains = json_encode($docker_compose_domains); - $this->preview->save(); } + + // Save the generated domain + $docker_compose_domains = data_get($this->preview, 'docker_compose_domains'); + $docker_compose_domains = json_decode($docker_compose_domains, true); + $docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn; + $this->preview->docker_compose_domains = json_encode($docker_compose_domains); + $this->preview->save(); + $this->dispatch('update_links'); $this->dispatch('success', 'Domain generated.'); } diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index c635f146a..7bf45de85 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -56,8 +56,34 @@ public function generate_preview_fqdn_compose() $docker_compose_domains = data_get($this, 'docker_compose_domains'); $docker_compose_domains = json_decode($docker_compose_domains, true) ?? []; + // Get all services from the parsed compose file to ensure all services have entries + $parsedServices = $this->application->parse(pull_request_id: $this->pull_request_id); + if (isset($parsedServices['services'])) { + foreach ($parsedServices['services'] as $serviceName => $service) { + if (! isDatabaseImage(data_get($service, 'image'))) { + // Remove PR suffix from service name to get original service name + $originalServiceName = str($serviceName)->replaceLast('-pr-'.$this->pull_request_id, '')->toString(); + + // Ensure all services have an entry, even if empty + if (! $services->has($originalServiceName)) { + $services->put($originalServiceName, ['domain' => '']); + } + } + } + } + foreach ($services as $service_name => $service_config) { $domain_string = data_get($service_config, 'domain'); + + // If domain string is empty or null, don't auto-generate domain + // Only generate domains when main app already has domains set + if (empty($domain_string)) { + // Ensure service has an empty domain entry for form binding + $docker_compose_domains[$service_name]['domain'] = ''; + + continue; + } + $service_domains = str($domain_string)->explode(',')->map(fn ($d) => trim($d)); $preview_domains = []; @@ -80,6 +106,9 @@ public function generate_preview_fqdn_compose() if (! empty($preview_domains)) { $docker_compose_domains[$service_name]['domain'] = implode(',', $preview_domains); + } else { + // Ensure service has an empty domain entry for form binding + $docker_compose_domains[$service_name]['domain'] = ''; } } From 8a107b3c4bcd37ea3aa21eec2de2db8e2efa086f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:47:16 +0200 Subject: [PATCH 0055/1717] fix(previews): refine preview domain generation for Docker Compose applications, ensuring correct method usage based on build pack type --- app/Jobs/ApplicationDeploymentJob.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index bc5fab30c..708d2f15d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -229,7 +229,16 @@ public function __construct(public int $application_deployment_queue_id) // Set preview fqdn if ($this->pull_request_id !== 0) { - $this->preview = $this->application->generate_preview_fqdn($this->pull_request_id); + if ($this->application->build_pack === 'dockercompose') { + // For Docker Compose apps, use the preview model's compose-specific method + $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); + if ($this->preview) { + $this->preview->generate_preview_fqdn_compose(); + } + } else { + // For non-Docker Compose apps, use the application model's method + $this->preview = $this->application->generate_preview_fqdn($this->pull_request_id); + } if ($this->application->is_github_based()) { ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS); } From 5bdf2e84816f5bfa195b1e1c06db6a27e3d60df9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:12:57 +0200 Subject: [PATCH 0056/1717] refactor(previews): move preview domain generation logic to ApplicationPreview model for better encapsulation and consistency across webhook handlers --- app/Http/Controllers/Webhook/Bitbucket.php | 3 +- app/Http/Controllers/Webhook/Gitea.php | 3 +- app/Http/Controllers/Webhook/Github.php | 3 +- app/Http/Controllers/Webhook/Gitlab.php | 3 +- app/Jobs/ApplicationDeploymentJob.php | 7 +++-- app/Livewire/Project/Application/Previews.php | 4 +-- app/Models/Application.php | 28 ------------------- app/Models/ApplicationPreview.php | 27 ++++++++++++++++++ 8 files changed, 42 insertions(+), 36 deletions(-) diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 490b66e58..078494f82 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -143,12 +143,13 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - ApplicationPreview::create([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'bitbucket', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, ]); + $pr_app->generate_preview_fqdn(); } } $result = queue_application_deployment( diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 3c3d6e0b6..3e0c5a0b6 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -175,12 +175,13 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - ApplicationPreview::create([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'gitea', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, ]); + $pr_app->generate_preview_fqdn(); } } $result = queue_application_deployment( diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 597ec023f..8872754e5 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -183,12 +183,13 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - ApplicationPreview::create([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'github', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, ]); + $pr_app->generate_preview_fqdn(); } } diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index d6d12a05f..3187663d4 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -202,12 +202,13 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - ApplicationPreview::create([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'gitlab', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, ]); + $pr_app->generate_preview_fqdn(); } } $result = queue_application_deployment( diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 708d2f15d..67043555d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -236,8 +236,11 @@ public function __construct(public int $application_deployment_queue_id) $this->preview->generate_preview_fqdn_compose(); } } else { - // For non-Docker Compose apps, use the application model's method - $this->preview = $this->application->generate_preview_fqdn($this->pull_request_id); + // For non-Docker Compose apps, use the preview model's method + $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); + if ($this->preview) { + $this->preview->generate_preview_fqdn(); + } } if ($this->application->is_github_based()) { ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS); diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index c781c9d8a..64e920cd7 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -86,7 +86,7 @@ public function generate_preview($preview_id) return; } - $this->application->generate_preview_fqdn($preview->pull_request_id); + $preview->generate_preview_fqdn(); $this->application->refresh(); $this->dispatch('update_links'); $this->dispatch('success', 'Domain generated.'); @@ -118,7 +118,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null) 'pull_request_html_url' => $pull_request_html_url, ]); } - $this->application->generate_preview_fqdn($pull_request_id); + $found->generate_preview_fqdn(); $this->application->refresh(); $this->dispatch('update_links'); $this->dispatch('success', 'Preview added.'); diff --git a/app/Models/Application.php b/app/Models/Application.php index f304debad..86eea1de8 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1583,34 +1583,6 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false } } - public function generate_preview_fqdn(int $pull_request_id) - { - $preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->id, $pull_request_id); - if (is_null(data_get($preview, 'fqdn')) && $this->fqdn) { - if (str($this->fqdn)->contains(',')) { - $url = Url::fromString(str($this->fqdn)->explode(',')[0]); - $preview_fqdn = getFqdnWithoutPort(str($this->fqdn)->explode(',')[0]); - } else { - $url = Url::fromString($this->fqdn); - if (data_get($preview, 'fqdn')) { - $preview_fqdn = getFqdnWithoutPort(data_get($preview, 'fqdn')); - } - } - $template = $this->preview_url_template; - $host = $url->getHost(); - $schema = $url->getScheme(); - $random = new Cuid2; - $preview_fqdn = str_replace('{{random}}', $random, $template); - $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); - $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); - $preview_fqdn = "$schema://$preview_fqdn"; - $preview->fqdn = $preview_fqdn; - $preview->save(); - } - - return $preview; - } - public static function getDomainsByUuid(string $uuid): array { $application = self::where('uuid', $uuid)->first(); diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 7bf45de85..a92eb320b 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -50,6 +50,33 @@ public function application() return $this->belongsTo(Application::class); } + public function generate_preview_fqdn() + { + if (is_null($this->fqdn) && $this->application->fqdn) { + if (str($this->application->fqdn)->contains(',')) { + $url = Url::fromString(str($this->application->fqdn)->explode(',')[0]); + $preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]); + } else { + $url = Url::fromString($this->application->fqdn); + if ($this->fqdn) { + $preview_fqdn = getFqdnWithoutPort($this->fqdn); + } + } + $template = $this->application->preview_url_template; + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2; + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $this->fqdn = $preview_fqdn; + $this->save(); + } + + return $this; + } + public function generate_preview_fqdn_compose() { $services = collect(json_decode($this->application->docker_compose_domains)) ?? collect(); From 13f10028db3c749387e3b3c617dab8d6aced15b9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:17:40 +0200 Subject: [PATCH 0057/1717] feat(previews): implement soft delete and cleanup for ApplicationPreview, enhancing resource management in DeleteResourceJob --- app/Jobs/DeleteResourceJob.php | 61 ++++++++++++++++++- app/Livewire/Project/Application/Previews.php | 51 +++++----------- app/Models/ApplicationPreview.php | 28 ++++++++- ...leted_at_to_application_previews_table.php | 28 +++++++++ 4 files changed, 129 insertions(+), 39 deletions(-) create mode 100644 database/migrations/2025_07_14_191016_add_deleted_at_to_application_previews_table.php diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 408bb2a7a..2750110f2 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -8,6 +8,7 @@ use App\Actions\Service\DeleteService; use App\Actions\Service\StopService; use App\Models\Application; +use App\Models\ApplicationPreview; use App\Models\Service; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; @@ -30,7 +31,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( - public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, + public Application|ApplicationPreview|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = true, public bool $deleteVolumes = true, public bool $dockerCleanup = true, @@ -42,6 +43,13 @@ public function __construct( public function handle() { try { + // Handle ApplicationPreview instances separately + if ($this->resource instanceof ApplicationPreview) { + $this->deleteApplicationPreview(); + + return; + } + switch ($this->resource->type()) { case 'application': StopApplication::run($this->resource, previewDeployments: true); @@ -104,4 +112,55 @@ public function handle() Artisan::queue('cleanup:stucked-resources'); } } + + private function deleteApplicationPreview() + { + $application = $this->resource->application; + $server = $application->destination->server; + $pull_request_id = $this->resource->pull_request_id; + + // Ensure the preview is soft deleted (may already be done in Livewire component) + if (! $this->resource->trashed()) { + $this->resource->delete(); + } + + try { + if ($server->isSwarm()) { + instant_remote_process(["docker stack rm {$application->uuid}-{$pull_request_id}"], $server); + } else { + $containers = getCurrentApplicationContainerStatus($server, $application->id, $pull_request_id)->toArray(); + $this->stopPreviewContainers($containers, $server); + } + } catch (\Throwable $e) { + // Log the error but don't fail the job + ray('Error stopping preview containers: '.$e->getMessage()); + } + + // Finally, force delete to trigger resource cleanup + $this->resource->forceDelete(); + } + + private function stopPreviewContainers(array $containers, $server, int $timeout = 30) + { + if (empty($containers)) { + return; + } + + $containerNames = []; + foreach ($containers as $container) { + $containerNames[] = str_replace('/', '', $container['Names']); + } + + $containerList = implode(' ', array_map('escapeshellarg', $containerNames)); + $commands = [ + "docker stop --time=$timeout $containerList", + "docker rm -f $containerList", + ]; + + instant_remote_process( + command: $commands, + server: $server, + throwError: false + ); + } } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 64e920cd7..62b1f1929 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Application; use App\Actions\Docker\GetContainersStatus; +use App\Jobs\DeleteResourceJob; use App\Models\Application; use App\Models\ApplicationPreview; use Illuminate\Support\Collection; @@ -205,48 +206,28 @@ public function stop(int $pull_request_id) public function delete(int $pull_request_id) { try { - $server = $this->application->destination->server; + $preview = ApplicationPreview::where('application_id', $this->application->id) + ->where('pull_request_id', $pull_request_id) + ->first(); - if ($this->application->destination->server->isSwarm()) { - instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server); - } else { - $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); - $this->stopContainers($containers, $server); + if (! $preview) { + $this->dispatch('error', 'Preview not found.'); + + return; } - ApplicationPreview::where('application_id', $this->application->id) - ->where('pull_request_id', $pull_request_id) - ->first() - ->delete(); + // Soft delete immediately for instant UI feedback + $preview->delete(); - $this->application->refresh(); + // Dispatch the job for async cleanup (container stopping + force delete) + DeleteResourceJob::dispatch($preview); + + // Refresh the application and its previews relationship to reflect the soft delete + $this->application->load('previews'); $this->dispatch('update_links'); - $this->dispatch('success', 'Preview deleted.'); + $this->dispatch('success', 'Preview deletion started. It may take a few moments to complete.'); } catch (\Throwable $e) { return handleError($e, $this); } } - - private function stopContainers(array $containers, $server, int $timeout = 30) - { - if (empty($containers)) { - return; - } - $containerNames = []; - foreach ($containers as $container) { - $containerNames[] = str_replace('/', '', $container['Names']); - } - - $containerList = implode(' ', array_map('escapeshellarg', $containerNames)); - $commands = [ - "docker stop --time=$timeout $containerList", - "docker rm -f $containerList", - ]; - - instant_remote_process( - command: $commands, - server: $server, - throwError: false - ); - } } diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index a92eb320b..f45f9da40 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -2,19 +2,25 @@ namespace App\Models; +use Illuminate\Database\Eloquent\SoftDeletes; use Spatie\Url\Url; use Visus\Cuid2\Cuid2; class ApplicationPreview extends BaseModel { + use SoftDeletes; + protected $guarded = []; protected static function booted() { - static::deleting(function ($preview) { + static::forceDeleting(function ($preview) { + $server = $preview->application->destination->server; + $application = $preview->application; + if (data_get($preview, 'application.build_pack') === 'dockercompose') { - $server = $preview->application->destination->server; - $composeFile = $preview->application->parse(pull_request_id: $preview->pull_request_id); + // Docker Compose volume and network cleanup + $composeFile = $application->parse(pull_request_id: $preview->pull_request_id); $volumes = data_get($composeFile, 'volumes'); $networks = data_get($composeFile, 'networks'); $networkKeys = collect($networks)->keys(); @@ -26,7 +32,18 @@ protected static function booted() instant_remote_process(["docker network disconnect $key coolify-proxy"], $server, false); instant_remote_process(["docker network rm $key"], $server, false); }); + } else { + // Regular application volume cleanup + $persistentStorages = $preview->persistentStorages()->get() ?? collect(); + if ($persistentStorages->count() > 0) { + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } } + + // Clean up persistent storage records + $preview->persistentStorages()->delete(); }); static::saving(function ($preview) { if ($preview->isDirty('status')) { @@ -50,6 +67,11 @@ public function application() return $this->belongsTo(Application::class); } + public function persistentStorages() + { + return $this->morphMany(\App\Models\LocalPersistentVolume::class, 'resource'); + } + public function generate_preview_fqdn() { if (is_null($this->fqdn) && $this->application->fqdn) { diff --git a/database/migrations/2025_07_14_191016_add_deleted_at_to_application_previews_table.php b/database/migrations/2025_07_14_191016_add_deleted_at_to_application_previews_table.php new file mode 100644 index 000000000..25aa0f5f0 --- /dev/null +++ b/database/migrations/2025_07_14_191016_add_deleted_at_to_application_previews_table.php @@ -0,0 +1,28 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_previews', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; From ed6e58f7ee817eed369c27ffc842e5088cce64d6 Mon Sep 17 00:00:00 2001 From: ari Date: Fri, 18 Jul 2025 15:00:49 +0200 Subject: [PATCH 0058/1717] fix(ui): typo on proxy request handler tooltip (#6192) --- resources/views/livewire/server/proxy.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index 39bab8185..76fbd09b7 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -29,7 +29,7 @@ label="Generate labels only for {{ str($server->proxyType())->title() }}" instantSave /> + helper="Requests to unknown hosts or stopped services will receive a 503 response or be redirected to the URL you set below (need to enable this first)." /> @if ($redirect_enabled) From e5e1bdcd4ddbc80ecd24a2ea6b56fd3d0ace5218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8F=94=EF=B8=8F=20Peak?= <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:47:14 +0200 Subject: [PATCH 0059/1717] fix(backups): large database backups are not working (#6217) --- app/Jobs/DatabaseBackupJob.php | 33 ++++++++++++++++--- app/Livewire/Project/Database/BackupEdit.php | 5 +++ ...ut_to_scheduled_database_backups_table.php | 18 ++++++++++ .../project/database/backup-edit.blade.php | 1 + 4 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 database/migrations/2025_07_16_202201_add_timeout_to_scheduled_database_backups_table.php diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index a6c423cac..adb5473b5 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -23,6 +23,8 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; +use Throwable; +use Visus\Cuid2\Cuid2; class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { @@ -60,9 +62,16 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue public ?S3Storage $s3 = null; + public $timeout = 3600; + + public string $backup_log_uuid; + public function __construct(public ScheduledDatabaseBackup $backup) { $this->onQueue('high'); + $this->timeout = $backup->timeout; + + $this->backup_log_uuid = (string) new Cuid2; } public function handle(): void @@ -219,12 +228,8 @@ public function handle(): void $this->mongo_root_username = str($rootUsername)->after('MONGO_INITDB_ROOT_USERNAME=')->value(); } } - \Log::info('MongoDB credentials extracted from environment', [ - 'has_username' => filled($this->mongo_root_username), - 'has_password' => filled($this->mongo_root_password), - ]); + } catch (\Throwable $e) { - \Log::warning('Failed to extract MongoDB environment variables', ['error' => $e->getMessage()]); // Continue without env vars - will be handled in backup_standalone_mongodb method } } @@ -288,6 +293,7 @@ public function handle(): void } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'uuid' => $this->backup_log_uuid, 'database_name' => $database, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, @@ -307,6 +313,7 @@ public function handle(): void $this->backup_file = "/mongo-dump-$databaseName-".Carbon::now()->timestamp.'.tar.gz'; $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'uuid' => $this->backup_log_uuid, 'database_name' => $databaseName, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, @@ -319,6 +326,7 @@ public function handle(): void } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'uuid' => $this->backup_log_uuid, 'database_name' => $database, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, @@ -331,6 +339,7 @@ public function handle(): void } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'uuid' => $this->backup_log_uuid, 'database_name' => $database, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, @@ -574,4 +583,18 @@ private function getFullImageName(): string return "{$helperImage}:{$latestVersion}"; } + + public function failed(?Throwable $exception): void + { + $log = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->first(); + + if ($log) { + $log->update([ + 'status' => 'failed', + 'message' => 'Job failed: '.$exception->getMessage(), + 'size' => 0, + 'filename' => null, + ]); + } + } } diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 0d363e983..abc88d736 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -73,6 +73,9 @@ class BackupEdit extends Component #[Validate(['required', 'boolean'])] public bool $dumpAll = false; + #[Validate(['required', 'int', 'min:1', 'max:36000'])] + public int $timeout = 3600; + public function mount() { try { @@ -98,6 +101,7 @@ public function syncData(bool $toModel = false) $this->backup->s3_storage_id = $this->s3StorageId; $this->backup->databases_to_backup = $this->databasesToBackup; $this->backup->dump_all = $this->dumpAll; + $this->backup->timeout = $this->timeout; $this->customValidate(); $this->backup->save(); } else { @@ -114,6 +118,7 @@ public function syncData(bool $toModel = false) $this->s3StorageId = $this->backup->s3_storage_id; $this->databasesToBackup = $this->backup->databases_to_backup; $this->dumpAll = $this->backup->dump_all; + $this->timeout = $this->backup->timeout; } } diff --git a/database/migrations/2025_07_16_202201_add_timeout_to_scheduled_database_backups_table.php b/database/migrations/2025_07_16_202201_add_timeout_to_scheduled_database_backups_table.php new file mode 100644 index 000000000..f8f8cb8ad --- /dev/null +++ b/database/migrations/2025_07_16_202201_add_timeout_to_scheduled_database_backups_table.php @@ -0,0 +1,18 @@ +integer('timeout')->default(3600); + }); + } +}; diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php index f83af91a0..c070edba2 100644 --- a/resources/views/livewire/project/database/backup-edit.blade.php +++ b/resources/views/livewire/project/database/backup-edit.blade.php @@ -77,6 +77,7 @@ +

Backup Retention Settings

From a0db5b342f36dbcffe121794fcbe6cdefd45be08 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:56:01 +0200 Subject: [PATCH 0060/1717] fix(backups): error message if there is no exception --- app/Jobs/DatabaseBackupJob.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index adb5473b5..428cdfda2 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -591,7 +591,7 @@ public function failed(?Throwable $exception): void if ($log) { $log->update([ 'status' => 'failed', - 'message' => 'Job failed: '.$exception->getMessage(), + 'message' => 'Job failed: '.($exception?->getMessage() ?? 'Unknown error'), 'size' => 0, 'filename' => null, ]); From 889712b53a7b4366f7c7bcc40ac470cbbfbec7ec Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:24:13 +0200 Subject: [PATCH 0061/1717] chore(deps): update npm and composer deps --- composer.json | 68 +-- composer.lock | 426 +++++++++---------- docker/coolify-realtime/package-lock.json | 13 +- package-lock.json | 477 +++++++++++----------- 4 files changed, 500 insertions(+), 484 deletions(-) diff --git a/composer.json b/composer.json index 854ba1dab..7e8e768c0 100644 --- a/composer.json +++ b/composer.json @@ -13,65 +13,65 @@ "require": { "php": "^8.4", "danharrin/livewire-rate-limiting": "^2.1.0", - "doctrine/dbal": "^4.2.2", - "guzzlehttp/guzzle": "^7.9.2", - "laravel/fortify": "^1.25.4", - "laravel/framework": "^12.4.1", - "laravel/horizon": "^5.30.3", - "laravel/pail": "^1.2.2", - "laravel/prompts": "^0.3.5|^0.3.5|^0.3.5", - "laravel/sanctum": "^4.0.8", - "laravel/socialite": "^5.18.0", + "doctrine/dbal": "^4.3.0", + "guzzlehttp/guzzle": "^7.9.3", + "laravel/fortify": "^1.27.0", + "laravel/framework": "^12.20.0", + "laravel/horizon": "^5.33.1", + "laravel/pail": "^1.2.3", + "laravel/prompts": "^0.3.6|^0.3.6|^0.3.6", + "laravel/sanctum": "^4.1.2", + "laravel/socialite": "^5.21.0", "laravel/tinker": "^2.10.1", "laravel/ui": "^4.6.1", "lcobucci/jwt": "^5.5.0", "league/flysystem-aws-s3-v3": "^3.29", - "league/flysystem-sftp-v3": "^3.29", - "livewire/livewire": "^3.5.20", + "league/flysystem-sftp-v3": "^3.30", + "livewire/livewire": "^3.6.4", "log1x/laravel-webfonts": "^2.0.1", - "lorisleiva/laravel-actions": "^2.8.6", + "lorisleiva/laravel-actions": "^2.9.0", "nubs/random-name-generator": "^2.2", - "phpseclib/phpseclib": "^3.0.43", - "pion/laravel-chunk-upload": "^1.5.4", + "phpseclib/phpseclib": "^3.0.46", + "pion/laravel-chunk-upload": "^1.5.6", "poliander/cron": "^3.2.1", "purplepixie/phpdns": "^2.2", "pusher/pusher-php-server": "^7.2.7", "resend/resend-laravel": "^0.19.0", - "sentry/sentry-laravel": "^4.13", + "sentry/sentry-laravel": "^4.15.1", "socialiteproviders/authentik": "^5.2", "socialiteproviders/clerk": "^5.0", "socialiteproviders/discord": "^4.2", "socialiteproviders/google": "^4.1", "socialiteproviders/infomaniak": "^4.0", "socialiteproviders/microsoft-azure": "^5.2", - "socialiteproviders/zitadel": "^4.1", - "spatie/laravel-activitylog": "^4.10.1", - "spatie/laravel-data": "^4.13.1", - "spatie/laravel-ray": "^1.39.1", + "socialiteproviders/zitadel": "^4.2", + "spatie/laravel-activitylog": "^4.10.2", + "spatie/laravel-data": "^4.17.0", + "spatie/laravel-ray": "^1.40.2", "spatie/laravel-schemaless-attributes": "^2.5.1", "spatie/url": "^2.4", - "stevebauman/purify": "^6.3", - "stripe/stripe-php": "^16.5.1", - "symfony/yaml": "^7.2.3", + "stevebauman/purify": "^6.3.1", + "stripe/stripe-php": "^16.6.0", + "symfony/yaml": "^7.3.1", "visus/cuid2": "^4.1.0", "yosymfony/toml": "^1.0.4", - "zircote/swagger-php": "^5.0.5" + "zircote/swagger-php": "^5.1.4" }, "require-dev": { - "barryvdh/laravel-debugbar": "^3.15.1", - "driftingly/rector-laravel": "^2.0.2", + "barryvdh/laravel-debugbar": "^3.15.4", + "driftingly/rector-laravel": "^2.0.5", "fakerphp/faker": "^1.24.1", - "laravel/dusk": "^8.3.1", - "laravel/pint": "^1.21", - "laravel/telescope": "^5.5", + "laravel/dusk": "^8.3.3", + "laravel/pint": "^1.24", + "laravel/telescope": "^5.10", "mockery/mockery": "^1.6.12", - "nunomaduro/collision": "^8.6.1", - "pestphp/pest": "^3.8.0", - "phpstan/phpstan": "^2.1.6", - "rector/rector": "^2.0.9", + "nunomaduro/collision": "^8.8.2", + "pestphp/pest": "^3.8.2", + "phpstan/phpstan": "^2.1.18", + "rector/rector": "^2.1.2", "serversideup/spin": "^3.0.2", "spatie/laravel-ignition": "^2.9.1", - "symfony/http-client": "^7.2.3" + "symfony/http-client": "^7.3.1" }, "minimum-stability": "stable", "prefer-stable": true, @@ -127,4 +127,4 @@ "@php artisan key:generate --ansi" ] } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index d655ad48d..8d170cdc1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f1d647186c558d85c525f8a6314474d4", + "content-hash": "52a680a0eb446dcaa74bc35e158aca57", "packages": [ { "name": "amphp/amp", @@ -870,16 +870,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.347.0", + "version": "3.351.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "c66a35e650f077caddd7db8d3a1f58b2c2b8c78b" + "reference": "f3e20c8cdd2cc5827d77a0b3c0872fab89cdf805" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c66a35e650f077caddd7db8d3a1f58b2c2b8c78b", - "reference": "c66a35e650f077caddd7db8d3a1f58b2c2b8c78b", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f3e20c8cdd2cc5827d77a0b3c0872fab89cdf805", + "reference": "f3e20c8cdd2cc5827d77a0b3c0872fab89cdf805", "shasum": "" }, "require": { @@ -961,9 +961,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.347.0" + "source": "https://github.com/aws/aws-sdk-php/tree/3.351.1" }, - "time": "2025-06-23T18:12:15+00:00" + "time": "2025-07-17T18:07:08+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1373,21 +1373,21 @@ }, { "name": "doctrine/dbal", - "version": "4.2.4", + "version": "4.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "b37d160498ea91a2382a2ebe825c4ea6254fc0ec" + "reference": "5fe09532be619202d59c70956c6fb20e97933ee3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/b37d160498ea91a2382a2ebe825c4ea6254fc0ec", - "reference": "b37d160498ea91a2382a2ebe825c4ea6254fc0ec", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/5fe09532be619202d59c70956c6fb20e97933ee3", + "reference": "5fe09532be619202d59c70956c6fb20e97933ee3", "shasum": "" }, "require": { - "doctrine/deprecations": "^0.5.3|^1", - "php": "^8.1", + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, @@ -1398,7 +1398,7 @@ "phpstan/phpstan": "2.1.17", "phpstan/phpstan-phpunit": "2.0.6", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "10.5.46", + "phpunit/phpunit": "11.5.23", "slevomat/coding-standard": "8.16.2", "squizlabs/php_codesniffer": "3.13.1", "symfony/cache": "^6.3.8|^7.0", @@ -1459,7 +1459,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.2.4" + "source": "https://github.com/doctrine/dbal/tree/4.3.0" }, "funding": [ { @@ -1475,7 +1475,7 @@ "type": "tidelift" } ], - "time": "2025-06-15T23:15:01+00:00" + "time": "2025-06-16T19:31:04+00:00" }, { "name": "doctrine/deprecations", @@ -2678,16 +2678,16 @@ }, { "name": "laravel/framework", - "version": "v12.19.3", + "version": "v12.20.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "4e6ec689ef704bb4bd282f29d9dd658dfb4fb262" + "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/4e6ec689ef704bb4bd282f29d9dd658dfb4fb262", - "reference": "4e6ec689ef704bb4bd282f29d9dd658dfb4fb262", + "url": "https://api.github.com/repos/laravel/framework/zipball/1b9a00f8caf5503c92aa436279172beae1a484ff", + "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff", "shasum": "" }, "require": { @@ -2889,7 +2889,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-18T12:56:23+00:00" + "time": "2025-07-08T15:02:21+00:00" }, { "name": "laravel/horizon", @@ -3052,16 +3052,16 @@ }, { "name": "laravel/prompts", - "version": "v0.3.5", + "version": "v0.3.6", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1" + "reference": "86a8b692e8661d0fb308cec64f3d176821323077" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1", + "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", + "reference": "86a8b692e8661d0fb308cec64f3d176821323077", "shasum": "" }, "require": { @@ -3105,22 +3105,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.5" + "source": "https://github.com/laravel/prompts/tree/v0.3.6" }, - "time": "2025-02-11T13:34:40+00:00" + "time": "2025-07-07T14:17:42+00:00" }, { "name": "laravel/sanctum", - "version": "v4.1.1", + "version": "v4.1.2", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "a360a6a1fd2400ead4eb9b6a9c1bb272939194f5" + "reference": "e4c09e69aecd5a383e0c1b85a6bb501c997d7491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/a360a6a1fd2400ead4eb9b6a9c1bb272939194f5", - "reference": "a360a6a1fd2400ead4eb9b6a9c1bb272939194f5", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/e4c09e69aecd5a383e0c1b85a6bb501c997d7491", + "reference": "e4c09e69aecd5a383e0c1b85a6bb501c997d7491", "shasum": "" }, "require": { @@ -3171,7 +3171,7 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-04-23T13:03:38+00:00" + "time": "2025-07-01T15:49:32+00:00" }, { "name": "laravel/serializable-closure", @@ -3699,16 +3699,16 @@ }, { "name": "league/flysystem", - "version": "3.29.1", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", "shasum": "" }, "require": { @@ -3732,13 +3732,13 @@ "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", - "ext-mongodb": "^1.3", + "ext-mongodb": "^1.3|^2", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "guzzlehttp/psr7": "^2.6", "microsoft/azure-storage-blob": "^1.1", - "mongodb/mongodb": "^1.2", + "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", @@ -3776,9 +3776,9 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" }, - "time": "2024-10-08T08:58:34+00:00" + "time": "2025-06-25T13:29:59+00:00" }, { "name": "league/flysystem-aws-s3-v3", @@ -3837,16 +3837,16 @@ }, { "name": "league/flysystem-local", - "version": "3.29.0", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", "shasum": "" }, "require": { @@ -3880,22 +3880,22 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" }, - "time": "2024-08-09T21:24:39+00:00" + "time": "2025-05-21T10:34:19+00:00" }, { "name": "league/flysystem-sftp-v3", - "version": "3.29.0", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-sftp-v3.git", - "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252" + "reference": "93f297837b5052f4cfee601b2e0352addd956448" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/ce9b209e2fbe33122c755ffc18eb4d5bd256f252", - "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252", + "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/93f297837b5052f4cfee601b2e0352addd956448", + "reference": "93f297837b5052f4cfee601b2e0352addd956448", "shasum": "" }, "require": { @@ -3929,9 +3929,9 @@ "sftp" ], "support": { - "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.30.0" }, - "time": "2024-08-14T19:35:54+00:00" + "time": "2025-04-17T15:49:35+00:00" }, { "name": "league/mime-type-detection", @@ -4241,16 +4241,16 @@ }, { "name": "livewire/livewire", - "version": "v3.6.3", + "version": "v3.6.4", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "56aa1bb63a46e06181c56fa64717a7287e19115e" + "reference": "ef04be759da41b14d2d129e670533180a44987dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/56aa1bb63a46e06181c56fa64717a7287e19115e", - "reference": "56aa1bb63a46e06181c56fa64717a7287e19115e", + "url": "https://api.github.com/repos/livewire/livewire/zipball/ef04be759da41b14d2d129e670533180a44987dc", + "reference": "ef04be759da41b14d2d129e670533180a44987dc", "shasum": "" }, "require": { @@ -4305,7 +4305,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.6.3" + "source": "https://github.com/livewire/livewire/tree/v3.6.4" }, "funding": [ { @@ -4313,7 +4313,7 @@ "type": "github" } ], - "time": "2025-04-12T22:26:52+00:00" + "time": "2025-07-17T05:12:15+00:00" }, { "name": "log1x/laravel-webfonts", @@ -5883,16 +5883,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.45", + "version": "3.0.46", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "bd81b90d5963c6b9d87de50357585375223f4dd8" + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/bd81b90d5963c6b9d87de50357585375223f4dd8", - "reference": "bd81b90d5963c6b9d87de50357585375223f4dd8", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", "shasum": "" }, "require": { @@ -5973,7 +5973,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.45" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46" }, "funding": [ { @@ -5989,20 +5989,20 @@ "type": "tidelift" } ], - "time": "2025-06-22T22:54:43+00:00" + "time": "2025-06-26T16:29:55+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", "shasum": "" }, "require": { @@ -6034,9 +6034,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" }, - "time": "2025-02-19T13:28:12+00:00" + "time": "2025-07-13T07:04:09+00:00" }, { "name": "pion/laravel-chunk-upload", @@ -6971,21 +6971,20 @@ }, { "name": "ramsey/uuid", - "version": "4.8.1", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28" + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28", - "reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", "shasum": "" }, "require": { "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", - "ext-json": "*", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -7044,9 +7043,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.8.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.0" }, - "time": "2025-06-01T06:28:46+00:00" + "time": "2025-06-25T14:20:11+00:00" }, { "name": "resend/resend-laravel", @@ -7119,16 +7118,16 @@ }, { "name": "resend/resend-php", - "version": "v0.18.0", + "version": "v0.18.1", "source": { "type": "git", "url": "https://github.com/resend/resend-php.git", - "reference": "d6194782ff1952627bcdd52e5958572c4bd98043" + "reference": "f20a9a50a7f705294af1662995c93bfd806fed3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/resend/resend-php/zipball/d6194782ff1952627bcdd52e5958572c4bd98043", - "reference": "d6194782ff1952627bcdd52e5958572c4bd98043", + "url": "https://api.github.com/repos/resend/resend-php/zipball/f20a9a50a7f705294af1662995c93bfd806fed3e", + "reference": "f20a9a50a7f705294af1662995c93bfd806fed3e", "shasum": "" }, "require": { @@ -7170,9 +7169,9 @@ ], "support": { "issues": "https://github.com/resend/resend-php/issues", - "source": "https://github.com/resend/resend-php/tree/v0.18.0" + "source": "https://github.com/resend/resend-php/tree/v0.18.1" }, - "time": "2025-05-06T21:18:26+00:00" + "time": "2025-07-04T00:12:53+00:00" }, { "name": "revolt/event-loop", @@ -7793,27 +7792,27 @@ }, { "name": "socialiteproviders/zitadel", - "version": "4.1.0", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/SocialiteProviders/Zitadel.git", - "reference": "2e1c0843a9531eb0e31a04b31683a63f1a7d1865" + "reference": "852ceebd71503ac116c42b8aa352e22b4fd396d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SocialiteProviders/Zitadel/zipball/2e1c0843a9531eb0e31a04b31683a63f1a7d1865", - "reference": "2e1c0843a9531eb0e31a04b31683a63f1a7d1865", + "url": "https://api.github.com/repos/SocialiteProviders/Zitadel/zipball/852ceebd71503ac116c42b8aa352e22b4fd396d9", + "reference": "852ceebd71503ac116c42b8aa352e22b4fd396d9", "shasum": "" }, "require": { "ext-json": "*", - "php": "^8.1", + "php": "^8.0", "socialiteproviders/manager": "^4.4" }, "type": "library", "autoload": { "psr-4": { - "Socialiteproviders\\Zitadel\\": "" + "SocialiteProviders\\Zitadel\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -7827,12 +7826,19 @@ } ], "description": "Zitadel OAuth2 Provider for Laravel Socialite", + "keywords": [ + "laravel", + "oauth", + "provider", + "socialite", + "zitadel" + ], "support": { - "docs": "https://socialiteproviders.com/zoho", + "docs": "https://socialiteproviders.com/zitadel", "issues": "https://github.com/socialiteproviders/providers/issues", "source": "https://github.com/socialiteproviders/providers" }, - "time": "2024-08-26T06:14:57+00:00" + "time": "2024-11-07T21:57:40+00:00" }, { "name": "spatie/backtrace", @@ -7990,16 +7996,16 @@ }, { "name": "spatie/laravel-data", - "version": "4.16.1", + "version": "4.17.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-data.git", - "reference": "e652b52bdaca4774abb4a6024736850a1c4ab50b" + "reference": "6b110d25ad4219774241b083d09695b20a7fb472" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-data/zipball/e652b52bdaca4774abb4a6024736850a1c4ab50b", - "reference": "e652b52bdaca4774abb4a6024736850a1c4ab50b", + "url": "https://api.github.com/repos/spatie/laravel-data/zipball/6b110d25ad4219774241b083d09695b20a7fb472", + "reference": "6b110d25ad4219774241b083d09695b20a7fb472", "shasum": "" }, "require": { @@ -8061,7 +8067,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-data/issues", - "source": "https://github.com/spatie/laravel-data/tree/4.16.1" + "source": "https://github.com/spatie/laravel-data/tree/4.17.0" }, "funding": [ { @@ -8069,20 +8075,20 @@ "type": "github" } ], - "time": "2025-06-24T08:19:42+00:00" + "time": "2025-06-25T11:36:37+00:00" }, { "name": "spatie/laravel-package-tools", - "version": "1.92.4", + "version": "1.92.7", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "d20b1969f836d210459b78683d85c9cd5c5f508c" + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d20b1969f836d210459b78683d85c9cd5c5f508c", - "reference": "d20b1969f836d210459b78683d85c9cd5c5f508c", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5", "shasum": "" }, "require": { @@ -8122,7 +8128,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.4" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7" }, "funding": [ { @@ -8130,7 +8136,7 @@ "type": "github" } ], - "time": "2025-04-11T15:27:14+00:00" + "time": "2025-07-17T15:46:43+00:00" }, { "name": "spatie/laravel-ray", @@ -8773,16 +8779,16 @@ }, { "name": "symfony/console", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", - "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", + "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", "shasum": "" }, "require": { @@ -8847,7 +8853,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.0" + "source": "https://github.com/symfony/console/tree/v7.3.1" }, "funding": [ { @@ -8863,7 +8869,7 @@ "type": "tidelift" } ], - "time": "2025-05-24T10:34:04+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/css-selector", @@ -8999,16 +9005,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "cf68d225bc43629de4ff54778029aee6dc191b83" + "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/cf68d225bc43629de4ff54778029aee6dc191b83", - "reference": "cf68d225bc43629de4ff54778029aee6dc191b83", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/35b55b166f6752d6aaf21aa042fc5ed280fce235", + "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235", "shasum": "" }, "require": { @@ -9056,7 +9062,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.0" + "source": "https://github.com/symfony/error-handler/tree/v7.3.1" }, "funding": [ { @@ -9072,7 +9078,7 @@ "type": "tidelift" } ], - "time": "2025-05-29T07:19:49+00:00" + "time": "2025-06-13T07:48:40+00:00" }, { "name": "symfony/event-dispatcher", @@ -9296,16 +9302,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "4236baf01609667d53b20371486228231eb135fd" + "reference": "23dd60256610c86a3414575b70c596e5deff6ed9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4236baf01609667d53b20371486228231eb135fd", - "reference": "4236baf01609667d53b20371486228231eb135fd", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/23dd60256610c86a3414575b70c596e5deff6ed9", + "reference": "23dd60256610c86a3414575b70c596e5deff6ed9", "shasum": "" }, "require": { @@ -9355,7 +9361,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.0" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.1" }, "funding": [ { @@ -9371,20 +9377,20 @@ "type": "tidelift" } ], - "time": "2025-05-12T14:48:23+00:00" + "time": "2025-06-23T15:07:14+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f" + "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ac7b8e163e8c83dce3abcc055a502d4486051a9f", - "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1644879a66e4aa29c36fe33dfa6c54b450ce1831", + "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831", "shasum": "" }, "require": { @@ -9469,7 +9475,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.0" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.1" }, "funding": [ { @@ -9485,20 +9491,20 @@ "type": "tidelift" } ], - "time": "2025-05-29T07:47:32+00:00" + "time": "2025-06-28T08:24:55+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "0f375bbbde96ae8c78e4aa3e63aabd486e33364c" + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/0f375bbbde96ae8c78e4aa3e63aabd486e33364c", - "reference": "0f375bbbde96ae8c78e4aa3e63aabd486e33364c", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368", "shasum": "" }, "require": { @@ -9549,7 +9555,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.0" + "source": "https://github.com/symfony/mailer/tree/v7.3.1" }, "funding": [ { @@ -9565,7 +9571,7 @@ "type": "tidelift" } ], - "time": "2025-04-04T09:51:09+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/mime", @@ -10894,16 +10900,16 @@ }, { "name": "symfony/translation", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "4aba29076a29a3aa667e09b791e5f868973a8667" + "reference": "241d5ac4910d256660238a7ecf250deba4c73063" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/4aba29076a29a3aa667e09b791e5f868973a8667", - "reference": "4aba29076a29a3aa667e09b791e5f868973a8667", + "url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063", + "reference": "241d5ac4910d256660238a7ecf250deba4c73063", "shasum": "" }, "require": { @@ -10970,7 +10976,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.0" + "source": "https://github.com/symfony/translation/tree/v7.3.1" }, "funding": [ { @@ -10986,7 +10992,7 @@ "type": "tidelift" } ], - "time": "2025-05-29T07:19:49+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/translation-contracts", @@ -11068,16 +11074,16 @@ }, { "name": "symfony/uid", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "7beeb2b885cd584cd01e126c5777206ae4c3c6a3" + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/7beeb2b885cd584cd01e126c5777206ae4c3c6a3", - "reference": "7beeb2b885cd584cd01e126c5777206ae4c3c6a3", + "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", "shasum": "" }, "require": { @@ -11122,7 +11128,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.0" + "source": "https://github.com/symfony/uid/tree/v7.3.1" }, "funding": [ { @@ -11138,20 +11144,20 @@ "type": "tidelift" } ], - "time": "2025-05-24T14:28:13+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" + "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", + "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", "shasum": "" }, "require": { @@ -11206,7 +11212,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.0" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.1" }, "funding": [ { @@ -11222,20 +11228,20 @@ "type": "tidelift" } ], - "time": "2025-04-27T18:39:23+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "cea40a48279d58dc3efee8112634cb90141156c2" + "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/cea40a48279d58dc3efee8112634cb90141156c2", - "reference": "cea40a48279d58dc3efee8112634cb90141156c2", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0c3555045a46ab3cd4cc5a69d161225195230edb", + "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb", "shasum": "" }, "require": { @@ -11278,7 +11284,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.0" + "source": "https://github.com/symfony/yaml/tree/v7.3.1" }, "funding": [ { @@ -11294,7 +11300,7 @@ "type": "tidelift" } ], - "time": "2025-04-04T10:10:33+00:00" + "time": "2025-06-03T06:57:57+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -11945,16 +11951,16 @@ }, { "name": "zircote/swagger-php", - "version": "5.1.3", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "b8ba6bd99805c0ae09a38d1b26c1c92820509bd0" + "reference": "471f2e7c24c9508a2ee08df245cab64b62dbf721" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/b8ba6bd99805c0ae09a38d1b26c1c92820509bd0", - "reference": "b8ba6bd99805c0ae09a38d1b26c1c92820509bd0", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/471f2e7c24c9508a2ee08df245cab64b62dbf721", + "reference": "471f2e7c24c9508a2ee08df245cab64b62dbf721", "shasum": "" }, "require": { @@ -12025,9 +12031,9 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/5.1.3" + "source": "https://github.com/zircote/swagger-php/tree/5.1.4" }, - "time": "2025-05-20T03:35:10+00:00" + "time": "2025-07-15T23:54:13+00:00" } ], "packages-dev": [ @@ -12566,16 +12572,16 @@ }, { "name": "laravel/pint", - "version": "v1.22.1", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "941d1927c5ca420c22710e98420287169c7bcaf7" + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/941d1927c5ca420c22710e98420287169c7bcaf7", - "reference": "941d1927c5ca420c22710e98420287169c7bcaf7", + "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", "shasum": "" }, "require": { @@ -12586,10 +12592,10 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.75.0", - "illuminate/view": "^11.44.7", - "larastan/larastan": "^3.4.0", - "laravel-zero/framework": "^11.36.1", + "friendsofphp/php-cs-fixer": "^3.82.2", + "illuminate/view": "^11.45.1", + "larastan/larastan": "^3.5.0", + "laravel-zero/framework": "^11.45.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3.1", "pestphp/pest": "^2.36.0" @@ -12599,6 +12605,9 @@ ], "type": "project", "autoload": { + "files": [ + "overrides/Runner/Parallel/ProcessFactory.php" + ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -12628,20 +12637,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-05-08T08:38:12+00:00" + "time": "2025-07-10T18:09:32+00:00" }, { "name": "laravel/telescope", - "version": "v5.9.1", + "version": "v5.10.0", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "403d4ad1ecfe126139f5cf29cabd6b1c816c46a2" + "reference": "fc0a8662682c0375b534033873debb780c003486" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/403d4ad1ecfe126139f5cf29cabd6b1c816c46a2", - "reference": "403d4ad1ecfe126139f5cf29cabd6b1c816c46a2", + "url": "https://api.github.com/repos/laravel/telescope/zipball/fc0a8662682c0375b534033873debb780c003486", + "reference": "fc0a8662682c0375b534033873debb780c003486", "shasum": "" }, "require": { @@ -12695,9 +12704,9 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v5.9.1" + "source": "https://github.com/laravel/telescope/tree/v5.10.0" }, - "time": "2025-06-10T21:42:27+00:00" + "time": "2025-07-07T14:47:19+00:00" }, { "name": "mockery/mockery", @@ -12784,16 +12793,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.1", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", "shasum": "" }, "require": { @@ -12832,7 +12841,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" }, "funding": [ { @@ -12840,20 +12849,20 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-07-05T12:25:42+00:00" }, { "name": "nunomaduro/collision", - "version": "v8.8.1", + "version": "v8.8.2", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "44ccb82e3e21efb5446748d2a3c81a030ac22bd5" + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/44ccb82e3e21efb5446748d2a3c81a030ac22bd5", - "reference": "44ccb82e3e21efb5446748d2a3c81a030ac22bd5", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", "shasum": "" }, "require": { @@ -12939,7 +12948,7 @@ "type": "patreon" } ], - "time": "2025-06-11T01:04:21+00:00" + "time": "2025-06-25T02:12:12+00:00" }, { "name": "pestphp/pest", @@ -13521,16 +13530,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.17", + "version": "2.1.18", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" + "reference": "ee1f390b7a70cdf74a2b737e554f68afea885db7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", - "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ee1f390b7a70cdf74a2b737e554f68afea885db7", + "reference": "ee1f390b7a70cdf74a2b737e554f68afea885db7", "shasum": "" }, "require": { @@ -13575,7 +13584,7 @@ "type": "github" } ], - "time": "2025-05-21T20:55:28+00:00" + "time": "2025-07-17T17:22:31+00:00" }, { "name": "phpunit/php-code-coverage", @@ -14015,21 +14024,21 @@ }, { "name": "rector/rector", - "version": "2.0.18", + "version": "2.1.2", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "be3a452085b524a04056e3dfe72d861948711062" + "reference": "40a71441dd73fa150a66102f5ca1364c44fc8fff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/be3a452085b524a04056e3dfe72d861948711062", - "reference": "be3a452085b524a04056e3dfe72d861948711062", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/40a71441dd73fa150a66102f5ca1364c44fc8fff", + "reference": "40a71441dd73fa150a66102f5ca1364c44fc8fff", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.17" + "phpstan/phpstan": "^2.1.18" }, "conflict": { "rector/rector-doctrine": "*", @@ -14054,6 +14063,7 @@ "MIT" ], "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", "keywords": [ "automation", "dev", @@ -14062,7 +14072,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.0.18" + "source": "https://github.com/rectorphp/rector/tree/2.1.2" }, "funding": [ { @@ -14070,7 +14080,7 @@ "type": "github" } ], - "time": "2025-06-11T11:19:37+00:00" + "time": "2025-07-17T19:30:06+00:00" }, { "name": "sebastian/cli-parser", @@ -15426,16 +15436,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "57e4fb86314015a695a750ace358d07a7e37b8a9" + "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/57e4fb86314015a695a750ace358d07a7e37b8a9", - "reference": "57e4fb86314015a695a750ace358d07a7e37b8a9", + "url": "https://api.github.com/repos/symfony/http-client/zipball/4403d87a2c16f33345dca93407a8714ee8c05a64", + "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64", "shasum": "" }, "require": { @@ -15447,6 +15457,7 @@ }, "conflict": { "amphp/amp": "<2.5", + "amphp/socket": "<1.1", "php-http/discovery": "<1.15", "symfony/http-foundation": "<6.4" }, @@ -15459,7 +15470,6 @@ "require-dev": { "amphp/http-client": "^4.2.1|^5.0", "amphp/http-tunnel": "^1.0|^2.0", - "amphp/socket": "^1.1", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", @@ -15501,7 +15511,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.0" + "source": "https://github.com/symfony/http-client/tree/v7.3.1" }, "funding": [ { @@ -15517,7 +15527,7 @@ "type": "tidelift" } ], - "time": "2025-05-02T08:23:16+00:00" + "time": "2025-06-28T07:58:39+00:00" }, { "name": "symfony/http-client-contracts", diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json index 1c329e47f..49907cbd4 100644 --- a/docker/coolify-realtime/package-lock.json +++ b/docker/coolify-realtime/package-lock.json @@ -181,14 +181,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -323,9 +324,9 @@ } }, "node_modules/nan": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.1.tgz", - "integrity": "sha512-pfRR4ZcNTSm2ZFHaztuvbICf+hyiG6ecA06SfAxoPmuHjvMu0KUIae7Y8GyVkbBqeEIidsmXeYooWIX9+qjfRQ==", + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", "license": "MIT" }, "node_modules/node-pty": { diff --git a/package-lock.json b/package-lock.json index d86caea87..10489a7d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,13 +74,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -90,9 +90,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -104,9 +104,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", "cpu": [ "ppc64" ], @@ -121,9 +121,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", "cpu": [ "arm" ], @@ -138,9 +138,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", "cpu": [ "arm64" ], @@ -155,9 +155,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", "cpu": [ "x64" ], @@ -172,9 +172,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", "cpu": [ "arm64" ], @@ -189,9 +189,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", "cpu": [ "x64" ], @@ -206,9 +206,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", "cpu": [ "arm64" ], @@ -223,9 +223,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", "cpu": [ "x64" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", "cpu": [ "arm" ], @@ -257,9 +257,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", "cpu": [ "arm64" ], @@ -274,9 +274,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", "cpu": [ "ia32" ], @@ -291,9 +291,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", "cpu": [ "loong64" ], @@ -308,9 +308,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", "cpu": [ "mips64el" ], @@ -325,9 +325,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", "cpu": [ "ppc64" ], @@ -342,9 +342,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", "cpu": [ "riscv64" ], @@ -359,9 +359,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", "cpu": [ "s390x" ], @@ -376,9 +376,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", "cpu": [ "x64" ], @@ -393,9 +393,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", "cpu": [ "arm64" ], @@ -410,9 +410,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", "cpu": [ "x64" ], @@ -427,9 +427,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", "cpu": [ "arm64" ], @@ -444,9 +444,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", "cpu": [ "x64" ], @@ -460,10 +460,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", "cpu": [ "x64" ], @@ -478,9 +495,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", "cpu": [ "arm64" ], @@ -495,9 +512,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", "cpu": [ "ia32" ], @@ -512,9 +529,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", "cpu": [ "x64" ], @@ -548,18 +565,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -572,27 +585,17 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -601,9 +604,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", "cpu": [ "arm" ], @@ -615,9 +618,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", "cpu": [ "arm64" ], @@ -629,9 +632,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", "cpu": [ "arm64" ], @@ -643,9 +646,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", "cpu": [ "x64" ], @@ -657,9 +660,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", "cpu": [ "arm64" ], @@ -671,9 +674,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", "cpu": [ "x64" ], @@ -685,9 +688,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", "cpu": [ "arm" ], @@ -699,9 +702,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", "cpu": [ "arm" ], @@ -713,9 +716,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", "cpu": [ "arm64" ], @@ -727,9 +730,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", "cpu": [ "arm64" ], @@ -741,9 +744,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", "cpu": [ "loong64" ], @@ -755,9 +758,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", "cpu": [ "ppc64" ], @@ -769,9 +772,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", "cpu": [ "riscv64" ], @@ -783,9 +786,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", "cpu": [ "riscv64" ], @@ -797,9 +800,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", "cpu": [ "s390x" ], @@ -811,9 +814,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", "cpu": [ "x64" ], @@ -825,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", "cpu": [ "x64" ], @@ -839,9 +842,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", "cpu": [ "arm64" ], @@ -853,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", "cpu": [ "ia32" ], @@ -867,9 +870,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", "cpu": [ "x64" ], @@ -1192,9 +1195,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -1544,9 +1547,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1620,9 +1623,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1633,31 +1636,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" } }, "node_modules/estree-walker": { @@ -1668,9 +1672,9 @@ "license": "MIT" }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1704,15 +1708,16 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -2306,9 +2311,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -2424,13 +2429,13 @@ } }, "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2440,26 +2445,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" } }, @@ -2600,9 +2605,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { From 6ddca603e2ad2d7865ecb8ce8ed6c75a95382f13 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:25:01 +0200 Subject: [PATCH 0062/1717] chore(deps): update docker deps - update Nixpacks to v1.39.0 --- docker/coolify-helper/Dockerfile | 10 +++++----- docker/coolify-realtime/Dockerfile | 2 +- docker/development/Dockerfile | 4 ++-- docker/production/Dockerfile | 4 ++-- docker/testing-host/Dockerfile | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index b62469cef..8c7073519 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -4,15 +4,15 @@ ARG BASE_IMAGE=alpine:3.21 # https://download.docker.com/linux/static/stable/ ARG DOCKER_VERSION=28.0.0 # https://github.com/docker/compose/releases -ARG DOCKER_COMPOSE_VERSION=2.34.0 +ARG DOCKER_COMPOSE_VERSION=2.38.2 # https://github.com/docker/buildx/releases -ARG DOCKER_BUILDX_VERSION=0.22.0 +ARG DOCKER_BUILDX_VERSION=0.25.0 # https://github.com/buildpacks/pack/releases -ARG PACK_VERSION=0.37.0 +ARG PACK_VERSION=0.38.2 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.34.1 +ARG NIXPACKS_VERSION=1.39.0 # https://github.com/minio/mc/releases -ARG MINIO_VERSION=RELEASE.2025-03-12T17-29-24Z +ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z FROM minio/mc:${MINIO_VERSION} AS minio-client diff --git a/docker/coolify-realtime/Dockerfile b/docker/coolify-realtime/Dockerfile index 7a24200d6..18c2f9301 100644 --- a/docker/coolify-realtime/Dockerfile +++ b/docker/coolify-realtime/Dockerfile @@ -2,7 +2,7 @@ # https://github.com/soketi/soketi/releases ARG SOKETI_VERSION=1.6-16-alpine # https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION=2025.5.0 +ARG CLOUDFLARED_VERSION=2025.7.0 FROM quay.io/soketi/soketi:${SOKETI_VERSION} diff --git a/docker/development/Dockerfile b/docker/development/Dockerfile index 8c5beec07..85cce14d7 100644 --- a/docker/development/Dockerfile +++ b/docker/development/Dockerfile @@ -2,9 +2,9 @@ # https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine # https://github.com/minio/mc/releases -ARG MINIO_VERSION=RELEASE.2025-03-12T17-29-24Z +ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z # https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION=2025.2.0 +ARG CLOUDFLARED_VERSION=2025.7.0 # https://www.postgresql.org/support/versioning/ ARG POSTGRES_VERSION=15 diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 5633170e3..a2a4b5fa3 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -2,9 +2,9 @@ # https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine # https://github.com/minio/mc/releases -ARG MINIO_VERSION=RELEASE.2025-03-12T17-29-24Z +ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z # https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION=2025.2.0 +ARG CLOUDFLARED_VERSION=2025.7.0 # https://www.postgresql.org/support/versioning/ ARG POSTGRES_VERSION=15 diff --git a/docker/testing-host/Dockerfile b/docker/testing-host/Dockerfile index b19d0875c..fdad3cc41 100644 --- a/docker/testing-host/Dockerfile +++ b/docker/testing-host/Dockerfile @@ -2,9 +2,9 @@ # https://download.docker.com/linux/static/stable/ ARG DOCKER_VERSION=28.0.0 # https://github.com/docker/compose/releases -ARG DOCKER_COMPOSE_VERSION=2.34.0 +ARG DOCKER_COMPOSE_VERSION=2.38.2 # https://github.com/docker/buildx/releases -ARG DOCKER_BUILDX_VERSION=0.22.0 +ARG DOCKER_BUILDX_VERSION=0.25.0 FROM debian:12-slim From d4fcafe4d3b0f2123d107826bedb9eaf25d9e2f6 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:37:58 +0200 Subject: [PATCH 0063/1717] fix(installer): public IPv4 link does not work --- other/nightly/install.sh | 7 ++++++- scripts/install.sh | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/other/nightly/install.sh b/other/nightly/install.sh index e9f54952a..92ad12302 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -253,6 +253,11 @@ if [ "$OS_TYPE" = "endeavouros" ]; then OS_TYPE="arch" fi +# Check if the OS is Cachy OS, if so, change it to arch +if [ "$OS_TYPE" = "cachyos" ]; then + OS_TYPE="arch" +fi + # Check if the OS is Asahi Linux, if so, change it to fedora if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then OS_TYPE="fedora" @@ -844,7 +849,7 @@ IPV6_PUBLIC_IP=$(curl -6s https://ifconfig.io || true) echo -e "\nYour instance is ready to use!\n" if [ -n "$IPV4_PUBLIC_IP" ]; then - echo -e "You can access Coolify through your Public IPV4: http://$(curl -4s https://ifconfig.io):8000" + echo -e "You can access Coolify through your Public IPV4: http://$IPV4_PUBLIC_IP:8000" fi if [ -n "$IPV6_PUBLIC_IP" ]; then echo -e "You can access Coolify through your Public IPv6: http://[$IPV6_PUBLIC_IP]:8000" diff --git a/scripts/install.sh b/scripts/install.sh index 48bbbb9f9..064fc7e4d 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -849,7 +849,7 @@ IPV6_PUBLIC_IP=$(curl -6s https://ifconfig.io || true) echo -e "\nYour instance is ready to use!\n" if [ -n "$IPV4_PUBLIC_IP" ]; then - echo -e "You can access Coolify through your Public IPV4: http://[$IPV4_PUBLIC_IP]:8000" + echo -e "You can access Coolify through your Public IPV4: http://$IPV4_PUBLIC_IP:8000" fi if [ -n "$IPV6_PUBLIC_IP" ]; then echo -e "You can access Coolify through your Public IPv6: http://[$IPV6_PUBLIC_IP]:8000" From d75eb49f0135a28d3f11f2c17b7c050c0535b12f Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:39:50 +0200 Subject: [PATCH 0064/1717] fix(composer): version constraint of prompts --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7e8e768c0..4557f1d80 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "laravel/framework": "^12.20.0", "laravel/horizon": "^5.33.1", "laravel/pail": "^1.2.3", - "laravel/prompts": "^0.3.6|^0.3.6|^0.3.6", + "laravel/prompts": "^0.3.6", "laravel/sanctum": "^4.1.2", "laravel/socialite": "^5.21.0", "laravel/tinker": "^2.10.1", From 8a0d5ab8c8fc24501fdf5d122651ece851392f9f Mon Sep 17 00:00:00 2001 From: Alfred Nutile <365385+alnutile@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:44:40 -0400 Subject: [PATCH 0065/1717] fix(service): Budibase secret keys (#6205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🏔️ Peak <122374094+peaklabs-dev@users.noreply.github.com> --- templates/compose/budibase.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/compose/budibase.yaml b/templates/compose/budibase.yaml index a5ecb8fbb..1364e7137 100644 --- a/templates/compose/budibase.yaml +++ b/templates/compose/budibase.yaml @@ -17,8 +17,9 @@ services: - INTERNAL_API_KEY=$SERVICE_BASE64_128_BUDIBASE - BUDIBASE_ENVIRONMENT=${BUDIBASE_ENVIRONMENT:-PRODUCTION} - PORT=4002 - - API_ENCRYPTION_KEY=$SERVICE_BASE64_64_BUDIBASE - - JWT_SECRET=$SERVICE_BASE64_64_BUDIBASE + - API_ENCRYPTION_KEY=$SERVICE_BASE64_64_BUDIBASEAPI + - ENCRYPTION_KEY=$SERVICE_BASE64_64_BUDIBASE + - JWT_SECRET=$SERVICE_BASE64_64_BUDIBASEJWT - LOG_LEVEL=info - ENABLE_ANALYTICS=${ENABLE_ANALYTICS:-true} - REDIS_URL=redis-service:6379 From 40407bae6889d67b06b4cb4a60e79431bbba63fc Mon Sep 17 00:00:00 2001 From: Katja Lutz Date: Fri, 18 Jul 2025 16:55:26 +0200 Subject: [PATCH 0066/1717] chore(service): update Nitropage template (#6181) - Use new registry - Updated the DATABASE_URL --- templates/compose/nitropage-with-postgresql.yaml | 2 +- templates/compose/nitropage.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/compose/nitropage-with-postgresql.yaml b/templates/compose/nitropage-with-postgresql.yaml index 4cea4d875..da515755c 100644 --- a/templates/compose/nitropage-with-postgresql.yaml +++ b/templates/compose/nitropage-with-postgresql.yaml @@ -6,7 +6,7 @@ services: nitropage: - image: codeberg.org/nitropage/nitropage + image: nitropage/nitropage environment: - SERVICE_FQDN_NITROPAGE_3000 - NP_AUTH_SALT=${SERVICE_BASE64_SALT} diff --git a/templates/compose/nitropage.yaml b/templates/compose/nitropage.yaml index bcc58de5f..2025c0b6e 100644 --- a/templates/compose/nitropage.yaml +++ b/templates/compose/nitropage.yaml @@ -6,12 +6,12 @@ services: nitropage: - image: codeberg.org/nitropage/nitropage:sqlite + image: nitropage/nitropage:sqlite environment: - SERVICE_FQDN_NITROPAGE_3000 - NP_AUTH_SALT=${SERVICE_BASE64_SALT} - NP_AUTH_PASSWORD=${SERVICE_PASSWORD_64_SESSION} - - DATABASE_URL=file:../../.data/dev.db + - DATABASE_URL=file:../.data/dev.db volumes: - nitropage-data:/app/.data healthcheck: From 7c85b80703e64a69b04f09eacce8638f16153e12 Mon Sep 17 00:00:00 2001 From: Nicanor Alexander de la Cruz Caba Date: Fri, 18 Jul 2025 11:03:40 -0400 Subject: [PATCH 0067/1717] feat(service): enable password protection for the Wireguard Ul --- templates/compose/wireguard-easy.yaml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/templates/compose/wireguard-easy.yaml b/templates/compose/wireguard-easy.yaml index 7ccf60554..311ca88d1 100644 --- a/templates/compose/wireguard-easy.yaml +++ b/templates/compose/wireguard-easy.yaml @@ -8,11 +8,11 @@ services: wg-easy: image: ghcr.io/wg-easy/wg-easy:latest environment: - - SERVICE_FQDN_WIREGUARDEASY_8000 + - SERVICE_FQDN_WIREGUARDEASY_51821 - WG_HOST=${SERVICE_FQDN_WIREGUARDEASY} - LANG=${LANG:-en} - - PORT=8000 - WG_PORT=51820 + - _PASSWORD=${SERVICE_PASSWORD_ADMIN} volumes: - wg-easy:/etc/wireguard ports: @@ -23,3 +23,9 @@ services: sysctls: - net.ipv4.conf.all.src_valid_mark=1 - net.ipv4.ip_forward=1 + entrypoint: + - /bin/bash + - -c + - | + eval "wgpw '${SERVICE_PASSWORD_ADMIN}' > /pass-hash" + eval "$(cat /pass-hash) dumb-init node server.js" From 675a711664e47c2fecdde4e54abec813491cc403 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 17:10:01 +0200 Subject: [PATCH 0068/1717] fix(service): wg-easy host should be just the FQDN --- templates/compose/wireguard-easy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/wireguard-easy.yaml b/templates/compose/wireguard-easy.yaml index 311ca88d1..6d5451c2d 100644 --- a/templates/compose/wireguard-easy.yaml +++ b/templates/compose/wireguard-easy.yaml @@ -9,7 +9,7 @@ services: image: ghcr.io/wg-easy/wg-easy:latest environment: - SERVICE_FQDN_WIREGUARDEASY_51821 - - WG_HOST=${SERVICE_FQDN_WIREGUARDEASY} + - WG_HOST=${SERVICE_URL_WIREGUARDEASY} - LANG=${LANG:-en} - WG_PORT=51820 - _PASSWORD=${SERVICE_PASSWORD_ADMIN} From e287b86759666af37a92f8da491e691668f5b308 Mon Sep 17 00:00:00 2001 From: Rachit Khurana <70265590+notnotrachit@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:45:18 +0530 Subject: [PATCH 0069/1717] fix(ui): search box overlaps the sidebar navigation (#6176) --- resources/views/livewire/project/new/select.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index 5c1e3c6f7..eae754e56 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -12,7 +12,7 @@
Deploy resources, like Applications, Databases, Services...
@if ($current_step === 'type') -
+
From 5fd8fec172f119396d8fc61f210b1bdd1c7fd1e6 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:57:43 +0200 Subject: [PATCH 0070/1717] chore(versions): update all version --- config/constants.php | 6 +++--- versions.json | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/config/constants.php b/config/constants.php index ae984aa18..c7a36d311 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,9 +2,9 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.420.6', - 'helper_version' => '1.0.8', - 'realtime_version' => '1.0.9', + 'version' => '4.0.0-beta.420.7', + 'helper_version' => '1.0.9', + 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/versions.json b/versions.json index 7b12bd23e..03f9a97e2 100644 --- a/versions.json +++ b/versions.json @@ -1,16 +1,16 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.420.6" - }, - "nightly": { "version": "4.0.0-beta.420.7" }, + "nightly": { + "version": "4.0.0-beta.420.8" + }, "helper": { - "version": "1.0.8" + "version": "1.0.9" }, "realtime": { - "version": "1.0.9" + "version": "1.0.10" }, "sentinel": { "version": "0.0.15" From 00225a9eff75505700384d76104d4b56bf3b128f Mon Sep 17 00:00:00 2001 From: Rado Date: Fri, 18 Jul 2025 19:04:08 +0200 Subject: [PATCH 0071/1717] fix(webhooks): exclude webhook routes from CSRF protection (#6200) --- app/Http/Middleware/VerifyCsrfToken.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 9e8652172..f07050d5e 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -12,6 +12,6 @@ class VerifyCsrfToken extends Middleware * @var array */ protected $except = [ - // + 'webhooks/*', ]; } From d736b3e8b79a6c6ee4fede5a2639dec5f10e1a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8F=94=EF=B8=8F=20Peak?= <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:17:28 +0200 Subject: [PATCH 0072/1717] feat(queues): improve Horizon config to reduce CPU and RAM usage (#6212) --- config/horizon.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/config/horizon.php b/config/horizon.php index 9dd80b8e4..cdabcb1e8 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -182,14 +182,15 @@ 'defaults' => [ 's6' => [ 'connection' => 'redis', - 'queue' => explode(',', env('HORIZON_QUEUES', 'high,default')), - 'balance' => env('HORIZON_BALANCE', 'auto'), - 'maxTime' => 0, - 'maxJobs' => 0, + 'balance' => env('HORIZON_BALANCE', 'false'), + 'queue' => env('HORIZON_QUEUES', 'high,default'), + 'maxTime' => 3600, + 'maxJobs' => 400, 'memory' => 128, 'tries' => 1, - 'timeout' => 3560, 'nice' => 0, + 'sleep' => 3, + 'timeout' => 3600, ], ], @@ -198,7 +199,7 @@ 's6' => [ 'autoScalingStrategy' => 'size', 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), - 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), + 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 4), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), ], @@ -208,7 +209,7 @@ 's6' => [ 'autoScalingStrategy' => 'size', 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), - 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), + 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 4), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), ], From 19a56170882fbcb75575c672b7c3c7497da3dd30 Mon Sep 17 00:00:00 2001 From: Ficky Irwanto Date: Sat, 19 Jul 2025 00:49:27 +0700 Subject: [PATCH 0073/1717] feat(service): add Gowa service (#6164) --- public/svgs/gowa.svg | 1 + templates/compose/gowa.yaml | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 public/svgs/gowa.svg create mode 100644 templates/compose/gowa.yaml diff --git a/public/svgs/gowa.svg b/public/svgs/gowa.svg new file mode 100644 index 000000000..1121b05bc --- /dev/null +++ b/public/svgs/gowa.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/compose/gowa.yaml b/templates/compose/gowa.yaml new file mode 100644 index 000000000..832c368ad --- /dev/null +++ b/templates/compose/gowa.yaml @@ -0,0 +1,25 @@ +# documentation: https://github.com/aldinokemal/go-whatsapp-web-multidevice +# slogan: Golang WhatsApp - Built with Go for efficient memory use +# tags: whatsapp,golang,multidevice,api,go-whatsapp +# logo: svgs/gowa.svg +# port: 3000 + +services: + gowa: + image: aldinokemal2104/go-whatsapp-web-multidevice + container_name: gowa + restart: always + volumes: + - 'gowa:/app/storages' + environment: + - SERVICE_FQDN_GOWA_3000 + - SERVICE_USER_GOWA + - SERVICE_PASSWORD_GOWA + - 'APP_BASIC_AUTH=${SERVICE_USER_GOWA}:${SERVICE_PASSWORD_GOWA}' + - APP_PORT=${APP_PORT:-3000} + - APP_DEBUG=${APP_DEBUG:-false} + - APP_ACCOUNT_VALIDATION=${APP_ACCOUNT_VALIDATION:-false} + - 'WHATSAPP_WEBHOOK=${WHATSAPP_WEBHOOK}' + - 'WHATSAPP_WEBHOOK_SECRET=${WHATSAPP_WEBHOOK_SECRET}' +volumes: + gowa: null From cacb7700de6a59480938c825f42a9cb5442f9d63 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:54:05 +0200 Subject: [PATCH 0074/1717] refactor(service): improve gowa - remove name and restart - hardcode port - formatting --- templates/compose/gowa.yaml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/templates/compose/gowa.yaml b/templates/compose/gowa.yaml index 832c368ad..9de7b49fe 100644 --- a/templates/compose/gowa.yaml +++ b/templates/compose/gowa.yaml @@ -6,20 +6,16 @@ services: gowa: - image: aldinokemal2104/go-whatsapp-web-multidevice - container_name: gowa - restart: always - volumes: - - 'gowa:/app/storages' + image: aldinokemal2104/go-whatsapp-web-multidevice:latest environment: - SERVICE_FQDN_GOWA_3000 - SERVICE_USER_GOWA - SERVICE_PASSWORD_GOWA - - 'APP_BASIC_AUTH=${SERVICE_USER_GOWA}:${SERVICE_PASSWORD_GOWA}' - - APP_PORT=${APP_PORT:-3000} + - APP_BASIC_AUTH=${SERVICE_USER_GOWA}:${SERVICE_PASSWORD_GOWA} + - APP_PORT=3000 - APP_DEBUG=${APP_DEBUG:-false} - APP_ACCOUNT_VALIDATION=${APP_ACCOUNT_VALIDATION:-false} - - 'WHATSAPP_WEBHOOK=${WHATSAPP_WEBHOOK}' - - 'WHATSAPP_WEBHOOK_SECRET=${WHATSAPP_WEBHOOK_SECRET}' -volumes: - gowa: null + - WHATSAPP_WEBHOOK=${WHATSAPP_WEBHOOK} + - WHATSAPP_WEBHOOK_SECRET=${WHATSAPP_WEBHOOK_SECRET} + volumes: + - gowa_data:/app/storages From 4df349f74d43ccccddc09db60c784f5289890ff2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 17 Jul 2025 09:31:55 +0200 Subject: [PATCH 0075/1717] refactor(previews): streamline preview domain generation logic in ApplicationDeploymentJob for improved clarity and maintainability --- app/Jobs/ApplicationDeploymentJob.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 67043555d..07d4ea9a0 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -229,16 +229,11 @@ public function __construct(public int $application_deployment_queue_id) // Set preview fqdn if ($this->pull_request_id !== 0) { - if ($this->application->build_pack === 'dockercompose') { - // For Docker Compose apps, use the preview model's compose-specific method - $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); - if ($this->preview) { + $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); + if ($this->preview) { + if ($this->application->build_pack === 'dockercompose') { $this->preview->generate_preview_fqdn_compose(); - } - } else { - // For non-Docker Compose apps, use the preview model's method - $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); - if ($this->preview) { + } else { $this->preview->generate_preview_fqdn(); } } From 9ed77e5eefbd5bfc77058e2de9e16a177b962d97 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:18:12 +0200 Subject: [PATCH 0076/1717] fix(services): update environment variable naming convention to use underscores instead of dashes for SERVICE_FQDN and SERVICE_URL --- bootstrap/helpers/services.php | 8 ++++---- bootstrap/helpers/shared.php | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index cd99713a2..1bddfede7 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -119,7 +119,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $resourceFqdns = str($resource->fqdn)->explode(','); if ($resourceFqdns->count() === 1) { $resourceFqdns = $resourceFqdns->first(); - $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_'); $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) ->where('resourceable_id', $resource->service_id) ->where('key', $variableName) @@ -151,7 +151,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $generatedEnv->save(); } } - $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_'); $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) ->where('resourceable_id', $resource->service_id) ->where('key', $variableName) @@ -243,7 +243,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $port_env_url->save(); } } else { - $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_'); $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) ->where('resourceable_id', $resource->service_id) ->where('key', $variableName) @@ -254,7 +254,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $generatedEnv->value = $fqdn; $generatedEnv->save(); } - $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_'); $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) ->where('resourceable_id', $resource->service_id) ->where('key', $variableName) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 00a674eeb..de6c954e0 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3080,11 +3080,8 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } } elseif ($isService) { if (blank($savedService->fqdn)) { - if ($fqdnFor) { - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - } else { - $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); - } + // For services, if no explicit FQDN is set, leave SERVICE_FQDN_ variables empty + $fqdn = ''; } else { $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); } @@ -3150,6 +3147,10 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($isApplication && $resource->build_pack === 'dockercompose') { continue; } + // For services, only generate FQDN if explicit FQDN is set + if ($isService && blank($savedService->fqdn)) { + continue; + } $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); if (str($fqdnFor)->contains('_')) { $fqdnFor = str($fqdnFor)->before('_'); @@ -3168,6 +3169,10 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($isApplication && $resource->build_pack === 'dockercompose') { continue; } + // For services, only generate URL if explicit FQDN is set + if ($isService && blank($savedService->fqdn)) { + continue; + } $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); if (str($fqdnFor)->contains('_')) { $fqdnFor = str($fqdnFor)->before('_'); @@ -3674,8 +3679,8 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $coolifyScheme = $coolifyUrl->getScheme(); $coolifyFqdn = $coolifyUrl->getHost(); $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); - $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper(), $coolifyUrl->__toString()); - $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper(), $coolifyFqdn); + $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyUrl->__toString()); + $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyFqdn); } } } From 5e693eb4b51a9f5d9e4e17afe8a11868eedb0a81 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:24:53 +0200 Subject: [PATCH 0077/1717] refactor(services): simplify environment variable updates by using updateOrCreate and add cleanup for removed FQDNs --- bootstrap/helpers/services.php | 91 ++++++++++++++++------------------ 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 1bddfede7..0adccd023 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -120,69 +120,53 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) if ($resourceFqdns->count() === 1) { $resourceFqdns = $resourceFqdns->first(); $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_'); - $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', $variableName) - ->first(); $fqdn = Url::fromString($resourceFqdns); $port = $fqdn->getPort(); $path = $fqdn->getPath(); $fqdn = $fqdn->getScheme().'://'.$fqdn->getHost(); - if ($generatedEnv) { - if ($path === '/') { - $generatedEnv->value = $fqdn; - } else { - $generatedEnv->value = $fqdn.$path; - } - $generatedEnv->save(); - } + $fqdnValue = ($path === '/') ? $fqdn : $fqdn.$path; + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $fqdnValue, + ]); if ($port) { $variableName = $variableName."_$port"; - $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', $variableName) - ->first(); - if ($generatedEnv) { - if ($path === '/') { - $generatedEnv->value = $fqdn; - } else { - $generatedEnv->value = $fqdn.$path; - } - $generatedEnv->save(); - } + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $fqdnValue, + ]); } $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_'); - $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', $variableName) - ->first(); $url = Url::fromString($fqdn); $port = $url->getPort(); $path = $url->getPath(); $url = $url->getHost(); - if ($generatedEnv) { - $url = str($fqdn)->after('://'); - if ($path === '/') { - $generatedEnv->value = $url; - } else { - $generatedEnv->value = $url.$path; - } - $generatedEnv->save(); + $urlValue = str($fqdn)->after('://'); + if ($path !== '/') { + $urlValue = $urlValue.$path; } + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $urlValue, + ]); if ($port) { $variableName = $variableName."_$port"; - $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', $variableName) - ->first(); - if ($generatedEnv) { - if ($path === '/') { - $generatedEnv->value = $url; - } else { - $generatedEnv->value = $url.$path; - } - $generatedEnv->save(); - } + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $urlValue, + ]); } } elseif ($resourceFqdns->count() > 1) { foreach ($resourceFqdns as $fqdn) { @@ -269,6 +253,17 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) } } } + } else { + // If FQDN is removed, delete the corresponding environment variables + $serviceName = str($resource->name)->upper()->replace('-', '_'); + EnvironmentVariable::where('resourceable_type', Service::class) + ->where('resourceable_id', $resource->service_id) + ->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%") + ->delete(); + EnvironmentVariable::where('resourceable_type', Service::class) + ->where('resourceable_id', $resource->service_id) + ->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%") + ->delete(); } } catch (\Throwable $e) { return handleError($e); From ef91e43384f5a05616b55e370c6ef710df467054 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:44:07 +0200 Subject: [PATCH 0078/1717] feat(container): add updatedSelectedContainer method to connect to non-default containers and update wire:model for improved reactivity --- app/Livewire/Project/Shared/ExecuteContainerCommand.php | 7 +++++++ .../project/shared/execute-container-command.blade.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index ca1597d4f..9003e093a 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -137,6 +137,13 @@ public function loadContainers() } } + public function updatedSelectedContainer() + { + if ($this->selected_container !== 'default') { + $this->connectToContainer(); + } + } + #[On('connectToServer')] public function connectToServer() { diff --git a/resources/views/livewire/project/shared/execute-container-command.blade.php b/resources/views/livewire/project/shared/execute-container-command.blade.php index c04af1dd5..71d253103 100644 --- a/resources/views/livewire/project/shared/execute-container-command.blade.php +++ b/resources/views/livewire/project/shared/execute-container-command.blade.php @@ -21,7 +21,7 @@
No containers are running or terminal access is disabled on this server.
@else
- + @foreach ($containers as $container) @if ($loop->first) From 90ccaeba51f6bb1b260baa83dd67184205ba72c3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:48:51 +0200 Subject: [PATCH 0079/1717] feat(application): implement environment variable updates for Docker Compose applications, including creation, updating, and deletion of SERVICE_FQDN and SERVICE_URL variables --- app/Livewire/Project/Application/General.php | 88 ++++++++++++++++++++ bootstrap/helpers/services.php | 8 ++ 2 files changed, 96 insertions(+) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 3e89ad663..ac2e6794d 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -4,6 +4,7 @@ use App\Actions\Application\GenerateConfig; use App\Models\Application; +use App\Models\EnvironmentVariable; use Illuminate\Support\Collection; use Livewire\Component; use Spatie\Url\Url; @@ -488,6 +489,12 @@ public function submit($showToaster = true) } $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); + + // Update SERVICE_FQDN_ and SERVICE_URL_ environment variables for Docker Compose applications + if ($this->application->build_pack === 'dockercompose') { + $this->updateServiceEnvironmentVariables(); + } + $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!'); } catch (\Throwable $e) { $originalFqdn = $this->application->getOriginal('fqdn'); @@ -513,4 +520,85 @@ public function downloadConfig() 'Content-Disposition' => 'attachment; filename='.$fileName, ]); } + + private function updateServiceEnvironmentVariables() + { + $domains = collect(json_decode($this->application->docker_compose_domains, true)) ?? collect([]); + + foreach ($domains as $serviceName => $service) { + $serviceNameFormatted = str($serviceName)->upper()->replace('-', '_'); + $domain = data_get($service, 'domain'); + + if ($domain) { + // Create or update SERVICE_FQDN_ and SERVICE_URL_ variables + $fqdn = Url::fromString($domain); + $port = $fqdn->getPort(); + $path = $fqdn->getPath(); + $fqdnValue = $fqdn->getScheme().'://'.$fqdn->getHost(); + if ($path !== '/') { + $fqdnValue = $fqdnValue.$path; + } + $urlValue = str($domain)->after('://'); + if ($path !== '/') { + $urlValue = $urlValue.$path; + } + + // Create/update SERVICE_FQDN_ + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Application::class, + 'resourceable_id' => $this->application->id, + 'key' => "SERVICE_FQDN_{$serviceNameFormatted}", + ], [ + 'value' => $fqdnValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + + // Create/update SERVICE_URL_ + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Application::class, + 'resourceable_id' => $this->application->id, + 'key' => "SERVICE_URL_{$serviceNameFormatted}", + ], [ + 'value' => $urlValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + + // Create/update port-specific variables if port exists + if ($port) { + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Application::class, + 'resourceable_id' => $this->application->id, + 'key' => "SERVICE_FQDN_{$serviceNameFormatted}_{$port}", + ], [ + 'value' => $fqdnValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Application::class, + 'resourceable_id' => $this->application->id, + 'key' => "SERVICE_URL_{$serviceNameFormatted}_{$port}", + ], [ + 'value' => $urlValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } else { + // Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed + EnvironmentVariable::where('resourceable_type', Application::class) + ->where('resourceable_id', $this->application->id) + ->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%") + ->delete(); + + EnvironmentVariable::where('resourceable_type', Application::class) + ->where('resourceable_id', $this->application->id) + ->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%") + ->delete(); + } + } + } } diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 0adccd023..1e1d2a073 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -131,6 +131,8 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) 'key' => $variableName, ], [ 'value' => $fqdnValue, + 'is_build_time' => false, + 'is_preview' => false, ]); if ($port) { $variableName = $variableName."_$port"; @@ -140,6 +142,8 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) 'key' => $variableName, ], [ 'value' => $fqdnValue, + 'is_build_time' => false, + 'is_preview' => false, ]); } $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_'); @@ -157,6 +161,8 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) 'key' => $variableName, ], [ 'value' => $urlValue, + 'is_build_time' => false, + 'is_preview' => false, ]); if ($port) { $variableName = $variableName."_$port"; @@ -166,6 +172,8 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) 'key' => $variableName, ], [ 'value' => $urlValue, + 'is_build_time' => false, + 'is_preview' => false, ]); } } elseif ($resourceFqdns->count() > 1) { From 8f920ab4e15769c2d08e25e8bb8389c3b9186d8c Mon Sep 17 00:00:00 2001 From: CrazyTim71 <118295691+CrazyTim71@users.noreply.github.com> Date: Fri, 18 Jul 2025 21:26:23 +0200 Subject: [PATCH 0080/1717] feat(service): add TriliumNext service (#5970) --- public/svgs/triliumnext.svg | 28 ++++++++++++++++++++++++++++ templates/compose/triliumnext.yaml | 23 +++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 public/svgs/triliumnext.svg create mode 100644 templates/compose/triliumnext.yaml diff --git a/public/svgs/triliumnext.svg b/public/svgs/triliumnext.svg new file mode 100644 index 000000000..173712891 --- /dev/null +++ b/public/svgs/triliumnext.svg @@ -0,0 +1,28 @@ + + + TriliumNext Notes + + + + + + + + + + + + + + + diff --git a/templates/compose/triliumnext.yaml b/templates/compose/triliumnext.yaml new file mode 100644 index 000000000..a87a766b5 --- /dev/null +++ b/templates/compose/triliumnext.yaml @@ -0,0 +1,23 @@ +# documentation: https://github.com/TriliumNext/Notes +# slogan: Build your personal knowledge base with TriliumNext Notes. +# tags: self-hosted, notes, todo, organize, markdown, wiki +# logo: svgs/triliumnext.svg +# port: 8080 + +services: + triliumnext: + image: triliumnext/notes:latest + environment: + - SERVICE_FQDN_TRILIUMNEXT_8080 + - TZ=${TZ:-Europe/Berlin} + volumes: + - triliumnext_data:/home/node/trilium-data + healthcheck: + test: + [ + "CMD-SHELL", + "wget --quiet --tries=1 --spider http://127.0.0.1:8080/api/health-check || exit 1", + ] + interval: 5s + timeout: 20s + retries: 10 From f14858ebeea344748e01406e8bcf4dc4ff2482d4 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 21:31:36 +0200 Subject: [PATCH 0081/1717] fix(service): triliumnext platform and link --- templates/compose/triliumnext.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/compose/triliumnext.yaml b/templates/compose/triliumnext.yaml index a87a766b5..1dd230473 100644 --- a/templates/compose/triliumnext.yaml +++ b/templates/compose/triliumnext.yaml @@ -1,4 +1,4 @@ -# documentation: https://github.com/TriliumNext/Notes +# documentation: https://github.com/TriliumNext/Trilium # slogan: Build your personal knowledge base with TriliumNext Notes. # tags: self-hosted, notes, todo, organize, markdown, wiki # logo: svgs/triliumnext.svg @@ -6,7 +6,8 @@ services: triliumnext: - image: triliumnext/notes:latest + image: ghcr.io/triliumnext/trilium:stable + platform: linux/amd64 # https://github.com/TriliumNext/Trilium/issues/6390 environment: - SERVICE_FQDN_TRILIUMNEXT_8080 - TZ=${TZ:-Europe/Berlin} From fea75443000db8f5d778759cec3c6836742e68a7 Mon Sep 17 00:00:00 2001 From: Nicanor Alexander de la Cruz Caba Date: Fri, 18 Jul 2025 15:39:11 -0400 Subject: [PATCH 0082/1717] feat(service): add Matrix service (#6029) --- public/svgs/matrix.svg | 7 ++ templates/compose/matrix.yaml | 130 ++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 public/svgs/matrix.svg create mode 100644 templates/compose/matrix.yaml diff --git a/public/svgs/matrix.svg b/public/svgs/matrix.svg new file mode 100644 index 000000000..bc41720a2 --- /dev/null +++ b/public/svgs/matrix.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/templates/compose/matrix.yaml b/templates/compose/matrix.yaml new file mode 100644 index 000000000..372ba57df --- /dev/null +++ b/templates/compose/matrix.yaml @@ -0,0 +1,130 @@ +# documentation: https://matrix.org/docs/chat_basics/matrix-for-im/ +# slogan: Chat securely with your family, friends, community, or build great apps with Matrix! +# tags: chat,slack,discord,voip,video,call +# logo: svgs/matrix.svg + +services: + matrix: + image: matrixdotorg/synapse:latest + volumes: + - ./data:/data + environment: + - SERVICE_FQDN_MATRIX_8008 + - SYNAPSE_SERVER_NAME=${SERVICE_URL_MATRIX} + - SYNAPSE_REPORT_STATS=no + - ENABLE_REGISTRATION=${ENABLE_REGISTRATION} + - RECAPTCHA_PUBLIC_KEY=${RECAPTCHA_PUBLIC_KEY} + - RECAPTCHA_PRIVATE_KEY=${RECAPTCHA_PRIVATE_KEY} + - _SERVER_NAME=${SERVICE_URL_MATRIX} + - _ADMIN_NAME=${SERVICE_USER_ADMIN} + - _ADMIN_PASS=${SERVICE_PASSWORD_ADMIN} + entrypoint: + - /bin/bash + - -c + - | + ! test -f /data/homeserver.yaml && /start.py generate + + # registration_shared_secret + grep "registration_shared_secret" /data/homeserver.yaml \ + | awk '{print $2}' > ./registration_shared_secret + + # macaroon_secret_key + grep "macaroon_secret_key" /data/homeserver.yaml \ + | awk '{print $2}' > ./macaroon_secret_key + + # form_secret + grep "form_secret" /data/homeserver.yaml \ + | awk '{print $2}' > ./form_secret + + ########################## + # # + # homeserver.yaml: start # + # # + ########################## + cat < /data/homeserver.yaml + server_name: "${SERVICE_URL_MATRIX}" + pid_file: /data/homeserver.pid + + # server + listeners: + - port: 8008 + tls: false + type: http + x_forwarded: true + resources: + - names: [client, federation] + compress: false + + # database + database: + name: sqlite3 + args: + database: /data/homeserver.db + + # general + log_config: "/data/${SERVICE_URL_MATRIX}.log.config" + media_store_path: /data/media_store + report_stats: false + + # secrets + registration_shared_secret: $(<./registration_shared_secret) + macaroon_secret_key: $(<./macaroon_secret_key) + form_secret: $(<./form_secret) + signing_key_path: "/data/${SERVICE_URL_MATRIX}.signing.key" + + #rooms + auto_join_rooms: + - "#general:${SERVICE_URL_MATRIX}" + + # federation + trusted_key_servers: + - server_name: "matrix.org" + autocreate_auto_join_rooms_federated: false + allow_public_rooms_over_federation: false + EOF + ######################## + # # + # homeserver.yaml: end # + # # + ######################## + + test -n "${ENABLE_REGISTRATION}" && ! grep "#registration" /data/homeserver.yaml &>/dev/null \ + && echo >> /data/homeserver.yaml \ + && cat <> /data/homeserver.yaml + #registration + enable_registration: true # Allows users to register on your server. + EOF + + ! grep ${RECAPTCHA_PUBLIC_KEY} /data/homeserver.yaml &>/dev/null \ + && echo >> /data/homeserver.yaml \ + && cat <> /data/homeserver.yaml + # reCAPTCHA settings + enable_registration_captcha: true # Enables CAPTCHA for registrations. + recaptcha_public_key: "${RECAPTCHA_PUBLIC_KEY}" + recaptcha_private_key: "${RECAPTCHA_PRIVATE_KEY}" + recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify" + EOF + + register_admin(){ + while ! curl -I localhost:8008 &>/dev/null; do + sleep 1 + done + register_new_matrix_user \ + -a \ + -u ${SERVICE_USER_ADMIN} \ + -p ${SERVICE_PASSWORD_ADMIN} \ + -c /data/homeserver.yaml \ + http://localhost:8008 &>/dev/null + } + register_admin & + + /start.py + healthcheck: + test: + - CMD + - curl + - -I + - localhost:8008 + interval: 5s + timeout: 3s + retries: 5 From 9292d3c223e7feaae6bf3c19cbf13ffb9f9c28a6 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 21:41:42 +0200 Subject: [PATCH 0083/1717] chore(service): improve matrix service - formatting and ENVs adjustment --- templates/compose/matrix.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/templates/compose/matrix.yaml b/templates/compose/matrix.yaml index 372ba57df..99232967e 100644 --- a/templates/compose/matrix.yaml +++ b/templates/compose/matrix.yaml @@ -2,22 +2,23 @@ # slogan: Chat securely with your family, friends, community, or build great apps with Matrix! # tags: chat,slack,discord,voip,video,call # logo: svgs/matrix.svg +# port: 8008 services: matrix: image: matrixdotorg/synapse:latest - volumes: - - ./data:/data environment: - SERVICE_FQDN_MATRIX_8008 - SYNAPSE_SERVER_NAME=${SERVICE_URL_MATRIX} - - SYNAPSE_REPORT_STATS=no - - ENABLE_REGISTRATION=${ENABLE_REGISTRATION} + - SYNAPSE_REPORT_STATS=${SYNAPSE_REPORT_STATS:-no} + - ENABLE_REGISTRATION=${ENABLE_REGISTRATION:-false} - RECAPTCHA_PUBLIC_KEY=${RECAPTCHA_PUBLIC_KEY} - RECAPTCHA_PRIVATE_KEY=${RECAPTCHA_PRIVATE_KEY} - _SERVER_NAME=${SERVICE_URL_MATRIX} - _ADMIN_NAME=${SERVICE_USER_ADMIN} - _ADMIN_PASS=${SERVICE_PASSWORD_ADMIN} + volumes: + - matrix-data:/data entrypoint: - /bin/bash - -c From 451aadbd7f7fc484722b7e2d68c362ad4ce20ba1 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Fri, 18 Jul 2025 15:54:39 -0400 Subject: [PATCH 0084/1717] feat(service): add GitHub Action runner service (#6209) --- public/svgs/github-runner.png | Bin 0 -> 13322 bytes templates/compose/github-runner.yml | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 public/svgs/github-runner.png create mode 100644 templates/compose/github-runner.yml diff --git a/public/svgs/github-runner.png b/public/svgs/github-runner.png new file mode 100644 index 0000000000000000000000000000000000000000..fb5b5c1b769af43cbab8638df0f2fd73f5af5e1d GIT binary patch literal 13322 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 literal 0 HcmV?d00001 diff --git a/templates/compose/github-runner.yml b/templates/compose/github-runner.yml new file mode 100644 index 000000000..1f175280b --- /dev/null +++ b/templates/compose/github-runner.yml @@ -0,0 +1,27 @@ +# documentation: https://github.com/myoung34/docker-github-actions-runner/wiki/Usage +# slogan: A GitHub Actions runner for Docker +# tags: github,actions,runner,docker +# logo: svgs/github-runner.png + +services: + runner: + image: 'myoung34/github-runner:latest' + environment: + - REPO_URL=${REPO_URL} + - RUNNER_NAME_PREFIX=${RUNNER_NAME_PREFIX:-coolify-runner} + - RUNNER_NAME_SUFFIX=${RUNNER_NAME_SUFFIX:-true} + - ACCESS_TOKEN=${ACCESS_TOKEN} + - RUNNER_WORKDIR=/tmp/runner/work + - RUNNER_SCOPE=${RUNNER_SCOPE:-repo} + - LABELS=${LABELS:-default} + - ORG_NAME=${ORG_NAME} + security_opt: + - 'label:disable' + volumes: + - '/var/run/docker.sock:/var/run/docker.sock' + - runner:/tmp/runner + healthcheck: + test: ["CMD-SHELL", "ps aux | grep '[R]unner' > /dev/null || exit 1"] + interval: 5s + timeout: 10s + retries: 15 From 948c50167d1f4682bdb210a934539690feb2c06e Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 21:57:34 +0200 Subject: [PATCH 0085/1717] chore(service): format runner service --- .../{github-runner.yml => github-runner.yaml} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename templates/compose/{github-runner.yml => github-runner.yaml} (73%) diff --git a/templates/compose/github-runner.yml b/templates/compose/github-runner.yaml similarity index 73% rename from templates/compose/github-runner.yml rename to templates/compose/github-runner.yaml index 1f175280b..75a144830 100644 --- a/templates/compose/github-runner.yml +++ b/templates/compose/github-runner.yaml @@ -5,21 +5,21 @@ services: runner: - image: 'myoung34/github-runner:latest' + image: myoung34/github-runner:latest environment: - REPO_URL=${REPO_URL} - RUNNER_NAME_PREFIX=${RUNNER_NAME_PREFIX:-coolify-runner} - - RUNNER_NAME_SUFFIX=${RUNNER_NAME_SUFFIX:-true} + - RUNNER_NAME_SUFFIX=${RUNNER_NAME_SUFFIX:-false} - ACCESS_TOKEN=${ACCESS_TOKEN} - - RUNNER_WORKDIR=/tmp/runner/work + - RUNNER_WORKDIR=/runner/work - RUNNER_SCOPE=${RUNNER_SCOPE:-repo} - LABELS=${LABELS:-default} - ORG_NAME=${ORG_NAME} security_opt: - - 'label:disable' + - label:disable volumes: - - '/var/run/docker.sock:/var/run/docker.sock' - - runner:/tmp/runner + - /var/run/docker.sock:/var/run/docker.sock + - runner-data:/runner healthcheck: test: ["CMD-SHELL", "ps aux | grep '[R]unner' > /dev/null || exit 1"] interval: 5s From fe900d3cd782237fba0a76f591c3cbd738243308 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Fri, 18 Jul 2025 22:36:38 +0200 Subject: [PATCH 0086/1717] Update service-templates.json --- templates/service-templates.json | 68 ++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/templates/service-templates.json b/templates/service-templates.json index d308acb7c..6aa4e1eb8 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -250,7 +250,7 @@ "budibase": { "documentation": "https://docs.budibase.com/docs/docker-compose?utm_source=coolify.io", "slogan": "Low code platform for building business apps and workflows in minutes. Supports PostgreSQL, MySQL, MSSQL, MongoDB, Rest API, Docker, K8s, and more.", - "compose": "c2VydmljZXM6CiAgYXBwLXNlcnZpY2U6CiAgICBpbWFnZTogYnVkaWJhc2UuZG9ja2VyLnNjYXJmLnNoL2J1ZGliYXNlL2FwcHMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFTEZfSE9TVEVEPTEKICAgICAgLSAnQ09VQ0hfREJfVVJMPWh0dHA6Ly8kU0VSVklDRV9VU0VSX0NPVUNIREI6JFNFUlZJQ0VfUEFTU1dPUkRfQ09VQ0hEQkBjb3VjaGRiLXNlcnZpY2U6NTk4NCcKICAgICAgLSAnV09SS0VSX1VSTD1odHRwOi8vd29ya2VyLXNlcnZpY2U6NDAwMycKICAgICAgLSAnTUlOSU9fVVJMPWh0dHA6Ly9taW5pby1zZXJ2aWNlOjkwMDAnCiAgICAgIC0gTUlOSU9fQUNDRVNTX0tFWT0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtIElOVEVSTkFMX0FQSV9LRVk9JFNFUlZJQ0VfQkFTRTY0XzEyOF9CVURJQkFTRQogICAgICAtICdCVURJQkFTRV9FTlZJUk9OTUVOVD0ke0JVRElCQVNFX0VOVklST05NRU5UOi1QUk9EVUNUSU9OfScKICAgICAgLSBQT1JUPTQwMDIKICAgICAgLSBBUElfRU5DUllQVElPTl9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0JVRElCQVNFCiAgICAgIC0gSldUX1NFQ1JFVD0kU0VSVklDRV9CQVNFNjRfNjRfQlVESUJBU0UKICAgICAgLSBMT0dfTEVWRUw9aW5mbwogICAgICAtICdFTkFCTEVfQU5BTFlUSUNTPSR7RU5BQkxFX0FOQUxZVElDUzotdHJ1ZX0nCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpcy1zZXJ2aWNlOjYzNzknCiAgICAgIC0gUkVESVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUkVESVMKICAgICAgLSBCQl9BRE1JTl9VU0VSX0VNQUlMPQogICAgICAtIEJCX0FETUlOX1VTRVJfUEFTU1dPUkQ9CiAgICBkZXBlbmRzX29uOgogICAgICAtIHdvcmtlci1zZXJ2aWNlCiAgICAgIC0gcmVkaXMtc2VydmljZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICctcU8tJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6NDAwMi9oZWFsdGgnCiAgICAgIGludGVydmFsOiAxNXMKICAgICAgdGltZW91dDogMTVzCiAgICAgIHJldHJpZXM6IDUKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICB3b3JrZXItc2VydmljZToKICAgIGltYWdlOiBidWRpYmFzZS5kb2NrZXIuc2NhcmYuc2gvYnVkaWJhc2Uvd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUxGX0hPU1RFRD0xCiAgICAgIC0gUE9SVD00MDAzCiAgICAgIC0gQ0xVU1RFUl9QT1JUPTEwMDAwCiAgICAgIC0gQVBJX0VOQ1JZUFRJT05fS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9CVURJQkFTRQogICAgICAtIEpXVF9TRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0XzY0X0JVRElCQVNFCiAgICAgIC0gTUlOSU9fQUNDRVNTX0tFWT0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdNSU5JT19VUkw9aHR0cDovL21pbmlvLXNlcnZpY2U6OTAwMCcKICAgICAgLSAnQVBQU19VUkw9aHR0cDovL2FwcC1zZXJ2aWNlOjQwMDInCiAgICAgIC0gQ09VQ0hfREJfVVNFUk5BTUU9JFNFUlZJQ0VfVVNFUl9DT1VDSERCCiAgICAgIC0gQ09VQ0hfREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfQ09VQ0hEQgogICAgICAtICdDT1VDSF9EQl9VUkw9aHR0cDovLyRTRVJWSUNFX1VTRVJfQ09VQ0hEQjokU0VSVklDRV9QQVNTV09SRF9DT1VDSERCQGNvdWNoZGItc2VydmljZTo1OTg0JwogICAgICAtIElOVEVSTkFMX0FQSV9LRVk9JFNFUlZJQ0VfQkFTRTY0XzEyOF9CVURJQkFTRQogICAgICAtICdSRURJU19VUkw9cmVkaXMtc2VydmljZTo2Mzc5JwogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICBkZXBlbmRzX29uOgogICAgICAtIHJlZGlzLXNlcnZpY2UKICAgICAgLSBtaW5pby1zZXJ2aWNlCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJy1xTy0nCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo0MDAzL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAxNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogIG1pbmlvLXNlcnZpY2U6CiAgICBpbWFnZTogbWluaW8vbWluaW8KICAgIHZvbHVtZXM6CiAgICAgIC0gJ21pbmlvX2RhdGE6L2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSBNSU5JT19CUk9XU0VSPW9mZgogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjkwMDAvbWluaW8vaGVhbHRoL2xpdmUnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDMKICBwcm94eS1zZXJ2aWNlOgogICAgaW1hZ2U6IGJ1ZGliYXNlL3Byb3h5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQlVESUJBU0VfMTAwMDAKICAgICAgLSBQUk9YWV9SQVRFX0xJTUlUX1dFQkhPT0tTX1BFUl9TRUNPTkQ9MTAKICAgICAgLSBQUk9YWV9SQVRFX0xJTUlUX0FQSV9QRVJfU0VDT05EPTIwCiAgICAgIC0gJ0FQUFNfVVBTVFJFQU1fVVJMPWh0dHA6Ly9hcHAtc2VydmljZTo0MDAyJwogICAgICAtICdXT1JLRVJfVVBTVFJFQU1fVVJMPWh0dHA6Ly93b3JrZXItc2VydmljZTo0MDAzJwogICAgICAtICdNSU5JT19VUFNUUkVBTV9VUkw9aHR0cDovL21pbmlvLXNlcnZpY2U6OTAwMCcKICAgICAgLSAnQ09VQ0hEQl9VUFNUUkVBTV9VUkw9aHR0cDovL2NvdWNoZGItc2VydmljZTo1OTg0JwogICAgICAtICdXQVRDSFRPV0VSX1VQU1RSRUFNX1VSTD1odHRwOi8vd2F0Y2h0b3dlci1zZXJ2aWNlOjgwODAnCiAgICAgIC0gUkVTT0xWRVI9MTI3LjAuMC4xMQogICAgZGVwZW5kc19vbjoKICAgICAgLSBtaW5pby1zZXJ2aWNlCiAgICAgIC0gd29ya2VyLXNlcnZpY2UKICAgICAgLSBhcHAtc2VydmljZQogICAgICAtIGNvdWNoZGItc2VydmljZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjEwMDAwLycKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAxNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogIGNvdWNoZGItc2VydmljZToKICAgIGltYWdlOiBidWRpYmFzZS9jb3VjaGRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBDT1VDSERCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX0NPVUNIREIKICAgICAgLSBDT1VDSERCX1VTRVI9JFNFUlZJQ0VfVVNFUl9DT1VDSERCCiAgICAgIC0gVEFSR0VUQlVJTEQ9ZG9ja2VyLWNvbXBvc2UKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo1OTg0LycKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAxNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY291Y2hkYjNfZGF0YTovb3B0L2NvdWNoZGIvZGF0YScKICByZWRpcy1zZXJ2aWNlOgogICAgaW1hZ2U6IHJlZGlzCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgIiRTRVJWSUNFX1BBU1NXT1JEX1JFRElTIicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAkU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogMTVzCiAgICAgIHRpbWVvdXQ6IDE1cwogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCiAgd2F0Y2h0b3dlci1zZXJ2aWNlOgogICAgcmVzdGFydDogYWx3YXlzCiAgICBpbWFnZTogY29udGFpbnJyci93YXRjaHRvd2VyCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgIGNvbW1hbmQ6ICctLWRlYnVnIC0taHR0cC1hcGktdXBkYXRlIGJiYXBwcyBiYndvcmtlciBiYnByb3h5JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gV0FUQ0hUT1dFUl9IVFRQX0FQST10cnVlCiAgICAgIC0gV0FUQ0hUT1dFUl9IVFRQX0FQSV9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9XQVRDSFRPV0VSCiAgICAgIC0gV0FUQ0hUT1dFUl9DTEVBTlVQPXRydWUKICAgIGxhYmVsczoKICAgICAgLSBjb20uY2VudHVyeWxpbmtsYWJzLndhdGNodG93ZXIuZW5hYmxlPWZhbHNlCiAgICBleGNsdWRlX2Zyb21faGM6IHRydWUK", + "compose": "c2VydmljZXM6CiAgYXBwLXNlcnZpY2U6CiAgICBpbWFnZTogYnVkaWJhc2UuZG9ja2VyLnNjYXJmLnNoL2J1ZGliYXNlL2FwcHMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFTEZfSE9TVEVEPTEKICAgICAgLSAnQ09VQ0hfREJfVVJMPWh0dHA6Ly8kU0VSVklDRV9VU0VSX0NPVUNIREI6JFNFUlZJQ0VfUEFTU1dPUkRfQ09VQ0hEQkBjb3VjaGRiLXNlcnZpY2U6NTk4NCcKICAgICAgLSAnV09SS0VSX1VSTD1odHRwOi8vd29ya2VyLXNlcnZpY2U6NDAwMycKICAgICAgLSAnTUlOSU9fVVJMPWh0dHA6Ly9taW5pby1zZXJ2aWNlOjkwMDAnCiAgICAgIC0gTUlOSU9fQUNDRVNTX0tFWT0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtIElOVEVSTkFMX0FQSV9LRVk9JFNFUlZJQ0VfQkFTRTY0XzEyOF9CVURJQkFTRQogICAgICAtICdCVURJQkFTRV9FTlZJUk9OTUVOVD0ke0JVRElCQVNFX0VOVklST05NRU5UOi1QUk9EVUNUSU9OfScKICAgICAgLSBQT1JUPTQwMDIKICAgICAgLSBBUElfRU5DUllQVElPTl9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0JVRElCQVNFQVBJCiAgICAgIC0gRU5DUllQVElPTl9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0JVRElCQVNFCiAgICAgIC0gSldUX1NFQ1JFVD0kU0VSVklDRV9CQVNFNjRfNjRfQlVESUJBU0VKV1QKICAgICAgLSBMT0dfTEVWRUw9aW5mbwogICAgICAtICdFTkFCTEVfQU5BTFlUSUNTPSR7RU5BQkxFX0FOQUxZVElDUzotdHJ1ZX0nCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpcy1zZXJ2aWNlOjYzNzknCiAgICAgIC0gUkVESVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUkVESVMKICAgICAgLSBCQl9BRE1JTl9VU0VSX0VNQUlMPQogICAgICAtIEJCX0FETUlOX1VTRVJfUEFTU1dPUkQ9CiAgICBkZXBlbmRzX29uOgogICAgICAtIHdvcmtlci1zZXJ2aWNlCiAgICAgIC0gcmVkaXMtc2VydmljZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICctcU8tJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6NDAwMi9oZWFsdGgnCiAgICAgIGludGVydmFsOiAxNXMKICAgICAgdGltZW91dDogMTVzCiAgICAgIHJldHJpZXM6IDUKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICB3b3JrZXItc2VydmljZToKICAgIGltYWdlOiBidWRpYmFzZS5kb2NrZXIuc2NhcmYuc2gvYnVkaWJhc2Uvd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUxGX0hPU1RFRD0xCiAgICAgIC0gUE9SVD00MDAzCiAgICAgIC0gQ0xVU1RFUl9QT1JUPTEwMDAwCiAgICAgIC0gQVBJX0VOQ1JZUFRJT05fS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9CVURJQkFTRQogICAgICAtIEpXVF9TRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0XzY0X0JVRElCQVNFCiAgICAgIC0gTUlOSU9fQUNDRVNTX0tFWT0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdNSU5JT19VUkw9aHR0cDovL21pbmlvLXNlcnZpY2U6OTAwMCcKICAgICAgLSAnQVBQU19VUkw9aHR0cDovL2FwcC1zZXJ2aWNlOjQwMDInCiAgICAgIC0gQ09VQ0hfREJfVVNFUk5BTUU9JFNFUlZJQ0VfVVNFUl9DT1VDSERCCiAgICAgIC0gQ09VQ0hfREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfQ09VQ0hEQgogICAgICAtICdDT1VDSF9EQl9VUkw9aHR0cDovLyRTRVJWSUNFX1VTRVJfQ09VQ0hEQjokU0VSVklDRV9QQVNTV09SRF9DT1VDSERCQGNvdWNoZGItc2VydmljZTo1OTg0JwogICAgICAtIElOVEVSTkFMX0FQSV9LRVk9JFNFUlZJQ0VfQkFTRTY0XzEyOF9CVURJQkFTRQogICAgICAtICdSRURJU19VUkw9cmVkaXMtc2VydmljZTo2Mzc5JwogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICBkZXBlbmRzX29uOgogICAgICAtIHJlZGlzLXNlcnZpY2UKICAgICAgLSBtaW5pby1zZXJ2aWNlCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJy1xTy0nCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo0MDAzL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAxNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogIG1pbmlvLXNlcnZpY2U6CiAgICBpbWFnZTogbWluaW8vbWluaW8KICAgIHZvbHVtZXM6CiAgICAgIC0gJ21pbmlvX2RhdGE6L2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSBNSU5JT19CUk9XU0VSPW9mZgogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjkwMDAvbWluaW8vaGVhbHRoL2xpdmUnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDMKICBwcm94eS1zZXJ2aWNlOgogICAgaW1hZ2U6IGJ1ZGliYXNlL3Byb3h5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQlVESUJBU0VfMTAwMDAKICAgICAgLSBQUk9YWV9SQVRFX0xJTUlUX1dFQkhPT0tTX1BFUl9TRUNPTkQ9MTAKICAgICAgLSBQUk9YWV9SQVRFX0xJTUlUX0FQSV9QRVJfU0VDT05EPTIwCiAgICAgIC0gJ0FQUFNfVVBTVFJFQU1fVVJMPWh0dHA6Ly9hcHAtc2VydmljZTo0MDAyJwogICAgICAtICdXT1JLRVJfVVBTVFJFQU1fVVJMPWh0dHA6Ly93b3JrZXItc2VydmljZTo0MDAzJwogICAgICAtICdNSU5JT19VUFNUUkVBTV9VUkw9aHR0cDovL21pbmlvLXNlcnZpY2U6OTAwMCcKICAgICAgLSAnQ09VQ0hEQl9VUFNUUkVBTV9VUkw9aHR0cDovL2NvdWNoZGItc2VydmljZTo1OTg0JwogICAgICAtICdXQVRDSFRPV0VSX1VQU1RSRUFNX1VSTD1odHRwOi8vd2F0Y2h0b3dlci1zZXJ2aWNlOjgwODAnCiAgICAgIC0gUkVTT0xWRVI9MTI3LjAuMC4xMQogICAgZGVwZW5kc19vbjoKICAgICAgLSBtaW5pby1zZXJ2aWNlCiAgICAgIC0gd29ya2VyLXNlcnZpY2UKICAgICAgLSBhcHAtc2VydmljZQogICAgICAtIGNvdWNoZGItc2VydmljZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjEwMDAwLycKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAxNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogIGNvdWNoZGItc2VydmljZToKICAgIGltYWdlOiBidWRpYmFzZS9jb3VjaGRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBDT1VDSERCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX0NPVUNIREIKICAgICAgLSBDT1VDSERCX1VTRVI9JFNFUlZJQ0VfVVNFUl9DT1VDSERCCiAgICAgIC0gVEFSR0VUQlVJTEQ9ZG9ja2VyLWNvbXBvc2UKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo1OTg0LycKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAxNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY291Y2hkYjNfZGF0YTovb3B0L2NvdWNoZGIvZGF0YScKICByZWRpcy1zZXJ2aWNlOgogICAgaW1hZ2U6IHJlZGlzCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgIiRTRVJWSUNFX1BBU1NXT1JEX1JFRElTIicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAkU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogMTVzCiAgICAgIHRpbWVvdXQ6IDE1cwogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCiAgd2F0Y2h0b3dlci1zZXJ2aWNlOgogICAgcmVzdGFydDogYWx3YXlzCiAgICBpbWFnZTogY29udGFpbnJyci93YXRjaHRvd2VyCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgIGNvbW1hbmQ6ICctLWRlYnVnIC0taHR0cC1hcGktdXBkYXRlIGJiYXBwcyBiYndvcmtlciBiYnByb3h5JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gV0FUQ0hUT1dFUl9IVFRQX0FQST10cnVlCiAgICAgIC0gV0FUQ0hUT1dFUl9IVFRQX0FQSV9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9XQVRDSFRPV0VSCiAgICAgIC0gV0FUQ0hUT1dFUl9DTEVBTlVQPXRydWUKICAgIGxhYmVsczoKICAgICAgLSBjb20uY2VudHVyeWxpbmtsYWJzLndhdGNodG93ZXIuZW5hYmxlPWZhbHNlCiAgICBleGNsdWRlX2Zyb21faGM6IHRydWUK", "tags": [ "budibase", "low-code", @@ -1248,6 +1248,19 @@ "logo": "svgs/gitea.svg", "minversion": "0.0.0" }, + "github-runner": { + "documentation": "https://github.com/myoung34/docker-github-actions-runner/wiki/Usage?utm_source=coolify.io", + "slogan": "A GitHub Actions runner for Docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdteW91bmczNC9naXRodWItcnVubmVyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRVBPX1VSTD0ke1JFUE9fVVJMfScKICAgICAgLSAnUlVOTkVSX05BTUVfUFJFRklYPSR7UlVOTkVSX05BTUVfUFJFRklYOi1jb29saWZ5LXJ1bm5lcn0nCiAgICAgIC0gJ1JVTk5FUl9OQU1FX1NVRkZJWD0ke1JVTk5FUl9OQU1FX1NVRkZJWDotZmFsc2V9JwogICAgICAtICdBQ0NFU1NfVE9LRU49JHtBQ0NFU1NfVE9LRU59JwogICAgICAtIFJVTk5FUl9XT1JLRElSPS9ydW5uZXIvd29yawogICAgICAtICdSVU5ORVJfU0NPUEU9JHtSVU5ORVJfU0NPUEU6LXJlcG99JwogICAgICAtICdMQUJFTFM9JHtMQUJFTFM6LWRlZmF1bHR9JwogICAgICAtICdPUkdfTkFNRT0ke09SR19OQU1FfScKICAgIHNlY3VyaXR5X29wdDoKICAgICAgLSAnbGFiZWw6ZGlzYWJsZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgICAtICdydW5uZXItZGF0YTovcnVubmVyJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJwcyBhdXggfCBncmVwICdbUl11bm5lcicgPiAvZGV2L251bGwgfHwgZXhpdCAxIgogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", + "tags": [ + "github", + "actions", + "runner", + "docker" + ], + "logo": "svgs/github-runner.png", + "minversion": "0.0.0" + }, "gitlab": { "documentation": "https://docs.gitlab.com/ee/install/docker.html?utm_source=coolify.io", "slogan": "The all-in-one DevOps platform for seamless collaboration and continuous delivery.", @@ -1334,6 +1347,21 @@ "minversion": "0.0.0", "port": "3000" }, + "gowa": { + "documentation": "https://github.com/aldinokemal/go-whatsapp-web-multidevice?utm_source=coolify.io", + "slogan": "Golang WhatsApp - Built with Go for efficient memory use", + "compose": "c2VydmljZXM6CiAgZ293YToKICAgIGltYWdlOiAnYWxkaW5va2VtYWwyMTA0L2dvLXdoYXRzYXBwLXdlYi1tdWx0aWRldmljZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR09XQV8zMDAwCiAgICAgIC0gU0VSVklDRV9VU0VSX0dPV0EKICAgICAgLSBTRVJWSUNFX1BBU1NXT1JEX0dPV0EKICAgICAgLSAnQVBQX0JBU0lDX0FVVEg9JHtTRVJWSUNFX1VTRVJfR09XQX06JHtTRVJWSUNFX1BBU1NXT1JEX0dPV0F9JwogICAgICAtIEFQUF9QT1JUPTMwMDAKICAgICAgLSAnQVBQX0RFQlVHPSR7QVBQX0RFQlVHOi1mYWxzZX0nCiAgICAgIC0gJ0FQUF9BQ0NPVU5UX1ZBTElEQVRJT049JHtBUFBfQUNDT1VOVF9WQUxJREFUSU9OOi1mYWxzZX0nCiAgICAgIC0gJ1dIQVRTQVBQX1dFQkhPT0s9JHtXSEFUU0FQUF9XRUJIT09LfScKICAgICAgLSAnV0hBVFNBUFBfV0VCSE9PS19TRUNSRVQ9JHtXSEFUU0FQUF9XRUJIT09LX1NFQ1JFVH0nCiAgICB2b2x1bWVzOgogICAgICAtICdnb3dhX2RhdGE6L2FwcC9zdG9yYWdlcycK", + "tags": [ + "whatsapp", + "golang", + "multidevice", + "api", + "go-whatsapp" + ], + "logo": "svgs/gowa.svg", + "minversion": "0.0.0", + "port": "3000" + }, "grafana-with-postgresql": { "documentation": "https://grafana.com?utm_source=coolify.io", "slogan": "Grafana is the open source analytics & monitoring solution for every database.", @@ -1918,6 +1946,22 @@ "minversion": "0.0.0", "port": "3000" }, + "matrix": { + "documentation": "https://matrix.org/docs/chat_basics/matrix-for-im/?utm_source=coolify.io", + "slogan": "Chat securely with your family, friends, community, or build great apps with Matrix!", + "compose": "c2VydmljZXM6CiAgbWF0cml4OgogICAgaW1hZ2U6ICdtYXRyaXhkb3Rvcmcvc3luYXBzZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUFUUklYXzgwMDgKICAgICAgLSAnU1lOQVBTRV9TRVJWRVJfTkFNRT0ke1NFUlZJQ0VfVVJMX01BVFJJWH0nCiAgICAgIC0gJ1NZTkFQU0VfUkVQT1JUX1NUQVRTPSR7U1lOQVBTRV9SRVBPUlRfU1RBVFM6LW5vfScKICAgICAgLSAnRU5BQkxFX1JFR0lTVFJBVElPTj0ke0VOQUJMRV9SRUdJU1RSQVRJT046LWZhbHNlfScKICAgICAgLSAnUkVDQVBUQ0hBX1BVQkxJQ19LRVk9JHtSRUNBUFRDSEFfUFVCTElDX0tFWX0nCiAgICAgIC0gJ1JFQ0FQVENIQV9QUklWQVRFX0tFWT0ke1JFQ0FQVENIQV9QUklWQVRFX0tFWX0nCiAgICAgIC0gJ19TRVJWRVJfTkFNRT0ke1NFUlZJQ0VfVVJMX01BVFJJWH0nCiAgICAgIC0gJ19BRE1JTl9OQU1FPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnX0FETUlOX1BBU1M9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21hdHJpeC1kYXRhOi9kYXRhJwogICAgZW50cnlwb2ludDoKICAgICAgLSAvYmluL2Jhc2gKICAgICAgLSAnLWMnCiAgICAgIC0gIiEgdGVzdCAtZiAvZGF0YS9ob21lc2VydmVyLnlhbWwgJiYgL3N0YXJ0LnB5IGdlbmVyYXRlXG5cbiMgcmVnaXN0cmF0aW9uX3NoYXJlZF9zZWNyZXRcbmdyZXAgXCJyZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldFwiIC9kYXRhL2hvbWVzZXJ2ZXIueWFtbCBcXFxufCBhd2sgJ3twcmludCAkMn0nID4gLi9yZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldFxuXG4jIG1hY2Fyb29uX3NlY3JldF9rZXlcbmdyZXAgXCJtYWNhcm9vbl9zZWNyZXRfa2V5XCIgL2RhdGEvaG9tZXNlcnZlci55YW1sIFxcXG58IGF3ayAne3ByaW50ICQyfScgPiAuL21hY2Fyb29uX3NlY3JldF9rZXlcblxuIyBmb3JtX3NlY3JldFxuZ3JlcCBcImZvcm1fc2VjcmV0XCIgL2RhdGEvaG9tZXNlcnZlci55YW1sIFxcXG58IGF3ayAne3ByaW50ICQyfScgPiAuL2Zvcm1fc2VjcmV0XG5cbiMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjXG4jICAgICAgICAgICAgICAgICAgICAgICAgI1xuIyBob21lc2VydmVyLnlhbWw6IHN0YXJ0ICNcbiMgICAgICAgICAgICAgICAgICAgICAgICAjXG4jIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjI1xuY2F0IDw8RU9GID4gL2RhdGEvaG9tZXNlcnZlci55YW1sXG5zZXJ2ZXJfbmFtZTogXCIke1NFUlZJQ0VfVVJMX01BVFJJWH1cIlxucGlkX2ZpbGU6IC9kYXRhL2hvbWVzZXJ2ZXIucGlkXG5cbiMgc2VydmVyXG5saXN0ZW5lcnM6XG4gIC0gcG9ydDogODAwOFxuICAgIHRsczogZmFsc2VcbiAgICB0eXBlOiBodHRwXG4gICAgeF9mb3J3YXJkZWQ6IHRydWVcbiAgICByZXNvdXJjZXM6XG4gICAgICAtIG5hbWVzOiBbY2xpZW50LCBmZWRlcmF0aW9uXVxuICAgICAgICBjb21wcmVzczogZmFsc2VcblxuIyBkYXRhYmFzZVxuZGF0YWJhc2U6XG4gIG5hbWU6IHNxbGl0ZTNcbiAgYXJnczpcbiAgICBkYXRhYmFzZTogL2RhdGEvaG9tZXNlcnZlci5kYlxuXG4jIGdlbmVyYWxcbmxvZ19jb25maWc6IFwiL2RhdGEvJHtTRVJWSUNFX1VSTF9NQVRSSVh9LmxvZy5jb25maWdcIlxubWVkaWFfc3RvcmVfcGF0aDogL2RhdGEvbWVkaWFfc3RvcmVcbnJlcG9ydF9zdGF0czogZmFsc2VcblxuIyBzZWNyZXRzXG5yZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldDogJCg8Li9yZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldClcbm1hY2Fyb29uX3NlY3JldF9rZXk6ICQoPC4vbWFjYXJvb25fc2VjcmV0X2tleSlcbmZvcm1fc2VjcmV0OiAkKDwuL2Zvcm1fc2VjcmV0KVxuc2lnbmluZ19rZXlfcGF0aDogXCIvZGF0YS8ke1NFUlZJQ0VfVVJMX01BVFJJWH0uc2lnbmluZy5rZXlcIlxuXG4jcm9vbXNcbmF1dG9fam9pbl9yb29tczpcbiAgLSBcIiNnZW5lcmFsOiR7U0VSVklDRV9VUkxfTUFUUklYfVwiXG5cbiMgZmVkZXJhdGlvblxudHJ1c3RlZF9rZXlfc2VydmVyczpcbiAgLSBzZXJ2ZXJfbmFtZTogXCJtYXRyaXgub3JnXCJcbmF1dG9jcmVhdGVfYXV0b19qb2luX3Jvb21zX2ZlZGVyYXRlZDogZmFsc2VcbmFsbG93X3B1YmxpY19yb29tc19vdmVyX2ZlZGVyYXRpb246IGZhbHNlXG5FT0ZcbiMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjI1xuIyAgICAgICAgICAgICAgICAgICAgICAjXG4jIGhvbWVzZXJ2ZXIueWFtbDogZW5kICNcbiMgICAgICAgICAgICAgICAgICAgICAgI1xuIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjXG5cbnRlc3QgLW4gXCIke0VOQUJMRV9SRUdJU1RSQVRJT059XCIgJiYgISBncmVwIFwiI3JlZ2lzdHJhdGlvblwiIC9kYXRhL2hvbWVzZXJ2ZXIueWFtbCAmPi9kZXYvbnVsbCBcXFxuJiYgZWNobyA+PiAvZGF0YS9ob21lc2VydmVyLnlhbWwgXFxcbiYmIGNhdCA8PEVPRiA+PiAvZGF0YS9ob21lc2VydmVyLnlhbWxcbiNyZWdpc3RyYXRpb25cbmVuYWJsZV9yZWdpc3RyYXRpb246IHRydWUgICMgQWxsb3dzIHVzZXJzIHRvIHJlZ2lzdGVyIG9uIHlvdXIgc2VydmVyLlxuRU9GXG5cbiEgZ3JlcCAke1JFQ0FQVENIQV9QVUJMSUNfS0VZfSAvZGF0YS9ob21lc2VydmVyLnlhbWwgJj4vZGV2L251bGwgXFxcbiYmIGVjaG8gPj4gL2RhdGEvaG9tZXNlcnZlci55YW1sIFxcXG4mJiBjYXQgPDxFT0YgPj4gL2RhdGEvaG9tZXNlcnZlci55YW1sXG4jIHJlQ0FQVENIQSBzZXR0aW5nc1xuZW5hYmxlX3JlZ2lzdHJhdGlvbl9jYXB0Y2hhOiB0cnVlICAjIEVuYWJsZXMgQ0FQVENIQSBmb3IgcmVnaXN0cmF0aW9ucy5cbnJlY2FwdGNoYV9wdWJsaWNfa2V5OiBcIiR7UkVDQVBUQ0hBX1BVQkxJQ19LRVl9XCJcbnJlY2FwdGNoYV9wcml2YXRlX2tleTogXCIke1JFQ0FQVENIQV9QUklWQVRFX0tFWX1cIlxucmVjYXB0Y2hhX3NpdGV2ZXJpZnlfYXBpOiBcImh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vcmVjYXB0Y2hhL2FwaS9zaXRldmVyaWZ5XCJcbkVPRlxuXG5yZWdpc3Rlcl9hZG1pbigpe1xuICB3aGlsZSAhIGN1cmwgLUkgbG9jYWxob3N0OjgwMDggJj4vZGV2L251bGw7IGRvXG4gICAgc2xlZXAgMVxuICBkb25lXG4gIHJlZ2lzdGVyX25ld19tYXRyaXhfdXNlciBcXFxuICAgIC1hIFxcXG4gICAgLXUgJHtTRVJWSUNFX1VTRVJfQURNSU59IFxcXG4gICAgLXAgJHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfSBcXFxuICAgIC1jIC9kYXRhL2hvbWVzZXJ2ZXIueWFtbCBcXFxuICAgIGh0dHA6Ly9sb2NhbGhvc3Q6ODAwOCAmPi9kZXYvbnVsbFxufVxucmVnaXN0ZXJfYWRtaW4gJlxuXG4vc3RhcnQucHlcbiIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLUknCiAgICAgICAgLSAnbG9jYWxob3N0OjgwMDgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAzcwogICAgICByZXRyaWVzOiA1Cg==", + "tags": [ + "chat", + "slack", + "discord", + "voip", + "video", + "call" + ], + "logo": "svgs/matrix.svg", + "minversion": "0.0.0", + "port": "8008" + }, "mattermost": { "documentation": "https://docs.mattermost.com?utm_source=coolify.io", "slogan": "Mattermost is an open source, self-hosted Slack-alternative.", @@ -2341,7 +2385,7 @@ "nitropage-with-postgresql": { "documentation": "https://nitropage.org?utm_source=coolify.io", "slogan": "Nitropage is an extensible, visual website builder, offering a growing library of versatile building blocks, focal-point image cropping and sovereign font management.", - "compose": "c2VydmljZXM6CiAgbml0cm9wYWdlOgogICAgaW1hZ2U6IGNvZGViZXJnLm9yZy9uaXRyb3BhZ2Uvbml0cm9wYWdlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTklUUk9QQUdFXzMwMDAKICAgICAgLSAnTlBfQVVUSF9TQUxUPSR7U0VSVklDRV9CQVNFNjRfU0FMVH0nCiAgICAgIC0gJ05QX0FVVEhfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT059JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfUBwb3N0Z3Jlc3FsOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1uaXRyb3BhZ2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnbml0cm9wYWdlLWRhdGE6L2FwcC8uZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FkbWluJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnbml0cm9wYWdlLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LW5pdHJvcGFnZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgbml0cm9wYWdlOgogICAgaW1hZ2U6IG5pdHJvcGFnZS9uaXRyb3BhZ2UKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9OSVRST1BBR0VfMzAwMAogICAgICAtICdOUF9BVVRIX1NBTFQ9JHtTRVJWSUNFX0JBU0U2NF9TQUxUfScKICAgICAgLSAnTlBfQVVUSF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfU0VTU0lPTn0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH06JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9QHBvc3RncmVzcWw6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LW5pdHJvcGFnZX0nCiAgICB2b2x1bWVzOgogICAgICAtICduaXRyb3BhZ2UtZGF0YTovYXBwLy5kYXRhJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYWRtaW4nCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICduaXRyb3BhZ2UtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotbml0cm9wYWdlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", "tags": [ "nitropage", "builder", @@ -2358,7 +2402,7 @@ "nitropage": { "documentation": "https://nitropage.org?utm_source=coolify.io", "slogan": "Nitropage is an extensible, visual website builder, offering a growing library of versatile building blocks, focal-point image cropping and sovereign font management.", - "compose": "c2VydmljZXM6CiAgbml0cm9wYWdlOgogICAgaW1hZ2U6ICdjb2RlYmVyZy5vcmcvbml0cm9wYWdlL25pdHJvcGFnZTpzcWxpdGUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTklUUk9QQUdFXzMwMDAKICAgICAgLSAnTlBfQVVUSF9TQUxUPSR7U0VSVklDRV9CQVNFNjRfU0FMVH0nCiAgICAgIC0gJ05QX0FVVEhfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT059JwogICAgICAtICdEQVRBQkFTRV9VUkw9ZmlsZTouLi8uLi8uZGF0YS9kZXYuZGInCiAgICB2b2x1bWVzOgogICAgICAtICduaXRyb3BhZ2UtZGF0YTovYXBwLy5kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYWRtaW4nCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "compose": "c2VydmljZXM6CiAgbml0cm9wYWdlOgogICAgaW1hZ2U6ICduaXRyb3BhZ2Uvbml0cm9wYWdlOnNxbGl0ZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9OSVRST1BBR0VfMzAwMAogICAgICAtICdOUF9BVVRIX1NBTFQ9JHtTRVJWSUNFX0JBU0U2NF9TQUxUfScKICAgICAgLSAnTlBfQVVUSF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfU0VTU0lPTn0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1maWxlOi4uLy5kYXRhL2Rldi5kYicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ25pdHJvcGFnZS1kYXRhOi9hcHAvLmRhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hZG1pbicKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=", "tags": [ "nitropage", "builder", @@ -3341,6 +3385,22 @@ "minversion": "0.0.0", "port": "3000" }, + "triliumnext": { + "documentation": "https://github.com/TriliumNext/Trilium?utm_source=coolify.io", + "slogan": "Build your personal knowledge base with TriliumNext Notes.", + "compose": "c2VydmljZXM6CiAgdHJpbGl1bW5leHQ6CiAgICBpbWFnZTogJ2doY3IuaW8vdHJpbGl1bW5leHQvdHJpbGl1bTpzdGFibGUnCiAgICBwbGF0Zm9ybTogbGludXgvYW1kNjQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklMSVVNTkVYVF84MDgwCiAgICAgIC0gJ1RaPSR7VFo6LUV1cm9wZS9CZXJsaW59JwogICAgdm9sdW1lczoKICAgICAgLSAndHJpbGl1bW5leHRfZGF0YTovaG9tZS9ub2RlL3RyaWxpdW0tZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtLXF1aWV0IC0tdHJpZXM9MSAtLXNwaWRlciBodHRwOi8vMTI3LjAuMC4xOjgwODAvYXBpL2hlYWx0aC1jaGVjayB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "self-hosted", + "notes", + "todo", + "organize", + "markdown", + "wiki" + ], + "logo": "svgs/triliumnext.svg", + "minversion": "0.0.0", + "port": "8080" + }, "typesense": { "documentation": "https://typesense.org/docs?utm_source=coolify.io", "slogan": "Cutting-edge, in-memory search engine for mere mortals. Knowledge of rocket science optional.", @@ -3674,7 +3734,7 @@ "wireguard-easy": { "documentation": "https://github.com/wg-easy/wg-easy?utm_source=coolify.io", "slogan": "The easiest way to run WireGuard VPN + Web-based Admin UI.", - "compose": "c2VydmljZXM6CiAgd2ctZWFzeToKICAgIGltYWdlOiAnZ2hjci5pby93Zy1lYXN5L3dnLWVhc3k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1dJUkVHVUFSREVBU1lfODAwMAogICAgICAtICdXR19IT1NUPSR7U0VSVklDRV9GUUROX1dJUkVHVUFSREVBU1l9JwogICAgICAtICdMQU5HPSR7TEFORzotZW59JwogICAgICAtIFBPUlQ9ODAwMAogICAgICAtIFdHX1BPUlQ9NTE4MjAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3dnLWVhc3k6L2V0Yy93aXJlZ3VhcmQnCiAgICBwb3J0czoKICAgICAgLSAnNTE4MjA6NTE4MjAvdWRwJwogICAgY2FwX2FkZDoKICAgICAgLSBORVRfQURNSU4KICAgICAgLSBTWVNfTU9EVUxFCiAgICBzeXNjdGxzOgogICAgICAtIG5ldC5pcHY0LmNvbmYuYWxsLnNyY192YWxpZF9tYXJrPTEKICAgICAgLSBuZXQuaXB2NC5pcF9mb3J3YXJkPTEK", + "compose": "c2VydmljZXM6CiAgd2ctZWFzeToKICAgIGltYWdlOiAnZ2hjci5pby93Zy1lYXN5L3dnLWVhc3k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1dJUkVHVUFSREVBU1lfNTE4MjEKICAgICAgLSAnV0dfSE9TVD0ke1NFUlZJQ0VfVVJMX1dJUkVHVUFSREVBU1l9JwogICAgICAtICdMQU5HPSR7TEFORzotZW59JwogICAgICAtIFdHX1BPUlQ9NTE4MjAKICAgICAgLSAnX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICB2b2x1bWVzOgogICAgICAtICd3Zy1lYXN5Oi9ldGMvd2lyZWd1YXJkJwogICAgcG9ydHM6CiAgICAgIC0gJzUxODIwOjUxODIwL3VkcCcKICAgIGNhcF9hZGQ6CiAgICAgIC0gTkVUX0FETUlOCiAgICAgIC0gU1lTX01PRFVMRQogICAgc3lzY3RsczoKICAgICAgLSBuZXQuaXB2NC5jb25mLmFsbC5zcmNfdmFsaWRfbWFyaz0xCiAgICAgIC0gbmV0LmlwdjQuaXBfZm9yd2FyZD0xCiAgICBlbnRyeXBvaW50OgogICAgICAtIC9iaW4vYmFzaAogICAgICAtICctYycKICAgICAgLSAiZXZhbCBcIndncHcgJyR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nID4gL3Bhc3MtaGFzaFwiXG5ldmFsIFwiJChjYXQgL3Bhc3MtaGFzaCkgZHVtYi1pbml0IG5vZGUgc2VydmVyLmpzXCJcbiIK", "tags": [ "wireguard", "vpn", From 43ff38d91430f227fddca4f2afcbc229f2bd05f3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 18 Jul 2025 21:19:32 +0200 Subject: [PATCH 0087/1717] feat(terminal): dispatch focus event for terminal after connection and enhance focus handling in JavaScript --- .../Shared/ExecuteContainerCommand.php | 6 ++++++ resources/js/terminal.js | 21 +++++++++++++++++++ .../execute-container-command.blade.php | 3 ++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 9003e093a..2d55807c7 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -158,6 +158,9 @@ public function connectToServer() data_get($server, 'name'), data_get($server, 'uuid') ); + + // Dispatch a frontend event to ensure terminal gets focus after connection + $this->dispatch('terminal-should-focus'); } catch (\Throwable $e) { return handleError($e, $this); } finally { @@ -213,6 +216,9 @@ public function connectToContainer() data_get($container, 'container.Names'), data_get($container, 'server.uuid') ); + + // Dispatch a frontend event to ensure terminal gets focus after connection + $this->dispatch('terminal-should-focus'); } catch (\Throwable $e) { return handleError($e, $this); } finally { diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 10535f3ea..b49aad9cf 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -48,6 +48,18 @@ export function initializeTerminalComponent() { this.sendCommandWhenReady({ command: command }); }); + this.$wire.on('terminal-should-focus', () => { + // Wait for terminal to be ready, then focus + const focusWhenReady = () => { + if (this.terminalActive && this.term) { + this.term.focus(); + } else { + setTimeout(focusWhenReady, 100); + } + }; + focusWhenReady(); + }); + this.keepAliveInterval = setInterval(this.keepAlive.bind(this), 30000); this.$watch('terminalActive', (active) => { @@ -353,6 +365,15 @@ export function initializeTerminalComponent() { this.resizeTerminal(); }, 200); + // Ensure terminal gets focus after connection with multiple attempts + setTimeout(() => { + this.term.focus(); + }, 100); + + setTimeout(() => { + this.term.focus(); + }, 500); + // Notify parent component that terminal is connected this.$wire.dispatch('terminalConnected'); } else if (event.data === 'unprocessable') { diff --git a/resources/views/livewire/project/shared/execute-container-command.blade.php b/resources/views/livewire/project/shared/execute-container-command.blade.php index 71d253103..7fe208a9b 100644 --- a/resources/views/livewire/project/shared/execute-container-command.blade.php +++ b/resources/views/livewire/project/shared/execute-container-command.blade.php @@ -45,7 +45,8 @@ @if ($servers->first()->isTerminalEnabled() && $servers->first()->isFunctional()) + x-data="{ autoConnected: false }" + x-on:terminal-websocket-ready.window="if (!autoConnected) { autoConnected = true; $wire.dispatchSelf('connectToServer'); }">

Terminal

{{ $isConnecting ? 'Connecting...' : 'Connect' }} From 9651e3a7ec6089fd8556eb3db2ce9ab05baa406b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 18 Jul 2025 23:10:08 +0200 Subject: [PATCH 0088/1717] revert(parser): enhance FQDN generation logic for services and applications --- bootstrap/helpers/shared.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index de6c954e0..7ce511f2c 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3049,7 +3049,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int // Get all SERVICE_ variables from keys and values $key = str($key); $value = str($value); - $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; preg_match_all($regex, $value, $valueMatches); if (count($valueMatches[1]) > 0) { @@ -3080,8 +3079,11 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } } elseif ($isService) { if (blank($savedService->fqdn)) { - // For services, if no explicit FQDN is set, leave SERVICE_FQDN_ variables empty - $fqdn = ''; + if ($fqdnFor) { + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + } else { + $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); + } } else { $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); } @@ -3147,10 +3149,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($isApplication && $resource->build_pack === 'dockercompose') { continue; } - // For services, only generate FQDN if explicit FQDN is set - if ($isService && blank($savedService->fqdn)) { - continue; - } $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); if (str($fqdnFor)->contains('_')) { $fqdnFor = str($fqdnFor)->before('_'); From 11341d7c2c47ca3f8d149e363d7005fdab168f22 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 18 Jul 2025 23:15:51 +0200 Subject: [PATCH 0089/1717] refactor(jobs): remove logging for ScheduledJobManager and ServerResourceManager start and completion --- app/Jobs/ScheduledJobManager.php | 26 -------------------------- app/Jobs/ServerResourceManager.php | 6 ------ 2 files changed, 32 deletions(-) diff --git a/app/Jobs/ScheduledJobManager.php b/app/Jobs/ScheduledJobManager.php index 572491c0d..b90e853fc 100644 --- a/app/Jobs/ScheduledJobManager.php +++ b/app/Jobs/ScheduledJobManager.php @@ -58,10 +58,6 @@ public function handle(): void // Freeze the execution time at the start of the job $this->executionTime = Carbon::now(); - Log::channel('scheduled')->info('ScheduledJobManager started', [ - 'execution_time' => $this->executionTime->format('Y-m-d H:i:s T'), - ]); - // Process backups - don't let failures stop task processing try { $this->processScheduledBackups(); @@ -81,8 +77,6 @@ public function handle(): void 'trace' => $e->getTraceAsString(), ]); } - - Log::channel('scheduled')->info('ScheduledJobManager completed'); } private function processScheduledBackups(): void @@ -111,16 +105,6 @@ private function processScheduledBackups(): void } if ($this->shouldRunNow($frequency, $serverTimezone)) { - Log::channel('scheduled')->info('Dispatching backup job', [ - 'backup_id' => $backup->id, - 'backup_name' => $backup->name ?? 'unnamed', - 'server_name' => $server->name, - 'frequency' => $frequency, - 'timezone' => $serverTimezone, - 'execution_time' => $this->executionTime?->format('Y-m-d H:i:s T'), - 'current_time' => Carbon::now()->format('Y-m-d H:i:s T'), - ]); - DatabaseBackupJob::dispatch($backup); } } catch (\Exception $e) { @@ -157,16 +141,6 @@ private function processScheduledTasks(): void } if ($this->shouldRunNow($frequency, $serverTimezone)) { - Log::channel('scheduled')->info('Dispatching task job', [ - 'task_id' => $task->id, - 'task_name' => $task->name ?? 'unnamed', - 'server_name' => $server->name, - 'frequency' => $frequency, - 'timezone' => $serverTimezone, - 'execution_time' => $this->executionTime?->format('Y-m-d H:i:s T'), - 'current_time' => Carbon::now()->format('Y-m-d H:i:s T'), - ]); - ScheduledTaskJob::dispatch($task); } } catch (\Exception $e) { diff --git a/app/Jobs/ServerResourceManager.php b/app/Jobs/ServerResourceManager.php index 9059bc6cb..cdf8efc56 100644 --- a/app/Jobs/ServerResourceManager.php +++ b/app/Jobs/ServerResourceManager.php @@ -59,10 +59,6 @@ public function handle(): void $this->instanceTimezone = config('app.timezone'); } - Log::channel('scheduled')->info('ServerResourceManager started', [ - 'execution_time' => $this->executionTime->format('Y-m-d H:i:s T'), - ]); - // Process server checks - don't let failures stop the job try { $this->processServerChecks(); @@ -72,8 +68,6 @@ public function handle(): void 'trace' => $e->getTraceAsString(), ]); } - - Log::channel('scheduled')->info('ServerResourceManager completed'); } private function processServerChecks(): void From 039aa863212fa88457333f163aa3b20dedad9874 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 18 Jul 2025 23:16:12 +0200 Subject: [PATCH 0090/1717] fix(application): update service environment variables when generating domain for Docker Compose --- app/Livewire/Project/Application/General.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index ac2e6794d..03b67dce6 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -274,6 +274,7 @@ public function generateDomain(string $serviceName) $this->application->save(); $this->dispatch('success', 'Domain generated.'); if ($this->application->build_pack === 'dockercompose') { + $this->updateServiceEnvironmentVariables(); $this->loadComposeFile(); } From 5ddaf3a61fa378c940f5eedf79223617bfd8889a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 18 Jul 2025 23:22:24 +0200 Subject: [PATCH 0091/1717] fix(application): add option to suppress toast notifications when loading compose file --- app/Livewire/Project/Application/General.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 03b67dce6..58a35caa0 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -215,25 +215,21 @@ public function instantSave() } - public function loadComposeFile($isInit = false) + public function loadComposeFile($isInit = false, $showToast = true) { try { if ($isInit && $this->application->docker_compose_raw) { return; } - // Must reload the application to get the latest database changes - // Why? Not sure, but it works. - // $this->application->refresh(); - ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit); if (is_null($this->parsedServices)) { - $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); + $showToast && $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); return; } $this->application->parse(); - $this->dispatch('success', 'Docker compose file loaded.'); + $showToast && $this->dispatch('success', 'Docker compose file loaded.'); $this->dispatch('compose_loaded'); $this->dispatch('refreshStorages'); $this->dispatch('refreshEnvs'); @@ -275,7 +271,7 @@ public function generateDomain(string $serviceName) $this->dispatch('success', 'Domain generated.'); if ($this->application->build_pack === 'dockercompose') { $this->updateServiceEnvironmentVariables(); - $this->loadComposeFile(); + $this->loadComposeFile(showToast: false); } return $domain; From 7a110880c1e7bc36b4a841890912799746310945 Mon Sep 17 00:00:00 2001 From: jvdboog <110812872+jvdboog@users.noreply.github.com> Date: Sun, 20 Jul 2025 22:15:42 +0200 Subject: [PATCH 0092/1717] feat: Improve detection of special network modes --- bootstrap/helpers/shared.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7ce511f2c..4e77b35c3 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -614,10 +614,14 @@ function getTopLevelNetworks(Service|Application $resource) $definedNetwork = collect([$resource->uuid]); $services = collect($services)->map(function ($service, $_) use ($topLevelNetworks, $definedNetwork) { $serviceNetworks = collect(data_get($service, 'networks', [])); - $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; + $networkMode = data_get($service, 'network_mode'); - // Only add 'networks' key if 'network_mode' is not 'host' - if (! $hasHostNetworkMode) { + $hasValidNetworkMode = + $networkMode === 'host' || + (is_string($networkMode) && (str_starts_with($networkMode, 'service:') || str_starts_with($networkMode, 'container:'))); + + // Only add 'networks' key if 'network_mode' is not 'host' or does not start with 'service:' or 'container:' + if (! $hasValidNetworkMode) { // Collect/create/update networks if ($serviceNetworks->count() > 0) { foreach ($serviceNetworks as $networkName => $networkDetails) { @@ -1502,7 +1506,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceNetworks = collect(data_get($service, 'networks', [])); $serviceVariables = collect(data_get($service, 'environment', [])); $serviceLabels = collect(data_get($service, 'labels', [])); - $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; + $networkMode = data_get($service, 'network_mode'); + + $hasValidNetworkMode = + $networkMode === 'host' || + (is_string($networkMode) && (str_starts_with($networkMode, 'service:') || str_starts_with($networkMode, 'container:'))); + if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { @@ -1613,7 +1622,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $savedService->ports = $collectedPorts->implode(','); $savedService->save(); - if (! $hasHostNetworkMode) { + if (! $hasValidNetworkMode) { // Add Coolify specific networks $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; From 1c2374120394a9b0e82ddc63ad8bc8d2c2da02d1 Mon Sep 17 00:00:00 2001 From: Gabriel Peralta Date: Wed, 23 Jul 2025 20:05:35 -0300 Subject: [PATCH 0093/1717] Create newt-pangolin --- templates/compose/newt-pangolin | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 templates/compose/newt-pangolin diff --git a/templates/compose/newt-pangolin b/templates/compose/newt-pangolin new file mode 100644 index 000000000..1e52330f9 --- /dev/null +++ b/templates/compose/newt-pangolin @@ -0,0 +1,19 @@ +# documentation: https://docs.fossorial.io/Getting%20Started/overview +# slogan: Pangolin tunnels your services to the internet so you can access anything from anywhere. +# tags: wireguard, reverse-proxy, zero-trust-network-access, open source +# logo: svgs/pangolin-logo.png + +services: + newt: + image: fosrl/newt + container_name: newt + restart: unless-stopped + environment: + - 'PANGOLIN_ENDPOINT=${PANGOLIN_ENDPOINT:-domain.tld}' + - 'NEWT_ID=${NEWT_ID}' + - 'NEWT_SECRET=${NEWT_SECRET}' + healthcheck: + test: ["CMD", "newt", "--version"] + interval: 5s + timeout: 20s + retries: 10 From e16174d96b420f98ff90687d48fabdbe56027fd3 Mon Sep 17 00:00:00 2001 From: Gabriel Peralta Date: Wed, 23 Jul 2025 20:05:58 -0300 Subject: [PATCH 0094/1717] Add files via upload --- public/svgs/pangolin-logo.png | Bin 0 -> 33548 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/svgs/pangolin-logo.png diff --git a/public/svgs/pangolin-logo.png b/public/svgs/pangolin-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fb7a252d910a39b3f06b63e72d45a77db336ef55 GIT binary patch literal 33548 zcmYgXbySpH6JNSZk(68%krL@vy3wT@0qJg7Qt4h&TH2*k8dg9+8eBkn5fr6CVL|G9 zK;Lt|KRkHwdS~W0d1vN{ex{*FLP$pl0)a@BmE<5G5Dpjw!l=i`1%AWSJE;u(AaGSO z@Bo2Gd$0ds47j|r0)9#BDX;IT$IZb+}FGUr;KP!w?0v)eh=C>1gl4s&y6G($n4lHNZKC}L~{M0DvO!H&SW2Ux)YUGC-3dvcpQKe~Q z|7X%1eoKW7JmwsdFr*(L1ubc4{!5qxM_Sj)(uZ+_R&NO}uABRd6>Rt+UVO{5XO&!i ziL&~sVyYx+pl;T~HKlU`gMp@{jKx1^$b!F@)5QaqkLC_TR`|pD+mtXogYA`5WtBDOhJsFGmBodV0>rFpZF2=UM{q#NB}1kDMZK}UIe`x)KXS?|n$!jukM7t;;d zUcDMfxA<94BO>9Ij@w9kVo$uPwPa#q_MVU9=FThtAHkgXG6s<{4~31>P*=lEE)q^x z_CC$qz2DOAfmhaY-jw_kyHQ*SJYC!Whbm7aiYay?O-$7?)}i2cCZxZlf!7q>fVBGy zk9R*ndfs-Wun21|4Ld* z(8b3u`622rJ!&;m z%j`O++XzL@*S)hPq~IGs=3fKUs_H_Q`7PF^LoOgTcJ?em`CI`l?q3EsMA$V-6aJnv z%mB_cjb2D5KBSN$xtlDe+Cb!b+x7DOSQRy6t@~Oj((i_O$W|RKyi~X?VDjmU$7hr< zc0jwV<>a{HDgmxD@>?^-|L7VD|h>Qp3lcY`p_gXjo3lV0Kb@%YSGtVX%z;A z8xaJhvtjle(_MMg;p99q*Gh08FpE6d9YXESa8lZzVh|1pCSW32`4|=4X!hu3gIZ z3je3eDZrtR8w64-OeC4^?N1?}&-eM_2Ur;m%%K0O=*3^EVt`%0FTWsK!98GjqLf>r z?ij__QFT_ks*`iO0r~IH1z@g56~{+;GAM|-?k1PhdhmnrS^vTKn>%&@IOk29x;K;& zHWGI2U6=uOtcMTLDXG{03J?4emm0EJpJzGPlM(t6-q4uohQC*(z0sLJ-MyUt&mqdW zvtd)-Yr`98>)Q-wn2cFss`P&>@k{hPs|=y)o6LXJ;q9oPf_6VcobMVKoPG}5Z$ui* zKZ^?;?vV?S{Wb+7`OhD|&c&tfI{f70FHaNa!7+U!reZ|6s7&kv=c4VJ&}zQPBKF)u z;1UJ`7VUAv{iia%PMZ&QIM03JUc80c?zm|EGp3KVv>ZD<)1jXoM(fQawfOg6H&yZE@>QbAD&YBbQN7BZ&4oX-{UDak8cRonaM)F5u{ zTV=-`^Lk5nt+*1$?^b^9TUG-P5Q}Fk9q_zip8;H|E}{ODr_4wPwTlvbY3`3-Rs;nO zEB;x~)bF`CB&n0c4nj2d+lBv+HzG+~cjY+l>c`y09>F7xsg({Bg^371!TAxPA6grn z%j%5LAEGW-j@{3q9qp!t?>J-*?u%zh-39H2wD2W4aeYjPKhv>xL3KzwE@gkXj%$pv zcd^Cn4?fx^I$|xG7(6~?9E(o*57AB={(1&(GXbdM0+PZYx)Cp>v$(@g^&g2%6r!I7STr~G!X=DP70^#yOUF+m0jO>`hUrpCoM0THY4JBakYvL~o&A2?U3mZF!=qDXnV)a)$zGk9qk^7r zcuBxZEjmhbypH#LK5;u}DgN_bK(0(KN$Y-JbpP68-v@k9eKrXJgc~9bIawSg)K8x7 zRbz;5{pX@n)pK!<=05p>gDhGpDT&~)+aUv(1DLtdmdf!Qu21h}ANfZ(FD8q{+_=R& zUp9=S*k5<=HHVu8n-o>g8WxvwuCo0@nAsGCsdlUD(h8E!UAu=d-C ze56L&S0aljcC19KAvnLXlcN^#LENqmSg(xqMmvyp;J&0uelpQVo>t?L+r$>WT;vd& zjW!lv^azCj@;$CdZ&~KWEPiY@kw;+E*Hhk$z+@kCa@+m1= zW$EUA#CP+=5{m;L(^*P1L2cJ#;jeF?L~1xH><+~|qwDnhpv;nPN^tNAP(GV-cqSoV zM;(v^@cuywHvw)8y@{ii-```f^7>E}Bj+)Ry>!R|I4XiWxxN0vXqY=X2!-=(iAKcf zJryBAze=4~L}lM*Fv<@`48>9rH)RO?PNNn7;qIk;EW7OSF1aX)=0JL54JH$uIH4m#H=yTlq)#v`r}evz z&KLD#&HdzNL?bG>iVz}#gPx&+Ewdp}K2-+!YpoYsPt%PK-YF39_87P0bLW{z5lbkZ zp)`Nz@M-Wdee#WAsB3-}1`nK3OM1pQz6^;h8)JnZ%_ZH?vHO$NOxqeTxn1{ZJKu7h zw})Q0ZeY%m zSKI_aEd$Iid$_z!$z^<9|}TukMuw$V4jyT4gvUHXru zBBjr9#&koL7o8h2CstwAY_x3f3g>yFa%lOj129EetFcre ztStSr-X!q>B3`678iQ0g@Vhs`nPDeXF1$@nUA7wh%HPJrcxwHxN+{88wqE~% zcr_5F-$p0$jt5HxVmbFFt&~x4qmU$6jX~~4VcMJZHdamDXdIUGuJCvzg|HPVBEr2RiOBa<@i0NKvI?Aa`?Q&VOwx`}H z;K7T&0SAds*p9#4;ds!YA{r1om=asgp;C<*$Qv~j&DF6j18_XpROAbQKQ1}*JBJbP5IqV z3+JYzw}%n82{qZ2ceaip8|!Wjddouph0CT|^l;-fGMjS8@_@{cNz$yWA1x2O@NXox zf4P?h8tz;$SpZ^@nPFWK;z zriw-0s7HQb&3fH7c-)A&t27;`bwMMeLn%zE3^F%1Qn)jTA!La~q#4f_go)pj$fk_R zyx%iazB&!Jyh%3NsY^m&pmLMs?%!5@ViO1?%J&Q8;;Ll3rme@v|B8l(Jv z?f6=+$4oqrNvUuGU~p&Yn^MsYOZZ1(UQfKErD}Qc*9%t7YM(cBbnLZhgG&f1bE44p z=og%i7^4?t%pGeg6s%?5kkq9-38xWb_EvlliMVS)On%J!Cl-eEPN=Fj*gqu9$@mu{ z&c4>915WL4rhz(_^YgZ%?^|GYWUht9wab`mQMGU)Oso#7pZu(j+va(&q%%}1l=id5 z0VrvIEJpA0>0bF8ZStB|?xFH%hVro(bAdcp+QiWZFKUkviu>naQ=S(eM+PTDaS{m% ze(!nFiau70_F}QbZiy$4h@SIDy;ss#G?(4xa(c7NQQr`wF79L*NR&?@dun0+6bRH= z^NW?*LDH@Y{~Sbp_G&b4@!I^8)Gs9v$(WlOWy}#Z5e4Yi?Y)TVY;Z;sU zDh%jsk4bL&fAxx;rU<_~Z#<8g=QFQH|9BuSlbAc(*-y>E{_16QWU%wm%&Z3&i!sCM zHPUwN4_&)~=fBgx;MFKT2Gogbft}+qEXEFC2;?;!yGOeC9}jr|4~#TBTY*~m9eCNz z$#hzR-sz-!|L3gpcd{MY>w#BuU?&qvD0;O!t+-!@AZ3nIEAplgvh&HIZRM|#vbSzB z?4;Ds0!saz(0I#~g9)6Bgt(Hw*=}k~WWfbbD7S4rWYhMD4v1U!jKA8%WX%3W$DNqb z!7~~F@>&VYg=T`BH)64DJMo|z8~Gnf|7wBUI$A&8=>}@4N3q|SZj0~$^~<~w1u4Bz z11j}DYoN`BEyVL0c}i#5SkkI$x z?%uYu;aju+9tBgKO*=j91S!Lg;eI8O<;G&$n%~WO}VsoR4pUd;#LD_Yod1ZYxUHCw$DWAM)Wwg!3z4?Fod?l1gYp}ByC=<78f2{{z z{+{=6>au_KAZge|E9(9I)f#gUqvmKjGQtAC>^}e`4gE8ZzrVy|G8h_K!SaUyC7qSZ z!IvRo@gE)h7HjRJF>+4W7g?OZs!U>W;2XTx3;AnjaIja1Ocj2~X5Xt}W{LZN zH#^xq?70HXreXHN9^J9nj^g8C4a37AW0_Qc`{p13RmRbueogr&BlmRNU6aoq&LQ2Z zxv~&wbZAZT#|jyIvu(z^T~0Pk!d(GDE<%%jkNc=GgN+8F`XB*}`|17(H!abrz@`zq z`64VyES88^p!P@V1}SlLz}+X?_I7vJ6Y`pVgguMdi#+*ah`j(KUvo2&1$URv`d%9x z+mfE@Hm&GP_x?|oh%3O3Aq#Inrlz!agGM~*V4x-Hnv0;9wkK|Uht|xTLdyvsA-+M9 z{Wq_0JDV&(4}e=cvQ?LUOwUoZ*Y_d!^?&JlVtHoF(f>tKAi$YTikfB#ZQIVU*DA>L zw}LRS-F!JeeYt!}5p>I3`MPxP@r|BZvmvX#@S6+=ig8U)7pW(e$(gAI+{1lxJFS^y z)|L1sR(Y5E*(g-wGa%B0Sno|Mv-O7>3b0~4eBHYeUE4v`w0EyBI-zXZ_rJq-*|2HB z&MSpn&7w5q%vcR^d_Z|RmM0l0+0&N7bW6FO&khD=H(qCAH)Or8ztZ!WrArK`S)D9X zKb?&E6Cb?aN^-+vS!R^|a`$n+MU+nlrco(t5rcu< zE%S$u+O>zQzju@It7_F{L+muz#z<(F!djTg{rP{8)ROCiQd0ucMj!SuAsuwrCEIvg zkI(@6TOoMUwo$howDtZ1V4r1e<)yx7#3Ci=MQWE-8bTksB||*>JZ9ELt3=wXZ%X}a zX>aBQ&}1FG%|-G}eTgXD^O@A+WCnR+bYfo-=3SyM>nqVpCo(Z^vc!4=UURw!EYQlK^_6fO&6rzarZaSOIYl8jHlxzLBr0>mC2B7t43oYAG!45*N{z5&m zKRRe1ycZCq_r%o?PN##rs)uN{Bwk7QHs!Nn>^am=FVUDuMzO%clT5ur^l241NgTaS)d+12&{&BE|JYUb$8Bj zy3ph5VpQn+kHpo@HA_q3YOsb;PxL@FA`Z1l#9xW)#pHeqsF*a2W=Gbx{6&J&qH^gm zB9L#!XKCKeIx0I` z!{jjQFs`bMqGt|K2QO0Bqgt~JtwBN;l>!{A1_qsO~p zFFWV!EmOz>-_31*8Y1lfriiiV%x-K_^;~&w7s3kp3$}nhzk6kk&jZ{s zm0VeFr&r{>V=~8=qSQ_&z#wSH{(@H0#N?an{m@`gf?BRkGbXjRE=hNxX|E2RzckPY z@S?FVhwS{YKH~P7eF4lIQ@8F*@4M_CD^fp#p6v}Ia&iNfh0Gn#_VyMpx4s;A=Yw`z zW-X1lUv1TRg77f#l7pExKz9xUKW~6Az66tPs$pa|UtMaToXv-iR}wLrQ-aOtH@=!A zA3yCC0G>|%dABTEBF+hn9vWin+LXYX=fWJBRb~Aur_R38d|ilMl=jLoc{BJG`FhK( z{P+aX6KTVD`9@5VI$g%MZ(C<8$S|>PUQL*!jn^ZzKeP%i+Zf`1(OsW;Zqdvz%ItEB z60Wj*Zl}ZlDce&1i^^rdBoC&mi0kiTGcvqg#jZmK48OH<5gAGUg6Od-88I#S1jitwLRQ+`e zN4p1!Ug#V;uuIuf8x`CW3!vBQ0sofred<}YuKy*GZ@_4OOX8CNyP3OfrxHflTnH&ozX#cvVjCMoy3tK?AU0poWmrxB zw+rAiOyMY3?s^c6RU+&|QhjE)$u=K< zPPwaUKv7KtJjVoBuWlW-kCA|TJb(_HO9evQK~SN|;X^lMK!NQEG0RalNU-@y(2GZ8 zA{OjS4e93=KiUzqgS^gZP^u$4Im{e|Ga<0U3yyt*b73dNJE%;AM@VaDL_Nm0>F5&Ypn+zE?0$oLdcWWw zRfKdGHJPQ5VvVtOhq}}6(;m)ZH!G&ZpxIDqa9M%Zjn|!4Fi|C2aA?M{PF$#_RY`*7 zbxX1Tx*x0bS-^8G9cc6K#cF-)Wy!%)D@|xL0Z9qY&z_ZjeHiiR`sWjU1O3rX+Mtbw zxne^<8dn}yqDRAG3+UkLHqW&IHkKVNy9w<&tN!6XO!cL`}h z&nV_)Xn{CQg>1bCl3hp+YZ_NbYC7FSTt!_;PG=q_A1_&k2p5x14kinN z%SuZx(*m$;?b?Ds5{>otCuSmhWb%32CAdHXboHvTFLQvR2RbOMt?7gTOVnleztm>< zA?dPRnaAG1x_x3^tH&hhQR%YE4|w0za}p!xxP1(ZCDQ6IX%m7C>++3sPsvV$beRf{ zGU6Cia>**@c~IMfhHq56UUW9ZWEs)=QoC~V)HvYq;smL6t&|*(XbqBa_-kx3Lt^zq z$DfL)tqbgIKG|DI_7_UZGN8iW@aCqyv>A(zug`e9`SM3U_nninx~@D;ik@%^#x${8 zwQmM1EI2yK+e&SS0z13e!$^|iW#IG{ysm7anIwKyVzksrd-QCAuk z{RnD^7*wjyxiWyU_8VnbvH!CKYIc^Bbl(WcZyVz0R zwy&%kU-BhnG#t**l-<8CF4H71R|h{!Hrjiq*X2D83~)Jh!v=JL2JF2sAB-h9zmsq* z!j1gX3fTbC&zrXo2M$(3yRn`smuX@d9-RJe46|He@?|@XW4W_sjB;3qeun#vuwQkx zCI&i)kZIO0>Af_+cM9DKR?U&O_a4AeoV=-ohl_0wd5!w!ETWeGbOGuafa4CmVB4p-*&U zojKnm<&BKnnHwGzQ3q|g|*7#^`#q9|j5Al_FlqM2;JaQMs`6bHn zjtfG;3MK`r#N$zAkh6Dp!MVJ>0=)=t69QjPQR={zox<`cle9^gPraDN-sr`3tM288 z$rt6G0aW#CSgS4}cwX34Nly9gR3S7mhH?u}yRHn#`YaFA?~#KI;K!=Z(v|yHe}=)ZAF6AWN(Y#MwYE7pb4ep{Gk{AIApzX*8K#S$|7lzG09puJvB) z`Uy%PadnJ4hlEQOfP;%4ZDD3DkU4!3Sq?7T6%%c97Z6AX(A`WfQ z9}@Vj+=Vb_E<2W`@)bX~`2m zkZjRUuV4V90h#bqA9J@&a_;TW^^}<9EU_r$x~ZZR_-Y>cl}#BZLs9+8RK=3jL01kFWv1gVl%U6@k$+) zk7rJCbXVMcHhoEUfYpbch}rlf-g84XQ?)KDQ*jFVjeRCNo3>9GE4eVSK`9kK*U>=9 zjPsRgdXuJuM~6h{JMh;(YVIm^{YGY|RjrDkej{1t5z-)PP%|*;60E!B%C}E$=pg;u z4nC}Xe7zvWdicseuH-v4mf6y+`;6Va3hRgR4Fi9mrp7yFC#Tg3uJo>V`ZAux3bJ0! zkI?uEu>{l`X5MAoGV*=yHP>^S;Dhi~`^5FoSFJx1w4$6_0+UC7(e?nu67z}*d_?bS zx=D_HoE_l(#(pDzFhi)kt|PL81wON{VVq||))3MR^tzftz#-@|(nL4yxw!)ONr?;uQ&O4bU37}loVz-3d}~1(K(bdB6Txs?{Khr-Vs*^ z-{VsVIS4%^YWOn8^wWSUN>xUUp_WB532}P1WD=y=RAcji(`IA5)wY*OE7D$sb4?D% zRCIp!*DvxO9N!lw)UWu#(-NlG9qHeV4Hu*0yVFXy16$NU!{o%#pp6T)dL%E?qpXye*P)zR#}wnL0!VQMW#R z+X?=DbdX%;$Cg{o-JQ^r11KFVJL5>IKJ_4R;^2zeyErIvlI{Dg9tTx}Q=RF{a6Op- zAE2|)hAf~{2Zr#d?_GIYDl7rG`ftQEi=xKj9B~~&;9?0rd$C0tbQ?WhLi+)S7v5Q) z0#HBUvfEE+efjo3AfFm8ZhaE2yxK!PonNdg&XyM@J|{l8eVPZ|3Nd8ep6{B<$pGj#^gR~{#r>~wE*z`$)!uMD2^4IbyOzg!khM@7T=1?$|q zBJcB-ll~66uV%fs*=BXY3+Ro_BSIlaq$}XEw;?)V95YcPw~RuBzn()P>yPRFq8_b? z?U|*NAO97`dP7$NM#ADH-_gEtRt@c`3>})hTdR)Nz~SZLl}+LR$(EKs_$XT*&5u%D z2)>~8`&f=-704?z<004TjXyKe+38Z$Q;c zm%h}ej@usZmd@I3%=sY?hg1&JZUeCZ;vxt=pxH6ozbo1ahX@ zd4#ixM@L;(!06ZL0(l_W?7rhWn+)5)p;2Qf!LVw-BDc$M2Ios2N}z%e-H>)#a}V2U zgHgrgaea;vIMDHh?)}@p|2T}?!d17}+lI<6!>2JNlW#wf z;l(gh1y*N`x!t>2_o04ojht;dIs@E;C3q2=H9th&P&H`t2t1h4GA+Yzf}gA6rYakZ zcaGgs`PLu(;Bk>RPtpn~yP=&-dsU<6#ZJYb%dKtu)`l3t+b7v{$R)MQKZj>@$m(W? z&?`l!tdEKPv&3*E)lbd8?KlPP6vQ|o_WF3YPJig@jRz+lF+gI4-A38_^=a?=(VtGW zdOHm@ZOvBAx8((Re@e7}b`~x8&VW;XOspf|tfss4OaH(vlpD^>=P?GtfCpYtLLTI< zGV%7on7$OXO$tX%mIKeTcb=HaJB&JBknShYA#+u`j>3w+=8*j^C4H_5(2t;5HjZse zj-kLxjC--kUUbpPq#vx1$j*TUtVks~OP`lz9v5)N0NdU}BKtm+!M?q90t1zCiq98u zjIgU)X7hPh=I}H=+u_W99y`KfFCm>&$b0HU`sEK#EwP!bmai{~eNbXY>!Q6BXz|(W zHnNJR>hN3!`~V5BDF25Yw4(kqg}|m8rHLM7RSdJE07BYb&SL}=7?0xLJ{EDmw}0!9 zWMLti8d6^JA|^kR4^Q9Nb=2@d{RcD_N)!F(*SGbvraRv#B}U5gbf2H4grq^eN$j2P zU!5-NzMh}&#G8Hlz1?<9lGVqCbKgbq9aN3PUS(&O?T|j*sbJsQ>Valw!H=wDdv;t> z_ShA=EwdBG^vE%!AEbhqG??zIU89cdiyIqwcqcw-K8R=Yf-|O_Dn8U8ZtfcAfSc4`R+lI|k_H9XU*}9{%av zWVahNfT3xyx*)Ej@4HQt?5#*2$$2;`Hfj<&DMU;mr72CcAQ#xaz8u!l^z`~)j;V{& z3RZz>j?MG!$Abz@9A2QN@T-R{rU2Ckhkm~+5If>0XX79rYLPMJb$&J-=?yGNpea_X z2w>dWA|YO4fMCIy~L)C-gsS!&j9~bjBK)B`i6mPyVL&dNi8kh|G^*a9R(@3TKWg>C4*zuPne|f3-DQd}1(=e7S9WSQ8H2NyS!YrF^+J(dWzNEmh z`cwFv7i_Q9R3oRzI>zpL1SEIam1LMq_h|)Fnqh0p?)Pg}`*P^?;2FMgl9*x@c8>|1 z{~fm)rtls88dx?=KCke?z&jA*^p1B&7_?0To0PLQ?|3BAE`?-k>m9blJ5<({fve%&2Occ4h(XZ%iN|)LEy3n?MAo6FpPCjzM<#lccy2JQLOXky??~S z;sUIdpFc*?1L5U2WgvJn;?t_a|2_>G>nC)I#7*;pM3q+B78aJxGVQwB`O!vEyx2J|`jF9%C7`u_p98 z95D=uv}KX6**sxzHTodfgwCNA4eaJ^$>RrP%O(f_2D}W#wr7iRi;F({&&l&4ax}^$ zPWN43Hg%)Z;IG*lfNZkOC5hL=1X1LG#2w5aX^}!gRK_k>fWsCHN>WP`sO@|h2+R-A z94nN-7wJOy&wK3QB`7fz%1gXO`AY+&N(VAwpV95Fwt)Ta%{AY*-^Lz&*|4_4FUeR< zz6Cc)4M~gx_O*bQkanA1b*E&+{#`>`tTUyKdtSIr@rBv22Eyv*8b-A`^$tnNCgI(W z1G^k#^~!5`BwMlKR`r1lrmm9Nu=s)>xL*Jh6|65@*}EpRk^kWG+56(-o%pe-k7qcq z%C3~1?)U9415JwA;xU*Cy1c0y93gdx`{Xlc#_(92c4yojV;6-FN5?ual8&v=tGU=j z-Zol;b$5@Q+yRUlRfa+w%B46i|LBU+vL7$!<(-+Pe#9(WBdm+HrE?K41(xGa?0^+3 zt0sr9t%#0S^-A`vd|_lwzmj)`Cfdet^+2^Y9`tCp?u&n$8gt}qF$H(B5Fu0vE78%N z=O=(2bGx1wl6|$iV}@gD6zR6Ce8B#+IlGsz+DGfdk$@x~wZ6kG8tz)tqV`UdzdUbd zM%LWz5ExzJ!4q@&_x$7cN)bnJAZ+{vodOVrFn^@ye zW`LpMP>Ay9C#r7dWhCR`c&BUSA2~%SDSp`aYOCoLA4&qnDadF;= z0CZ8?RZ@LC2h+i+SYEevyFmQG@fP45AYX=IXC=H z07gq=zh!E3;^c4X;Lpwq5eZXp-)7xgsj~PPiMg$)7UWaN8|Z14xGYybrlWf9OEHV6 zV3pjbP9;CAt!zFVl{(zte}%cF?h~fdcyd3gC+C)ww2khAYfmTCTW_jpeLVQsD*Z{t=Pni2APwy41KsVx&)*i zb^lQRm@P9qQFTR+@1mS*X~^2mQK^d`N*>dp^lXdc4w`k~t=W_ftG|J9bAe@q@hV;#zW^z9E>$E+albnV*qkOue#6?qLP z&v=c`-rjI@FaTHkB)brR-#(UKtlIkLSJsSi0ieCP8jRPKE;b-QITP2h^X*r=!nnSt z_VXiTa*VTrGmug|@66SnJw6jM+VG%++Mj+7ImMZA5FCbIK*lXYgW(4>Rn|`dq#B%F z#mt(xc6y6-d}quiss=u5`{Q7Sp*!sx2O^&8gEnipSX;Gv+J6vbYkD=pjtjPe5 zrm>6ymTP%a9O+=bCeNGN-uH=ms!6_o=WnlECBlCQLK)!9EX-!c>l{kP#!L}()FS+= ztg(GxGW4G+qWHXM%5*3tfiD?sCIkx$>CJqojcaHrF9fNVn9~hEZox0&D`i8;02Z<` zb@?Cn{E8)Ih3kZMGXa@5U6`v4SX~RF87ubFu#Q{k(S9*HScw!B?whz()(*EebhBRP zpll91=0Qmlz0>*)*?AwJu|kxODFrBC2=USP?L=Odo@*y7*~J9#EytqI8qTbTb#bF* z_9wFTiX*QV(ykD|`Jy|4E#d2(9#=UM!0##e`iX>i#ZLy2qL3eShG-ppCP4L?axJHC zb(-(Im*a@7qc7wKH*v}fbaF1;bj>9e1tu)IVED2X&%U@nN2PsAhc;O_!#PQ5ct=nGs zRxAY=KG`p=eZ-k{5NyWct?3o1!M=P?x+c)xiZjdUpD(GdIT>H0&{Ck|BH!)Yre-wX zB#a@9^o@HRN;c~o*WAe2hQ^IUv+e&LrrVHkMh(} zM(f!`!o~Vu6~n1UMBY3J`84os1ftcHSGYGn<9~mCH%G-ydaW6>!u^hFUA4(f$Gr3N zVE%aaOZpf}Db?j?LCPcp4=`jAZeN%e?${{2zi*?kD{iBpa-@R;3}s}yzNu7oM4glO z7P=_Rj+hF9D{PLW-rQ20eHe%A`f7+T&3x#fV$4+U8#w#rX}9H3P4l!xW~AZ`f8FtY@M!tflmi zem3OGCIoJ>lotXErvzDB!Jr(b7*;qlXB7K*Z{OYCO~huzp*-uD^f#54@~CT$u;jwH zBoS$kuoP%7)TCazj>?*o&{8pe_Z6=@#+vr>D|k|b9i{ziIhsM1(BghzGPY-}kS@`p z@NjUCIY|6jn>TB}1Dc7h6`uEcyv=qg%i#2+oBJ=WYz^e3h}r=*W#e`AM`Cvw(S8Y;g15{MT` zR4K?nhQp|B-Z@NI>6JJf8q86<39{Ue$fQsK|JuV&wlhklAsLA46rEqVk+hcSxGeH5K)RKt*t3a!ZG3N8S;^uPzP z6K0}&WJa8)zYq36oIk_^bOjy#w3#I&BTfRo3gnAm0ZUKQ?UnRATmmk)-}uZiWCH_0 zv#juloO>tPei=*8_iuH7!NRZ*dH|@A-eKdV5k3?C;!w9`+L#I^8QaN)B%a}JhM`M zXCl*T%1RabkSDrbi1|)+|RrJX$jR{n7K2b ze)4A2hUp!Dn3YSc#4OQV+{h|gCPP(nW`1c2b>#R(6mh_~rBjc_q=%SVGVaHx&OXqS zDr>UnVIp85SkXk`Nz8KMbp?;x3F*i^93?^Msnm|m4t^i&kc_(!+6i4eSo7~tI@;`C z40r2mAmrZBl3=?72&i+Ow72=S-}5A>nQU z)VXdlSx@DL@}by^WhPFZwgs(g+(#AjE`+%cxWm(r7d8#&SIk^Lf2Zf-8+g~PXR~Y4 zIlNKq_cqx28}IhEK!b3?PqSgNQwxYjR1o~qrHg&@@@MO^?}_7}iEP_# z&y3aPOn1Uh!mW?nDmX?d?yX?bb!7!6{kIFCQmY|)Fc(+Z|FgC7aOvdG&rZK}{{lKA zf>>vgGq|C-gi=-f_`d!WDK zpk#3NDClZ=4G09O4V?XUe`C096ExmbfHvoHY z8@oM*>L1)T2>+hY{EnivER))6S!;Hxi!C8W)BO{5_fq%%^mi_Qmv@c@cMKPT@s2Mh zL|HwSvteV4jH8Os-{(rOl#p59YCrK+I&7(CTeGKj&Oa|Wl(j}OsA0x=td<@mu0aGd zr((5lvA0rRtGtOz4YYggei$IkCIA>T-vuJG`08TYLy2d4IHxX`_Gbr^7%!wcMr7=h zkI(sR-1qn0VWcT9k1B+gOlGHoA~?U3;sqV&n>`xy^*z1RfiCM;zc~X15q z%p#Bs%qNk`hP5H?iw6jIA>(o+)D>dX7P%2+snB8ZAO z=Tvgv^I}e)AI+juxCOxtvI~vu<33nITB9Ve#{y5>-~UcXo%MV8iFP-W&z~Q$?RRgG zCHdvbn2%46VYmH7`ozX6u6BOS;9yssw4bxV%b}JT!xKUvgYf zkU=zv2`u_cKpp4povQ(GDp7vMY{+{%o9HZ9N}1C<>zoIts|H||G|F0!Z!0i)`FJ}L zxJOx~xH%WJUgS*hJygFaSGUX0PV);7-x&o$O$VN@1wlq<(4TO6p%b1>C^y#?VX?f}pkR)f!39fj|?v&(0dHwPK_j2(QW=v}t(J$Ap9AkHLapx{r! z@25|~f(rkH+(UR3pRrx~A)p~O#VrcDcAPHXApGu!jp{~h*V5fBw{NC`m%sUe3_a%iQImhSEr=>eRf`yt%| z(y2(pfOLb@&?zPLU4-}k-&xBga^sv`zrD}i`&>iILySVfo6QG()_VAq;xC5p@y^Hb zcy)g1k*&&scgxS({{Y*ImGJUL92Ug%Z&fb${_?G}E%MSGcVN1I>MK8aiG7aeJ5@-E z(f^E}T%~{PBrcXdLsq$5-qT-;sN; z3}1iDAq~ZXNp5=O!0aO{1nwI>)+D_D}=}N=EZDw65c-*AXXe&s`L%&1AX%7-iIM31o;to$c&Z zKC5SMCY=YSre5|Wfdm~B^imhSh4NkW?tLxke>v$(a{@c#JHW%vU_qKrObseWt<4{QlOylK}LYYc>S{=!6*46hnxq z^x(<7ud=S8cqSK}d>=HQjKD{;`bz~aN;GBhnJSCR9;1@(XSP|6k+YYRX#DEn!8?sf z@P%-?TDNCE#!!!*H2o6Rm4Ko)NA33;Rkf-u6sL2cWLxG*qJwwXmL-%kPE#U_t|ww7>G`EO-;A2r2ACtq4-dDjL9*-(Xno;YiP)-`AMP4+P zwk!T%PXP+(Szsy4-YF{Ix1(Gv8Zb;SokVx*X^XA0ZmS=j-=CN<(BKz6ms+ zfG@`6Hne{Vy1<}F_Vi#12)SdJqBSdH=9xL4kussn@yS(UwNG=_6VL(S-QC;HMZKl2ZPC;wHSMt)y*^9yHJ`oxn1m|i9 zK6j+OBeIZMi;6l*Ad?a_{Q%;Xighi7>3(Kcynux?$1jz zjVMGj&@n0k>sfAt%duWQN{C&69QfHT90#5dMf{eTdn8LNN`D93^v-O;&2yE4*7M6M zxMo}tJXmw$x6+qyZOd)iApo?qmLmPGJu!2j^s!kNqdFb}A+<|OTVVbFo{@ygj5CjbjE?Gi1-0B&ftOg}6F z_nLV`Fn5%B!^^;)J1Gae#%r6b@T-vC4tL$EDxZ;@dPY6dgmnppCq0Yro%|zU(uBKI z^Uq#M&9k6|`(+A__DlkS@%wqtG|P3`lDKESUDRcLZp zx!)qg+WJqs!Alss`(=ViZB+%z&T(RCn`3XmU1!9K2FZy{-bLtAZ#qkp_rSr<2VzcGdF>k@3VLDzR5*!70nVB;U$@wbztBG6TDsdsyY?rxhGN! zi5Y^jMikH|!HVSmx`SvqZ*m;%13_3OPIN`9X1iro!YA#9;c1n5~We|Ly^MQ<+L6t$!jjR{SnijO`&OHs`Y zCU}Fc(027`*5Z(Ie*Iywqdx8^6R|wNb$LmJ>)k^VoHn}}*Ub}9i?pSbZk6Ht2bVGl zcb_k8eigNx+Nn<_(=CnofHAFBlXt=RH7JsXlgE1qa2Y${!dN_(z0^x5;$d+8z0XXK zH;D~>yeJAmYaNwFs84QE<$Yub#&a!b4wZl(j*DlH%DX^;^`vN#- z9GvTTLs{E&%>OW!lVc%@tPcy6o97guqv}NN5R!Iwx}e3y%_ye zTMV37{nW44``V>H9fcB~%=LA0*hhVVO*fZXz zbF5##`Z|;z_u(SM%a`^=Wj!Z@8^YXptqUcqA)x&Hy&-T3`jM-T-rOth%s8%zk!}Rmiw(?vA#|NNiK1BD(^eF3 zN6F~R15TA|*CHM3*jzHHf-?>FHJzkRV77MWOee{(Pzk&D^U457oS~b@*EuHGs=w`L z*jGoVtrSm;u4nXyE9KsALp!%gFqK7v^VsWDr%EGQ>Vch?eanM6#du=tp%OWv`9le$ zg_Y!=hCB3IvbzkNB5 z`hD0RKRG!$#_mu4By)Ti7UN(C(FZ8a8yZM2wxw*D$PkM`PZz+p7>Kb|b8;(+rQ>I; ze0M?xu{Z{#%YYJU5y}vQjXWs5!#`J=kY!#^CtQ2&S%N)V=DGU6J~TmCP0UX9+lmH1 zNc>7{)570WIp$f~Rb3Uo6z2x^91<5U$&u`XGT}{rPEBj|GX#YEDjr?rJ|*qq_JQ^e z(_e$O@lzdAv!3fe@a74UFxvNcp1CzC7Lgs_a~g*1MHX4EHTY!V^d;~(H!EQwHtlz( zYAqp7J;^E`-B2-jZoj?KQkOQZQe-cvlZ$0nsIUcnX{o2?sPjQgnyy^CSpzgLQdrBS zJEr{QGsKL>FVZ%ICTq@)TrQ|f$*H!KJSCZWm+XmM zpwbVxRaoyOn-eyXo{6rw<814<36s zDZ7*LV44OJzztkIN`MM1&xwtnP`hIzi{}TFhj70!F_Et@+WmS@KIS7U$uvtM;{ljc zjqJUbl<8DGhK7#9w)dksm0^n_fyk!|(R2{Pn%sd{@}aR7X)T*dWSP+i>c-Sj=PB=Y zqCFOz1{$Z37eE82`RW7Wb<-ACvEa_UDZw#;!CNXjJWAq?qX2nMeU&KBC)L0<`txv3 zC<#6_%9bFpAK@N%kx}x-bw#6^VTG0pq}k{)3I=xQG~!+bma}eB9H>actv^jlMPot( zCo&9fxCsFY{r2Ksp11=KAmXtDwJB@-m_l z3V{!AUPd!L-oO@p21>5ivHtx6S#Q~)A@VfR5vxLu_v5o?1eYcGEcxPKwoMK7^0t)Z zUXoX-#tfP$&+wy&SMjK%3}9ciUN+nHO6W_Up}e#pMgx8gt>Y7wPdQiH>+a$?)6^6C<;xWVjUCke>{-z8mLd|MtlB9`X?GrMD}^S%c{jWj$F- zF(ifslAw@EQ5$R6i`?`12dty7T&(>=Pt^1QlPO8Zj?o9wF?uW@2f(@Yc-JGQhcGWi zlD_5Y*H!4s8ek}CvtpdM&!g&oIyRc3NqLqqEe@eS78*^)rdTY~RTCO>Y@}=vOS23M z@%6=MqJaE(b7Uq-vgj|zLzn7EU8*;7d4TK9vWk&xp$gl+NBKHS+xi-i+LIZSTNBOC zB8?Jv+Q+lgHUiatT(v!CyF=nu!W1=mkL%ZQ$`1!rJqUOA*JF$B@vS@C$Jz(<{fFhN z;{Au)w2SeEA)UTn7&Y8ZA&pxXeJ}phcun(zB_VtcN8CG16IDy~}&rT2pEJ?mVY#Hbv+$ z;5y!UgIV3)C~ML_DjRn|PESOa7(&2GoE!L?&NfH%-cj8t4^A1o5uK7F(5bJ5Lv>wTWfBFNt_ItdyLOB3t+t!Ga*3qd$c4rVzrLKT zKMQGP<1(2iuXq_L+|InSMM_w@04_jp#>%&y^yH~aKBqlS>_#mo7{xauH zK-zhMJdl~x%nG+`&wX?iZEUNbPWs~+7Bz-f!xz168?>q{l{3Nl0H%?+Ul(|*K&sZ% zAC>inH>RycKsj$p!ofcxBqm+UPcfJP-P4zDl?4w)siQVFxhG*eLa|aD7lSW*34aW? zcR;!)hB-lAye{wBt(TaBr;uu{u>!kd{dxB^%kCx*C_z#G_v2%lE&9eQ#R#MNqj&nv zF{7Wn@js>bu1r0iN45B7zo{Q4iT2u3SghTZU)>0U@}IGQ5^QEGKp-qz$;*0Mk3G3k zMg0+2kxUTaLpT*0dsc)fr)33Whe)iuKAU0&yBE0%eL)&F7X4xBl6We-Mw>i%LbqE9 z=#IW~_o1GwLwrpns)S-F@IuuQjuaT8}Ua* z=BLDK*4#x{!qWTP7TmjFM87cuQj=U1bNBr`AEMoDQhr!3N4q(OtZN*hm>%bL-E8a5=HHGgnO65RI@fZ|5`|Kdng{X8_wO?CR zE?n9@|Jg*kX4GN14L$&o6@HKWsR85^+o0qBatV3L9IqO*%P-?t^!>8M6MGIK z&5(ChH>E{_Rxm#^GGbUoAa!50LQ8KXK<4(Hzu1xE!b zx#?t=CFjHG<0n-LsnJjvQEb1G9Q~DnmARwUP@7>wuzjr<&9#NOP7PSVVH4#mPNC{gS?;`+gUuDY@BhK#%N5^{$4cWq;fbM|hkvUM z_O}k$naU0~vZSY1RB*jYvn2Dh*(t>_L3pK^z`a;=6MCZvx z=cwW9=Y8j@cTCcJdY}9GD_7MI;EJO2fR_mOsU)(<&|cl`3X{jE%e9KM5S%&*PX~%F zdne@fUk;aNBw9==ESp||32$6stY@|K=n!U9ZpY5^$Jx40QIa~W>XRl}2DYWwK}nM34^J^miZiNIbT54NKW zT^dm3NntTX?%Y1r>E{VT)pM%ho={}-LD%t*0@x|9c@=+6l#TZyQUF(Zqaj)Pi&TNTikbqoIHj2a~IO5rXAIh_+8uuik!((}c z6ET(O%`p!ohm4#q=mGQ+!@*s(q<@4|0tsWRE;Rg}#sG4IcZsBDs{IOOD#xk%I%h$2 zv!g4rY_+QqUrU&~cwemy11Pq#5)}CqVSb&;-Si!Lz7>{ZJ&Aah`|H>tVCP^pB5%Zu z*)AKEWFZ+hYprq|CyT>^1$deex1V@i8C#R)t5~zJNS+7z*aR91p+S$2%_NbhqbpR1 zi-k3gvz_mO)>jJ75i{{aq{xD$l)|OtO>u@|#;f@$zmBXollXF5Kpbcky7O`}o9{it z#x+(;xBLXKdR3Tk`Y}ZOm6rdaA3}%?uA+m}0E7_t9z|<=WRoUJ6m)pE0O56p5Syz# z@^#V>{AdWsF}V&FHnYV~Q#>DOx6)m`ORG8J-2Pz#x%r4Gw%M^cR;wyUMa!rm?Y>}} zVPn0Yh-Z;{iBISixvDcQ~`@Hha%%f&uend~2Q;I+IP!sF? z#g;@+owf+z^|a9$;g*9OXkPt} zeY_al*Ywz64tm%ZXcyr`qWcg5?AxuQOgIWA0ufH+(Pnrk>B~4i~kVRa}-a~Ee@GE^e$oE@2(I^84c=)>a&OY(tyRN={i2}^nc`Uq+vTz~3wRgur?s)CwJntX6`e~ucLXt0T{#n1`u^<& z;G}oO=gvMgZYNNA=7?WMB|2Zg()n#f_INtV0Nr-m`W0E!y9$0J5f%ym2IG)bfs`k?UF zQCyUeoE=Z-ad42i2F3c|Zh_*ZxNshbyZtWCA~bloV92js_MQttXR*;%DQZM4*!DR0UEJ zmQsWoKV9Wf0qw4K^AfL^yAnMx2oYSRY!47cunT{J5j~2= zd|2&_2%na-dWDdP z?;rR-!toUhd))sX=|_Or0;&|}doGBB_tf`677soA>SX`^dov_#u=Y4ZsFXCo3XXGibyc<4(Ay>r(MjX zvTBYL5&~uKDIPV`==ww&=RVRrgs+{vPeyB4CermtR?ovf9T1qgk7zkwHd48fY~GoE z$n0UetDibL8!jb%W>||Y&K%mh{}o0WfneZL+iolLYEFkbj9zPZ8)tOqsA>ZR1QEga z)G@UDWU=SaNIs<$U#RF4b%}4%yhh0PqS^6ESDfgzr2B8=Z}w*Z5gNn1bqk?4@pLXo zGv#4%#bp;2sR4Du$fV00eYl$evI^o%SEz=Zmi1H3a18l4I5T(h+Ads4uX9=+hs$=f z>`9b|$ts$v6&^bPZZ8I92Vq_1)P7Z6umHNmH?d_Ck`8CP+q@4@(#T}NJA6*NTl$Dl z1V8;yvv3q*T!*l0c%h^^*u`_|uqlEbg;n)7RVy~Q0{!H2eu~wZKD{TvKQKUq_30@E z6Rfs%w^5Z<96TGAx z2S}=RSPYnvC9mfAY0DBZ_xRl;+ucSX&-jG*@v&`q(Fp9;NHL)7s07?Xh$F0)G_tuq zSVTuli*OfenZub20taT%XiDujSYk^*avar-V*Vs(I_13A&4UPS*G2Sb{*gwWPQM;3 z8YIzfuJcRAnUhpf+&@i$oGdyadWMgtHte|tA<~z3H#xY$>@~WCT_}5I=|>Jz^Jn7X zFWQ?-B{B3jTI81+8jHCx-!fF0UIiCUd{}$`rNEe?<9*LskX}9n^)4dQpz_(=-CQIQ z5U$hGa=HjeGfeXY6VU2fk$XKNf^$4EF=B6WrEQ~VaGhL_Ae3YH;YBvAs55Y(T7CW1J20sfrSK&9 z6~(gQ&U5xHuyAWuTW`5nT6PTd@swr;I3z3M%s(jVgQv7#HgB()`IV)gv&0Rla`?6Z}Ga)X%A63VhHPtOtQg^UX`^;EpvE_;Nyb_z@X1n(;g zl;VG_x+OK=0Qi`B@pL(Hjp5F*fGKspUZsz0LB3!ze`5)L=TXF*(nEs%x4!581N`C) zh4u9t8%Q^kmb;dkYkx3H*lq-6Y?g&*#om#AMrls3{}ZM|DT4TbbUNhc!euee;ct<-89QrQ~>oY1Xc_lc(Wmasv^?Zsk9s4*4`E(OCgL*3 zn(VY0c(ZW1&dxq)OA0rNa80oJ zvBBVDgCw8{jn7oi5U5H4fPH@hI1h%_|qe^vJjyt3NcKxdy250;; zwblDKDyDQn(pFjifY|Ar;%M&w?pR$Q`%41R-sgfOI2zya*Z@%$MUA6p zJa-d|)4hBzot~9cvbOVt`wHS1^V7x^@1}O}C~v2EAvVHI7XhvQ)~&JhzQb6^wAWi^ z^{G;A9ZU)(xaf_zM$sr*zWsb!;sLP;O zg1DeaPHbP6uV1MF!@hY`C9WVDcIQjNhM4K6a!0GNSLbvjY~AY9rSioe7A~2Rd>pFM8eaa*s#Oe}0qBsDOno zQXlqFHsAe^e~PL+bT3H(zxrr#Gp6LbSbW9FKWiD8Q1`!)BxYGX(KH-*E*5-@Td;SZ z@J3S7I;v@)&_qC@Ru}|#DJ8l`cg}K1^*B)H@-rC$P#2)5gm37}fq!{o4tIzAX*eYE zEm-Mhor7;;qv3q0`!>K5x+f2=S5{|v>c#!lSWiA!VZ+Aayvl%7NiqV8X6p4O0(v{2 ztTPXt|9D36B&YN?KhFcbYG=(>Ls3Tf<~Y!FYcNxF0fAKqAvtxCC#o6Q7sr6z--uwj z07xl-kLw3OSr~j&>5eWJRbcXT5G3s5SfkZ!q2t@%>htCq6>_gA+G+`d321I~B(~Th z8GbmE>Wh91U8PVWT9M>v-qDhve~cTSj^bB;RQUhT z7CB46<^m&Whz*kh!U6RtGO8U3BB0A2ni@WVZGUf>^K(Tbf%M-w?D*Nj_@4=Pt2g#R z{lV`-`W@>(gELtbq{FN{3gR8s`xPwu85Bam14JMsxu zQt(n~@&E)o#>5Fm(hzr{qe$y4g*$=v&5YL0CFiDE8j;L)B!?T}^4!NeEimwY-_h=dx0c5dF7ecCRlHySUEg3; zkcybDr!m@sI*RmWVno3o>q(Y$k<5A#p9cw4z?Vyf1J2%)HddE7FD_?PbavJuHQ1*d z_840l@$(wU2P15WlcxF!{#E>f@Fe)F{=xQiem@)b z0h9C+q)Pd<;mGfpTe!JaAI?}uvZ(;@5I~qm1O%|xgT{>W;VQr7wzfKK@{Oa~_f?+7 zb4mdhACqgpLIRPiTmu-<^l4Gf^ysy|?y8_UxTF7|c&qe3P%4N{f(LlS@mzL&s}Mzf zda+{0yD8fU#%4`$yv*4iuz#U52g2!NTlq70uZkk!5k%3vM#`aWQesHGbnXe{CVCA~ zVdSR6Ippf|Oa~t?15t@zFnbe7tIz$gaMTu;GH3Aj-!^^Jtaap=awzE#tVLhCDcjo_ z2nPNRrN&_x#c;LLCoS_{eToZPh*Y|~o&SFBDXaE=Wgmn*P+p`d7oAv3;Xkm2KnYl zpl~Z@D${cWM!npOnh9@d?EFHCejsYv{RL+sZzaF(uJjESmW%dvHvVR@7Hs_{0YH|V zD0TZGvUZN|#M;{$HK(!mM=Ut$6JYuEnR`t3OKIkjrjSLA`7Z-y-9P;3e0+3dQHL1; zH`Wxg`PpPi7cw{fseep^{0%iEJcQ9(pBK}Yhji|72e)tj#*`bOr4EPFmznFLptrpB z)-MwKkF~2^E#MX*`~(t+7QaIz;Yekbudoxr4K%=KfFlwgw~g^h=QoQ0NK+ZYlfx}NG32B##t|>T7sfxHLb(YGoKHieAfSk8Pf}eU1NJFw zrh(yNI5%%4nn+E8i;a&C+sPTZhylFw|M3cE09O#UXwXZ=gQ9ZzGV{8>1V<9S^^_sZ zzS2a^a^)TxvQ|^S!heF##L(twzMp$Jfk#R}!d1mUz9VPg?F}=!(*?v_Y^O!4;b-`49^ zj@W{bx0ufPkee>}t;SGU*SF(qc+L}hkbro?muQX ztCO^JtE;`rV23+kHR}e@=AY~L2mK^XOcl&@chp*adSgelh8e&&XtmEHuEW!y`4;Xz zGn+Hn|6R>CS$cj?8RC1#Y#l1j*)FVWwpIg#Mz>y1y~YCnbqhp1$0 zg}?3VL6xkn;!@t;V#End6#O3PP4wg%#0rA>Mg63~$v-uMbqq}l8=3FMaBA%ws^$`rN4El7ZyS>HjfD-N|NMXeDR-__^0?`g^d z@he>S8sL<0U(;k(Ep@-a?z+GKBf;%03jlsTdZ4)%{mK?lLM6COxCM@GR`JBmWu;Ky zpokx+FL{c>OW^tqe7yCOtEXT!8ymuZ38KfhkW^6NIM&qHT75#RL<4JXf7>T}-F(3A zbhD`>f`0`T^4htOSMcK^^EExEj=Md;`3%zG(s#|zoNg3^2+jqF2se=FC#gRU;8@}N z#sf;~Q}+8n$ES1fzpO#@hkODKmSP1|Yc1O0J03GtcnTA`JBrYQXG#qnQ zZ07(ULPiBe($X3m<|e-Eq`0hew&e5BRfMNavu3kv7t>NlshnZdG?>kMc;r2qqVnMg8fSd_}iJ{OSkw& ze>L4RYi@#urR){*A%!vlSG<)`aVxivpG~qDuNlZ&q4*KWp>VdxZm%QTahBr9h^Zxt z^DzGv64pOPIc@(QIB`Q5NC0G^uE#^*e?FyJCbSVDtf6s=v=6Dn!#~3uu5{6c(?RY5 z29+WHqBE|Ok7I87l17a$m6`eGF;&GrXu4X>c-Wqn|K-5jb{12d%|HEXFUiM9NvcGs z7qy*eqB@Ocw7HI9puVl<`~L}*c^LMarMd2a+QjRbQ)HO~CdHQy0ks-`pti`5tX1}t zH&6zGMvyx{v_*jNpIiLvUM2<```K5e6Ttb^i)lcUv`~e{~IPqK+N;NHdJ8IQCfR zslB$qJRj%WU=!p&)hbc~b3VHUIQydesfJu8y_8!V&V5^?BjPUfcJjEip)tu@*woJ{ z2KT;e7H=K@$IxrE%gJMZ_Q!S}fICT}5_=ZH@Zovt`| zxurK~eQOf5naW^Jd7ui|PGpOXEh+^{OXfWahNzp_)VC~ef)eUk+uqP>s&j2t1a41A zp|p6XA3|U^vWM2b0doS!ye4hG@2w5+2N{+{0Nh{_|(O;c)amWp^;9KZol3LL|V)H7wqhreA<`u&XsI@S#S^Z5);v$#4>Fz2df1rSN z{L++SojC!j_C)*Vj$1ry-oLmK<5wIEG6+aCt_k4#ET-fLAJSGjoW0p?0U>&k`H zSaKJjuqOByh^1)kXMp@vFuz-ZcCUdbu)X`!+a2^<*Taou>dL#EwM(Z z^V|2~uu?nBz?CXI?CGzJ348APQWln>a=lx8Bq+9ZzMO$+ANf`G8?&R0mFw$#b*=Vx zyRifOXuV2Sk_1WDEsOw(!3%jccRQjwp>%2mlOCqqy`ruipMefuLOTUt#+$UCxRQy- zCFQ{Tp2NpvtggFZpE!E$m76LrG_$oU&U%9+_|EP`j-jb_S7xtU+efku9}cH<{N1SB zf{e-|ZaPd);P4>W@+?=Ekb;U`(ljHxk>Q(I9fs8%V z+aIXzwyrCeP7jJ&|H6hJnHur)2_(vBlq5w< zOy24BbyMU2<`x5N+OoGOH$P+IwX*b>GZ4F5`$|cVQAD+c=QyFLArr7Gmp6mS&Cgn4 z%^!5$*!T* zEVKOXCbKGtD0s3+mQw%aOIrLJ2pHnWWCNUw^Au zK=wx?W})E5d{hsf-flmYi2$x|tY^77h_UB5>*c!3T1tm1l z=)az{`{D^sROHi#QU~ zNr`=w$NyW%+8c1WoD+AS?`NVbl1z>^V>O^)Ba-<3Lcc0SxZX#`daH*6_$u=If9eJj z3z9wWY*r^+Kv0|ULs~o~gsITmr3?b#16q5>bA?ONF_ty{Nn6Xz+*e1YvSLvuDtU5f z$hpYzr!?R3WDI!&;`#gO_jcShg$Ft~+FB*3=&&2UcK*l*X__e6Kxfs=%tdGe>k_N3 zGcFF|Z0N@iDcG=&2vhS#o%GMY&3Atu(boKX;`3il(6a!Xcyo+srwciGY4;Cm5HnCT zqXozf?rK+iu~cdAz}hplk&S2_coYhRWyc=w4Oci-l3FUP6urghcN*ZFDH zjX#l*bOL0CaQ;X%0-T&cWZVbgd6zF!sclywofmzzCo7_|XDb8WZeN&no(Ixg@rds? zhn-mhO+^z@LXJnZUY1kA-ophi!e5SYdtVw1|B&+YEaG>4ViLvOVL(8D;gCTW`43@D z|2J=wB5=*veAQ*=jIiTt)J2h<{i@%sN-BYX#tt+sMXee=8;h&h`84pvJ8bn8tx3gw zxGeU|>dCDs6!#Gdn4{K!>eZ9cY7zz8;8!i%}iT7Y+sS1B! zth9ps7R>1j0V?QwimU3I?YV1sBwqLSdM^T2U5ozt|)=nng`z8LG@l(8Iw&R z4p;)>eo+}BDV4kBZnc3{>9n`DjDqHQY=P%QL-qVLuOE}=Rnqc8(Div(^!NXA0*WS&v4>>socj9t0Bk7ir1Gd=LiK31cLQjUL^ zRV>b^xNG^Q@#Aw9TC#0bvovw{M>KQlX%=Dm3XW)Kemhw(da{a(9QT4_T8DFyey;bJ zXlQ7!oMYgEESIc(Yc_98j_4Ln?naCQzeamQX-u#9E_#O3Qlc8BAr&LSAtQouegCuj z Date: Wed, 30 Jul 2025 15:59:35 -0400 Subject: [PATCH 0095/1717] refactor(services): update validation rules to be optional --- .../Controllers/Api/ServicesController.php | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 542be83de..f385b4d0e 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -7,6 +7,7 @@ use App\Actions\Service\StopService; use App\Http\Controllers\Controller; use App\Jobs\DeleteResourceJob; +use App\Models\Environment; use App\Models\EnvironmentVariable; use App\Models\Project; use App\Models\Server; @@ -550,7 +551,6 @@ public function delete_by_uuid(Request $request) mediaType: 'application/json', schema: new OA\Schema( type: 'object', - required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'], properties: [ 'name' => ['type' => 'string', 'description' => 'The service name.'], 'description' => ['type' => 'string', 'description' => 'The service description.'], @@ -627,16 +627,16 @@ private function upsert_service(Request $request, Service $service, string $team { $allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network']; $validator = customApiValidator($request->all(), [ - 'project_uuid' => 'string|required', + 'project_uuid' => 'string|nullable', 'environment_name' => 'string|nullable', 'environment_uuid' => 'string|nullable', - 'server_uuid' => 'string|required', + 'server_uuid' => 'string|nullable', 'destination_uuid' => 'string', 'name' => 'string|max:255', 'description' => 'string|nullable', 'instant_deploy' => 'boolean', 'connect_to_docker_network' => 'boolean', - 'docker_compose_raw' => 'string|required', + 'docker_compose_raw' => 'string|nullable', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); @@ -654,14 +654,19 @@ private function upsert_service(Request $request, Service $service, string $team ], 422); } - $environmentUuid = $request->environment_uuid; - $environmentName = $request->environment_name; - if (blank($environmentUuid) && blank($environmentName)) { - return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422); + $environmentUuid = null; + $environmentName = null; + + if ($request->environment_uuid) { + $environmentUuid = $request->environment_uuid; + } elseif ($request->environment_name) { + $environmentName = $request->environment_name; + } else { + $environmentUuid = $service->environment->uuid; } - $serverUuid = $request->server_uuid; - $instantDeploy = $request->instant_deploy ?? false; - $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + $serverUuid = $request->server_uuid ?? $service->server->uuid; + $projectUuid = $request->project_uuid ?? $service->environment->project->uuid; + $project = Project::whereTeamId($teamId)->whereUuid($projectUuid)->first(); if (! $project) { return response()->json(['message' => 'Project not found.'], 404); } @@ -684,39 +689,41 @@ private function upsert_service(Request $request, Service $service, string $team return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); } $destination = $destinations->first(); - if (! isBase64Encoded($request->docker_compose_raw)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', - ], - ], 422); + if ($request->has('docker_compose_raw')) { + if (! isBase64Encoded($request->docker_compose_raw)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', + ], + ], 422); + } + $dockerComposeRaw = base64_decode($request->docker_compose_raw); + if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose_raw); + $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + $service->docker_compose_raw = $dockerComposeRaw; } - $dockerComposeRaw = base64_decode($request->docker_compose_raw); - if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', - ], - ], 422); - } - $dockerCompose = base64_decode($request->docker_compose_raw); - $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $connectToDockerNetwork = $request->connect_to_docker_network ?? false; $service->name = $request->name ?? null; $service->description = $request->description ?? null; - $service->docker_compose_raw = $dockerComposeRaw; $service->environment_id = $environment->id; $service->server_id = $server->id; $service->destination_id = $destination->id; $service->destination_type = $destination->getMorphClass(); $service->connect_to_docker_network = $connectToDockerNetwork; $service->save(); - + $service->parse(); - if ($instantDeploy) { + if ($request->instant_deploy) { StartService::dispatch($service); } From 79fc9927d7e8af964467e2d1fa240d4bed1fe6c1 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:22:16 +0200 Subject: [PATCH 0096/1717] fix(git): tracking issue due to case sensitivity --- app/View/Components/services/advanced.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/View/Components/services/advanced.php b/app/View/Components/services/advanced.php index 8104eaad4..99729a262 100644 --- a/app/View/Components/services/advanced.php +++ b/app/View/Components/services/advanced.php @@ -1,13 +1,13 @@ Date: Sun, 3 Aug 2025 22:26:45 +0200 Subject: [PATCH 0097/1717] fix(git): tracking issue due to case sensitivity --- .../Components/{services/advanced.php => Services/Advanced.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/View/Components/{services/advanced.php => Services/Advanced.php} (100%) diff --git a/app/View/Components/services/advanced.php b/app/View/Components/Services/Advanced.php similarity index 100% rename from app/View/Components/services/advanced.php rename to app/View/Components/Services/Advanced.php From 7bb91faf920f3ce10524dbf022ec61a3f833859c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Sobczy=C5=84ski?= <32263891+h4570@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:11:39 +0200 Subject: [PATCH 0098/1717] fix(ui): Delete button width on small screens (#6308) --- .../livewire/project/shared/environment-variable/show.blade.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index 82ee789b6..7335124d1 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -88,6 +88,7 @@ @@ -101,6 +102,7 @@ From efdaf46d08ceb81ccc2658640d95973f90b70c21 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:27:31 +0200 Subject: [PATCH 0099/1717] fix(service): matrix entrypoint - make sure registration is enabled not just non empty - make sure RECAPTCHA_PUBLIC_KEY is set when adding the recaptcha block --- templates/compose/matrix.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/compose/matrix.yaml b/templates/compose/matrix.yaml index 99232967e..efaf505c2 100644 --- a/templates/compose/matrix.yaml +++ b/templates/compose/matrix.yaml @@ -89,14 +89,14 @@ services: # # ######################## - test -n "${ENABLE_REGISTRATION}" && ! grep "#registration" /data/homeserver.yaml &>/dev/null \ + [ "${ENABLE_REGISTRATION}" = "true" ] && ! grep "#registration" /data/homeserver.yaml &>/dev/null \ && echo >> /data/homeserver.yaml \ && cat <> /data/homeserver.yaml #registration enable_registration: true # Allows users to register on your server. EOF - ! grep ${RECAPTCHA_PUBLIC_KEY} /data/homeserver.yaml &>/dev/null \ + [ -n "${RECAPTCHA_PUBLIC_KEY}" ] && ! grep "${RECAPTCHA_PUBLIC_KEY}" /data/homeserver.yaml &>/dev/null \ && echo >> /data/homeserver.yaml \ && cat <> /data/homeserver.yaml # reCAPTCHA settings From 554b566735509fb2b834dd0dbad5987aad31123a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Sobczy=C5=84ski?= <32263891+h4570@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:09:26 +0200 Subject: [PATCH 0100/1717] fix(ui): add flex-wrap to prevent overflow on small screens (#6307) --- .../project/shared/environment-variable/show.blade.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index 7335124d1..5a197dc7a 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -21,7 +21,7 @@
@else @if ($isDisabled) -
+
@if ($is_shared) @@ -29,7 +29,7 @@ @endif
@else -
+
@if ($is_multiline) @@ -42,7 +42,7 @@ @endif
@endif -
+
@if (!$is_redis_credential) @if ($type === 'service') Date: Mon, 4 Aug 2025 14:13:33 +0200 Subject: [PATCH 0101/1717] feat(lang): add Polish language & improve forgot_password translation (#6306) --- lang/ar.json | 5 ++- lang/az.json | 5 ++- lang/cs.json | 5 ++- lang/de.json | 5 ++- lang/en.json | 5 ++- lang/es.json | 5 ++- lang/fa.json | 5 ++- lang/fr.json | 5 ++- lang/id.json | 5 ++- lang/it.json | 5 ++- lang/ja.json | 5 ++- lang/no.json | 5 ++- lang/pl.json | 44 +++++++++++++++++++ lang/pt-br.json | 5 ++- lang/pt.json | 5 ++- lang/ro.json | 5 ++- lang/tr.json | 5 ++- lang/vi.json | 5 ++- lang/zh-cn.json | 5 ++- lang/zh-tw.json | 5 ++- .../views/auth/forgot-password.blade.php | 2 +- resources/views/auth/login.blade.php | 4 +- 22 files changed, 104 insertions(+), 41 deletions(-) create mode 100644 lang/pl.json diff --git a/lang/ar.json b/lang/ar.json index 263924c24..c966cc686 100644 --- a/lang/ar.json +++ b/lang/ar.json @@ -11,7 +11,8 @@ "auth.login.infomaniak": "تسجيل الدخول باستخدام Infomaniak", "auth.already_registered": "هل سبق لك التسجيل؟", "auth.confirm_password": "تأكيد كلمة المرور", - "auth.forgot_password": "نسيت كلمة المرور", + "auth.forgot_password_link": "هل نسيت كلمة المرور؟", + "auth.forgot_password_heading": "استعادة كلمة المرور", "auth.forgot_password_send_email": "إرسال بريد إلكتروني لإعادة تعيين كلمة المرور", "auth.register_now": "تسجيل", "auth.logout": "تسجيل الخروج", @@ -39,4 +40,4 @@ "resource.delete_configurations": "حذف جميع ملفات التعريف من الخادم بشكل دائم.", "database.delete_backups_locally": "حذف كافة النسخ الاحتياطية نهائيًا من التخزين المحلي.", "warning.sslipdomain": "تم حفظ ملفات التعريف الخاصة بك، ولكن استخدام نطاق sslip مع https غير مستحسن، لأن خوادم Let's Encrypt مع هذا النطاق العام محدودة المعدل (ستفشل عملية التحقق من شهادة SSL).

استخدم نطاقك الخاص بدلاً من ذلك." -} +} \ No newline at end of file diff --git a/lang/az.json b/lang/az.json index 92f56ddbc..85cee7589 100644 --- a/lang/az.json +++ b/lang/az.json @@ -11,7 +11,8 @@ "auth.login.infomaniak": "Infomaniak ilə daxil ol", "auth.already_registered": "Qeytiyatınız var?", "auth.confirm_password": "Şifrəni təsdiqləyin", - "auth.forgot_password": "Şifrəmi unutdum", + "auth.forgot_password_link": "Şifrəmi unutdum?", + "auth.forgot_password_heading": "Şifrəni bərpa et", "auth.forgot_password_send_email": "Şifrəni sıfırlamaq üçün e-poçt göndər", "auth.register_now": "Qeydiyyat", "auth.logout": "Çıxış", @@ -39,4 +40,4 @@ "resource.delete_configurations": "Serverdən bütün konfiqurasiya faylları tamamilə silinəcək.", "database.delete_backups_locally": "Bütün ehtiyat nüsxələr lokal yaddaşdan tamamilə silinəcək.", "warning.sslipdomain": "Konfiqurasiya yadda saxlanıldı, lakin sslip domeni ilə https TÖVSİYƏ EDİLMİR, çünki Let's Encrypt serverləri bu ümumi domenlə məhdudlaşdırılır (SSL sertifikatının təsdiqlənməsi uğursuz olacaq).

Əvəzində öz domeninizdən istifadə edin." -} +} \ No newline at end of file diff --git a/lang/cs.json b/lang/cs.json index 00455aa81..9e5d2c44e 100644 --- a/lang/cs.json +++ b/lang/cs.json @@ -10,7 +10,8 @@ "auth.login.infomaniak": "Přihlásit se pomocí Infomaniak", "auth.already_registered": "Již jste registrováni?", "auth.confirm_password": "Potvrďte heslo", - "auth.forgot_password": "Zapomněli jste heslo", + "auth.forgot_password_link": "Zapomněli jste heslo?", + "auth.forgot_password_heading": "Obnovení hesla", "auth.forgot_password_send_email": "Poslat e-mail pro resetování hesla", "auth.register_now": "Registrovat se", "auth.logout": "Odhlásit se", @@ -30,4 +31,4 @@ "input.recovery_code": "Obnovovací kód", "button.save": "Uložit", "repository.url": "Příklady
Pro veřejné repozitáře, použijte https://....
Pro soukromé repozitáře, použijte git@....

https://github.com/coollabsio/coolify-examples main branch bude zvolena
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch bude vybrána.
https://gitea.com/sedlav/expressjs.git main branch vybrána.
https://gitlab.com/andrasbacsai/nodejs-example.git main branch bude vybrána." -} +} \ No newline at end of file diff --git a/lang/de.json b/lang/de.json index f56b21710..fd587de22 100644 --- a/lang/de.json +++ b/lang/de.json @@ -11,7 +11,8 @@ "auth.login.zitadel": "Mit Zitadel anmelden", "auth.already_registered": "Bereits registriert?", "auth.confirm_password": "Passwort bestätigen", - "auth.forgot_password": "Passwort vergessen", + "auth.forgot_password_link": "Passwort vergessen?", + "auth.forgot_password_heading": "Passwort-Wiederherstellung", "auth.forgot_password_send_email": "Passwort zurücksetzen E-Mail senden", "auth.register_now": "Registrieren", "auth.logout": "Abmelden", @@ -31,4 +32,4 @@ "input.recovery_code": "Wiederherstellungscode", "button.save": "Speichern", "repository.url": "Beispiele
Für öffentliche Repositories benutze https://....
Für private Repositories benutze git@....

https://github.com/coollabsio/coolify-examples main Branch wird ausgewählt
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify Branch wird ausgewählt.
https://gitea.com/sedlav/expressjs.git main Branch wird ausgewählt.
https://gitlab.com/andrasbacsai/nodejs-example.git main Branch wird ausgewählt." -} +} \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index 4a398a9f9..af7f2145d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -12,7 +12,8 @@ "auth.login.zitadel": "Login with Zitadel", "auth.already_registered": "Already registered?", "auth.confirm_password": "Confirm password", - "auth.forgot_password": "Forgot password", + "auth.forgot_password_link": "Forgot password?", + "auth.forgot_password_heading": "Password recovery", "auth.forgot_password_send_email": "Send password reset email", "auth.register_now": "Register", "auth.logout": "Logout", @@ -40,4 +41,4 @@ "resource.delete_configurations": "Permanently delete all configuration files from the server.", "database.delete_backups_locally": "All backups will be permanently deleted from local storage.", "warning.sslipdomain": "Your configuration is saved, but sslip domain with https is NOT recommended, because Let's Encrypt servers with this public domain are rate limited (SSL certificate validation will fail).

Use your own domain instead." -} +} \ No newline at end of file diff --git a/lang/es.json b/lang/es.json index 73363a9bf..f56387f05 100644 --- a/lang/es.json +++ b/lang/es.json @@ -10,7 +10,8 @@ "auth.login.infomaniak": "Acceder con Infomaniak", "auth.already_registered": "¿Ya estás registrado?", "auth.confirm_password": "Confirmar contraseña", - "auth.forgot_password": "¿Olvidaste tu contraseña?", + "auth.forgot_password_link": "¿Olvidaste tu contraseña?", + "auth.forgot_password_heading": "Recuperación de contraseña", "auth.forgot_password_send_email": "Enviar correo de recuperación de contraseña", "auth.register_now": "Registrar", "auth.logout": "Cerrar sesión", @@ -30,4 +31,4 @@ "input.recovery_code": "Código de recuperación", "button.save": "Guardar", "repository.url": "Examples
Para repositorios públicos, usar https://....
Para repositorios privados, usar git@....

https://github.com/coollabsio/coolify-examples main la rama 'main' será seleccionada.
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify la rama 'nodejs-fastify' será seleccionada.
https://gitea.com/sedlav/expressjs.git main la rama 'main' será seleccionada.
https://gitlab.com/andrasbacsai/nodejs-example.git main la rama 'main' será seleccionada." -} +} \ No newline at end of file diff --git a/lang/fa.json b/lang/fa.json index d68049e77..ae22ee946 100644 --- a/lang/fa.json +++ b/lang/fa.json @@ -10,7 +10,8 @@ "auth.login.infomaniak": "ورود با Infomaniak", "auth.already_registered": "قبلاً ثبت نام کرده‌اید؟", "auth.confirm_password": "تایید رمز عبور", - "auth.forgot_password": "فراموشی رمز عبور", + "auth.forgot_password_link": "رمز عبور را فراموش کرده‌اید؟", + "auth.forgot_password_heading": "بازیابی رمز عبور", "auth.forgot_password_send_email": "ارسال ایمیل بازیابی رمز عبور", "auth.register_now": "ثبت نام", "auth.logout": "خروج", @@ -30,4 +31,4 @@ "input.recovery_code": "کد بازیابی", "button.save": "ذخیره", "repository.url": "مثال‌ها
برای مخازن عمومی، از https://... استفاده کنید.
برای مخازن خصوصی، از git@... استفاده کنید.

شاخه main انتخاب خواهد شد.
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify شاخه nodejs-fastify انتخاب خواهد شد.
https://gitea.com/sedlav/expressjs.git شاخه main انتخاب خواهد شد.
https://gitlab.com/andrasbacsai/nodejs-example.git شاخه main انتخاب خواهد شد." -} +} \ No newline at end of file diff --git a/lang/fr.json b/lang/fr.json index 2516d0f69..d98a1ebc8 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -11,7 +11,8 @@ "auth.login.infomaniak": "Connexion avec Infomaniak", "auth.already_registered": "Déjà enregistré ?", "auth.confirm_password": "Confirmer le mot de passe", - "auth.forgot_password": "Mot de passe oublié", + "auth.forgot_password_link": "Mot de passe oublié ?", + "auth.forgot_password_heading": "Récupération du mot de passe", "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe", "auth.register_now": "S'enregistrer", "auth.logout": "Déconnexion", @@ -39,4 +40,4 @@ "resource.delete_configurations": "Supprimer définitivement tous les fichiers de configuration du serveur.", "database.delete_backups_locally": "Toutes les sauvegardes seront définitivement supprimées du stockage local.", "warning.sslipdomain": "Votre configuration est enregistrée, mais l'utilisation du domaine sslip avec https N'EST PAS recommandée, car les serveurs Let's Encrypt avec ce domaine public sont limités en taux (la validation du certificat SSL échouera).

Utilisez plutôt votre propre domaine." -} +} \ No newline at end of file diff --git a/lang/id.json b/lang/id.json index b0e38197a..d85176cda 100644 --- a/lang/id.json +++ b/lang/id.json @@ -11,7 +11,8 @@ "auth.login.infomaniak": "Masuk dengan Infomaniak", "auth.already_registered": "Sudah terdaftar?", "auth.confirm_password": "Konfirmasi kata sandi", - "auth.forgot_password": "Lupa kata sandi", + "auth.forgot_password_link": "Lupa kata sandi?", + "auth.forgot_password_heading": "Pemulihan Kata Sandi", "auth.forgot_password_send_email": "Kirim email reset kata sandi", "auth.register_now": "Daftar", "auth.logout": "Keluar", @@ -39,4 +40,4 @@ "resource.delete_configurations": "Hapus permanen semua file konfigurasi dari server.", "database.delete_backups_locally": "Semua backup akan dihapus permanen dari penyimpanan lokal.", "warning.sslipdomain": "Konfigurasi Anda disimpan, tetapi domain sslip dengan https TIDAK direkomendasikan, karena server Let's Encrypt dengan domain publik ini dibatasi (validasi sertifikat SSL akan gagal).

Gunakan domain Anda sendiri sebagai gantinya." -} +} \ No newline at end of file diff --git a/lang/it.json b/lang/it.json index c0edc314b..e4c1a9c05 100644 --- a/lang/it.json +++ b/lang/it.json @@ -11,7 +11,8 @@ "auth.login.infomaniak": "Accedi con Infomaniak", "auth.already_registered": "Già registrato?", "auth.confirm_password": "Conferma password", - "auth.forgot_password": "Password dimenticata", + "auth.forgot_password_link": "Hai dimenticato la password?", + "auth.forgot_password_heading": "Recupero password", "auth.forgot_password_send_email": "Invia email per reimpostare la password", "auth.register_now": "Registrati", "auth.logout": "Esci", @@ -39,4 +40,4 @@ "resource.delete_configurations": "Elimina definitivamente tutti i file di configurazione dal server.", "database.delete_backups_locally": "Tutti i backup verranno eliminati definitivamente dall'archiviazione locale.", "warning.sslipdomain": "La tua configurazione è stata salvata, ma il dominio sslip con https NON è raccomandato, poiché i server di Let's Encrypt con questo dominio pubblico hanno limitazioni di frequenza (la convalida del certificato SSL fallirà).

Utilizza invece il tuo dominio personale." -} +} \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index 87d87d99b..05987e7ce 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -10,7 +10,8 @@ "auth.login.infomaniak": "Infomaniakでログイン", "auth.already_registered": "すでに登録済みですか?", "auth.confirm_password": "パスワードを確認", - "auth.forgot_password": "パスワードを忘れた", + "auth.forgot_password_link": "パスワードをお忘れですか?", + "auth.forgot_password_heading": "パスワードの再設定", "auth.forgot_password_send_email": "パスワードリセットメールを送信", "auth.register_now": "今すぐ登録", "auth.logout": "ログアウト", @@ -30,4 +31,4 @@ "input.recovery_code": "リカバリーコード", "button.save": "保存", "repository.url": "
公開リポジトリの場合はhttps://...を使用してください。
プライベートリポジトリの場合はgit@...を使用してください。

https://github.com/coollabsio/coolify-examples mainブランチが選択されます
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastifyブランチが選択されます。
https://gitea.com/sedlav/expressjs.git mainブランチが選択されます。
https://gitlab.com/andrasbacsai/nodejs-example.git mainブランチが選択されます。" -} +} \ No newline at end of file diff --git a/lang/no.json b/lang/no.json index a84f6aa6c..967bdf606 100644 --- a/lang/no.json +++ b/lang/no.json @@ -11,7 +11,8 @@ "auth.login.infomaniak": "Logg inn med Infomaniak", "auth.already_registered": "Allerede registrert?", "auth.confirm_password": "Bekreft passord", - "auth.forgot_password": "Glemt passord", + "auth.forgot_password_link": "Glemt passord?", + "auth.forgot_password_heading": "Gjenoppretting av passord", "auth.forgot_password_send_email": "Send e-post for tilbakestilling av passord", "auth.register_now": "Registrer deg", "auth.logout": "Logg ut", @@ -39,4 +40,4 @@ "resource.delete_configurations": "Slett alle konfigurasjonsfiler fra serveren permanent.", "database.delete_backups_locally": "Alle sikkerhetskopier vil bli slettet permanent fra lokal lagring.", "warning.sslipdomain": "Konfigurasjonen din er lagret, men sslip-domene med https er IKKE anbefalt, fordi Let's Encrypt-servere med dette offentlige domenet er hastighetsbegrenset (SSL-sertifikatvalidering vil mislykkes).

Bruk ditt eget domene i stedet." -} +} \ No newline at end of file diff --git a/lang/pl.json b/lang/pl.json new file mode 100644 index 000000000..bcd8e2393 --- /dev/null +++ b/lang/pl.json @@ -0,0 +1,44 @@ +{ + "auth.login": "Zaloguj", + "auth.login.authentik": "Zaloguj się przez Authentik", + "auth.login.azure": "Zaloguj się przez Microsoft", + "auth.login.bitbucket": "Zaloguj się przez Bitbucket", + "auth.login.clerk": "Zaloguj się przez Clerk", + "auth.login.discord": "Zaloguj się przez Discord", + "auth.login.github": "Zaloguj się przez GitHub", + "auth.login.gitlab": "Zaloguj się przez Gitlab", + "auth.login.google": "Zaloguj się przez Google", + "auth.login.infomaniak": "Zaloguj się przez Infomaniak", + "auth.login.zitadel": "Zaloguj się przez Zitadel", + "auth.already_registered": "Już zarejestrowany?", + "auth.confirm_password": "Potwierdź hasło", + "auth.forgot_password_link": "Zapomniałeś hasło?", + "auth.forgot_password_heading": "Odzyskiwanie hasła", + "auth.forgot_password_send_email": "Wyślij email resetujący hasło", + "auth.register_now": "Zarejestruj", + "auth.logout": "Wyloguj", + "auth.register": "Zarejestruj", + "auth.registration_disabled": "Rejestracja jest wyłączona. Skontaktuj się z administratorem.", + "auth.reset_password": "Zresetuj hasło", + "auth.failed": "Podane dane nie zgadzają się z naszymi rekordami.", + "auth.failed.callback": "Nie udało się przeprocesować callbacku od dostawcy logowania.", + "auth.failed.password": "Podane hasło jest nieprawidłowe.", + "auth.failed.email": "Nie znaleziono użytkownika z takim adresem email.", + "auth.throttle": "Zbyt wiele prób logowania. Spróbuj ponownie za :seconds sekund.", + "input.name": "Nazwa", + "input.email": "Email", + "input.password": "Hasło", + "input.password.again": "Hasło ponownie", + "input.code": "Jednorazowy kod", + "input.recovery_code": "Kod odzyskiwania", + "button.save": "Zapisz", + "repository.url": "Przykłady
Dla publicznych repozytoriów użyj https://....
Dla prywatnych repozytoriów, użyj git@....

https://github.com/coollabsio/coolify-examples - zostanie wybrany branch main
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify - zostanie wybrany branch nodejs-fastify
https://gitea.com/sedlav/expressjs.git - zostanie wybrany branch main
https://gitlab.com/andrasbacsai/nodejs-example.git - zostanie wybrany branch main", + "service.stop": "Ten serwis zostanie zatrzymany.", + "resource.docker_cleanup": "Uruchom Docker Cleanup (usunie nieużywane obrazy i cache buildera).", + "resource.non_persistent": "Wszystkie nietrwałe dane zostaną usunięte.", + "resource.delete_volumes": "Trwale usuń wszystkie wolumeny powiązane z tym zasobem.", + "resource.delete_connected_networks": "Trwale usuń wszystkie niepredefiniowane sieci powiązane z tym zasobem.", + "resource.delete_configurations": "Trwale usuń wszystkie pliki konfiguracyjne z serwera.", + "database.delete_backups_locally": "Wszystkie backupy zostaną trwale usunięte z lokalnej pamięci.", + "warning.sslipdomain": "Twoja konfiguracja została zapisana, lecz domena sslip z https jest NIEZALECANA, ponieważ serwery Let's Encrypt z tą publiczną domeną są pod rate limitem (walidacja certyfikatu SSL certificate się nie powiedzie).

Lepiej użyj własnej domeny." +} \ No newline at end of file diff --git a/lang/pt-br.json b/lang/pt-br.json index c3a102995..f3ebb6c69 100644 --- a/lang/pt-br.json +++ b/lang/pt-br.json @@ -11,7 +11,8 @@ "auth.login.infomaniak": "Entrar com Infomaniak", "auth.already_registered": "Já tem uma conta?", "auth.confirm_password": "Confirmar senha", - "auth.forgot_password": "Esqueceu a senha", + "auth.forgot_password_link": "Esqueceu a senha?", + "auth.forgot_password_heading": "Recuperação de senha", "auth.forgot_password_send_email": "Enviar e-mail para redefinir senha", "auth.register_now": "Cadastre-se", "auth.logout": "Sair", @@ -39,4 +40,4 @@ "resource.delete_configurations": "Excluir permanentemente todos os arquivos de configuração do servidor.", "database.delete_backups_locally": "Todos os backups serão excluídos permanentemente do armazenamento local.", "warning.sslipdomain": "Sua configuração foi salva, mas o domínio sslip com https NÃO é recomendado, porque os servidores do Let's Encrypt com este domínio público têm limitação de taxa (a validação do certificado SSL falhará).

Use seu próprio domínio em vez disso." -} +} \ No newline at end of file diff --git a/lang/pt.json b/lang/pt.json index 80ff8c146..08ad19df3 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -10,7 +10,8 @@ "auth.login.infomaniak": "Entrar com Infomaniak", "auth.already_registered": "Já tem uma conta?", "auth.confirm_password": "Confirmar senha", - "auth.forgot_password": "Esqueceu a senha?", + "auth.forgot_password_link": "Esqueceu a senha?", + "auth.forgot_password_heading": "Recuperação de senha", "auth.forgot_password_send_email": "Enviar e-mail de redefinição de senha", "auth.register_now": "Cadastrar-se", "auth.logout": "Sair", @@ -30,4 +31,4 @@ "input.recovery_code": "Código de recuperação", "button.save": "Salvar", "repository.url": "Exemplos
Para repositórios públicos, use https://....
Para repositórios privados, use git@....

https://github.com/coollabsio/coolify-examples a branch main será selecionada
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify a branch nodejs-fastify será selecionada.
https://gitea.com/sedlav/expressjs.git a branch main será selecionada.
https://gitlab.com/andrasbacsai/nodejs-example.git a branch main será selecionada." -} +} \ No newline at end of file diff --git a/lang/ro.json b/lang/ro.json index 5588ea6f4..18028d087 100644 --- a/lang/ro.json +++ b/lang/ro.json @@ -10,7 +10,8 @@ "auth.login.infomaniak": "Autentificare prin Infomaniak", "auth.already_registered": "Sunteți deja înregistrat?", "auth.confirm_password": "Confirmați parola", - "auth.forgot_password": "Ați uitat parola", + "auth.forgot_password_link": "Ați uitat parola?", + "auth.forgot_password_heading": "Recuperare parolă", "auth.forgot_password_send_email": "Trimiteți e-mail-ul pentru resetarea parolei", "auth.register_now": "Înregistrare", "auth.logout": "Deconectare", @@ -37,4 +38,4 @@ "resource.delete_connected_networks": "Ștergeți definitiv toate rețelele non-predefinite asociate cu această resursă.", "resource.delete_configurations": "Ștergeți definitiv toate fișierele de configurare de pe server.", "database.delete_backups_locally": "Toate copiile de rezervă vor fi șterse definitiv din stocarea locală." -} +} \ No newline at end of file diff --git a/lang/tr.json b/lang/tr.json index 74f693dc9..e3f34aa14 100644 --- a/lang/tr.json +++ b/lang/tr.json @@ -10,7 +10,8 @@ "auth.login.infomaniak": "Infomaniak ile Giriş Yap", "auth.already_registered": "Zaten kayıtlı mısınız?", "auth.confirm_password": "Şifreyi Onayla", - "auth.forgot_password": "Şifremi Unuttum", + "auth.forgot_password_link": "Şifrenizi mi unuttunuz?", + "auth.forgot_password_heading": "Şifre Kurtarma", "auth.forgot_password_send_email": "Şifre sıfırlama e-postası gönder", "auth.register_now": "Kayıt Ol", "auth.logout": "Çıkış Yap", @@ -38,4 +39,4 @@ "resource.delete_configurations": "Sunucudaki tüm yapılandırma dosyaları kalıcı olarak silinecek.", "database.delete_backups_locally": "Tüm yedekler yerel depolamadan kalıcı olarak silinecek.", "warning.sslipdomain": "Yapılandırmanız kaydedildi, ancak sslip domain ile https ÖNERİLMEZ, çünkü Let's Encrypt sunucuları bu genel domain ile sınırlandırılmıştır (SSL sertifikası doğrulaması başarısız olur).

Bunun yerine kendi domaininizi kullanın." -} +} \ No newline at end of file diff --git a/lang/vi.json b/lang/vi.json index 46edac599..76e380477 100644 --- a/lang/vi.json +++ b/lang/vi.json @@ -10,7 +10,8 @@ "auth.login.infomaniak": "Đăng Nhập Bằng Infomaniak", "auth.already_registered": "Đã đăng ký?", "auth.confirm_password": "Nhập lại mật khẩu", - "auth.forgot_password": "Quên mật khẩu", + "auth.forgot_password_link": "Quên mật khẩu?", + "auth.forgot_password_heading": "Khôi phục mật khẩu", "auth.forgot_password_send_email": "Gửi email đặt lại mật khẩu", "auth.register_now": "Đăng ký ngay", "auth.logout": "Đăng xuất", @@ -30,4 +31,4 @@ "input.recovery_code": "Mã khôi phục", "button.save": "Lưu", "repository.url": "Ví dụ
Với repo công khai, sử dụng https://....
Với repo riêng tư, sử dụng git@....

https://github.com/coollabsio/coolify-examples nhánh chính sẽ được chọn
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nhánh nodejs-fastify sẽ được chọn.
https://gitea.com/sedlav/expressjs.git nhánh chính sẽ được chọn.
https://gitlab.com/andrasbacsai/nodejs-example.git nhánh chính sẽ được chọn." -} +} \ No newline at end of file diff --git a/lang/zh-cn.json b/lang/zh-cn.json index d46c71e07..530621ee1 100644 --- a/lang/zh-cn.json +++ b/lang/zh-cn.json @@ -10,7 +10,8 @@ "auth.login.infomaniak": "使用 Infomaniak 登录", "auth.already_registered": "已经注册?", "auth.confirm_password": "确认密码", - "auth.forgot_password": "忘记密码", + "auth.forgot_password_link": "忘记密码?", + "auth.forgot_password_heading": "密码找回", "auth.forgot_password_send_email": "发送密码重置邮件", "auth.register_now": "注册", "auth.logout": "退出登录", @@ -30,4 +31,4 @@ "input.recovery_code": "恢复码", "button.save": "保存", "repository.url": "示例
对于公共代码仓库,请使用 https://...
对于私有代码仓库,请使用 git@...

https://github.com/coollabsio/coolify-examples main 分支将被选择
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify 分支将被选择。
https://gitea.com/sedlav/expressjs.git main 分支将被选择。
https://gitlab.com/andrasbacsai/nodejs-example.git main 分支将被选择" -} +} \ No newline at end of file diff --git a/lang/zh-tw.json b/lang/zh-tw.json index c0784c7b7..aa078104b 100644 --- a/lang/zh-tw.json +++ b/lang/zh-tw.json @@ -10,7 +10,8 @@ "auth.login.infomaniak": "使用 Infomaniak 登入", "auth.already_registered": "已經註冊?", "auth.confirm_password": "確認密碼", - "auth.forgot_password": "忘記密碼", + "auth.forgot_password_link": "忘記密碼?", + "auth.forgot_password_heading": "密碼找回", "auth.forgot_password_send_email": "發送重設密碼電郵", "auth.register_now": "註冊", "auth.logout": "登出", @@ -30,4 +31,4 @@ "input.recovery_code": "恢復碼", "button.save": "儲存", "repository.url": "例子
對於公共代碼倉庫,請使用 https://...
對於私有代碼倉庫,請使用 git@...

https://github.com/coollabsio/coolify-examples main 分支將被選擇
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify 分支將被選擇。
https://gitea.com/sedlav/expressjs.git main 分支將被選擇。
https://gitlab.com/andrasbacsai/nodejs-example.git main 分支將被選擇。" -} +} \ No newline at end of file diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php index 249aa18f9..66a924fb8 100644 --- a/resources/views/auth/forgot-password.blade.php +++ b/resources/views/auth/forgot-password.blade.php @@ -4,7 +4,7 @@ Coolify
- {{ __('auth.forgot_password') }} + {{ __('auth.forgot_password_heading') }}
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 42faf517f..8bd8e81fc 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -23,7 +23,7 @@ required label="{{ __('input.password') }}" /> - {{ __('auth.forgot_password') }}? + {{ __('auth.forgot_password_link') }} @else - {{ __('auth.forgot_password') }}? + {{ __('auth.forgot_password_link') }} @endenv From 84a2073f7ff9f81d68efc4dee703d2ae1e83d006 Mon Sep 17 00:00:00 2001 From: Datenschmutz <63157166+Datenschmutz@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:15:37 +0200 Subject: [PATCH 0102/1717] feat(service): update Authentik template (#6264) --- templates/compose/authentik.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/compose/authentik.yaml b/templates/compose/authentik.yaml index d50110a44..06637d88b 100644 --- a/templates/compose/authentik.yaml +++ b/templates/compose/authentik.yaml @@ -6,7 +6,7 @@ services: authentik-server: - image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.6.3} + image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.6.4} restart: unless-stopped command: server environment: @@ -35,7 +35,7 @@ services: redis: condition: service_healthy authentik-worker: - image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.6.3} + image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.6.4} restart: unless-stopped command: worker environment: From 8332311999def671e6bfe0505acd82890768c2f0 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:53:18 +0200 Subject: [PATCH 0103/1717] chore(deps): update node and php deps --- composer.lock | 409 +++++++++++++++++++++++++-------------------- package-lock.json | 412 +++++++++++++++++++++++----------------------- 2 files changed, 439 insertions(+), 382 deletions(-) diff --git a/composer.lock b/composer.lock index 8d170cdc1..acf153038 100644 --- a/composer.lock +++ b/composer.lock @@ -870,16 +870,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.351.1", + "version": "3.352.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "f3e20c8cdd2cc5827d77a0b3c0872fab89cdf805" + "reference": "7f3ad0da2545b24259273ea7ab892188bae7d91b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f3e20c8cdd2cc5827d77a0b3c0872fab89cdf805", - "reference": "f3e20c8cdd2cc5827d77a0b3c0872fab89cdf805", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7f3ad0da2545b24259273ea7ab892188bae7d91b", + "reference": "7f3ad0da2545b24259273ea7ab892188bae7d91b", "shasum": "" }, "require": { @@ -961,9 +961,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.351.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.352.0" }, - "time": "2025-07-17T18:07:08+00:00" + "time": "2025-08-01T18:04:23+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1373,16 +1373,16 @@ }, { "name": "doctrine/dbal", - "version": "4.3.0", + "version": "4.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "5fe09532be619202d59c70956c6fb20e97933ee3" + "reference": "ac336c95ea9e13433d56ca81c308b39db0e1a2a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/5fe09532be619202d59c70956c6fb20e97933ee3", - "reference": "5fe09532be619202d59c70956c6fb20e97933ee3", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/ac336c95ea9e13433d56ca81c308b39db0e1a2a7", + "reference": "ac336c95ea9e13433d56ca81c308b39db0e1a2a7", "shasum": "" }, "require": { @@ -1459,7 +1459,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.3.0" + "source": "https://github.com/doctrine/dbal/tree/4.3.1" }, "funding": [ { @@ -1475,7 +1475,7 @@ "type": "tidelift" } ], - "time": "2025-06-16T19:31:04+00:00" + "time": "2025-07-22T10:09:51+00:00" }, { "name": "doctrine/deprecations", @@ -2678,16 +2678,16 @@ }, { "name": "laravel/framework", - "version": "v12.20.0", + "version": "v12.21.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff" + "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/1b9a00f8caf5503c92aa436279172beae1a484ff", - "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff", + "url": "https://api.github.com/repos/laravel/framework/zipball/ac8c4e73bf1b5387b709f7736d41427e6af1c93b", + "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b", "shasum": "" }, "require": { @@ -2889,7 +2889,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-08T15:02:21+00:00" + "time": "2025-07-22T15:41:55+00:00" }, { "name": "laravel/horizon", @@ -3111,16 +3111,16 @@ }, { "name": "laravel/sanctum", - "version": "v4.1.2", + "version": "v4.2.0", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "e4c09e69aecd5a383e0c1b85a6bb501c997d7491" + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/e4c09e69aecd5a383e0c1b85a6bb501c997d7491", - "reference": "e4c09e69aecd5a383e0c1b85a6bb501c997d7491", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", "shasum": "" }, "require": { @@ -3171,7 +3171,7 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-07-01T15:49:32+00:00" + "time": "2025-07-09T19:45:24+00:00" }, { "name": "laravel/serializable-closure", @@ -3236,16 +3236,16 @@ }, { "name": "laravel/socialite", - "version": "v5.21.0", + "version": "v5.23.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "d83639499ad14985c9a6a9713b70073300ce998d" + "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/d83639499ad14985c9a6a9713b70073300ce998d", - "reference": "d83639499ad14985c9a6a9713b70073300ce998d", + "url": "https://api.github.com/repos/laravel/socialite/zipball/e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", + "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", "shasum": "" }, "require": { @@ -3304,7 +3304,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-05-19T12:56:37+00:00" + "time": "2025-07-23T14:16:08+00:00" }, { "name": "laravel/tinker", @@ -3510,16 +3510,16 @@ }, { "name": "league/commonmark", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", - "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", "shasum": "" }, "require": { @@ -3548,7 +3548,7 @@ "symfony/process": "^5.4 | ^6.0 | ^7.0", "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", - "vimeo/psalm": "^4.24.0 || ^5.0.0" + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" }, "suggest": { "symfony/yaml": "v2.3+ required if using the Front Matter extension" @@ -3613,7 +3613,7 @@ "type": "tidelift" } ], - "time": "2025-05-05T12:20:28+00:00" + "time": "2025-07-20T12:47:49+00:00" }, { "name": "league/config", @@ -4696,16 +4696,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.1", + "version": "3.10.2", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00" + "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1fd1935b2d90aef2f093c5e35f7ae1257c448d00", - "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", + "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", "shasum": "" }, "require": { @@ -4797,7 +4797,7 @@ "type": "tidelift" } ], - "time": "2025-06-21T15:19:35+00:00" + "time": "2025-08-02T09:36:06+00:00" }, { "name": "nette/schema", @@ -4949,16 +4949,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.5.0", + "version": "v5.6.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", "shasum": "" }, "require": { @@ -5001,9 +5001,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" }, - "time": "2025-05-31T08:24:38+00:00" + "time": "2025-07-27T20:03:57+00:00" }, { "name": "nubs/random-name-generator", @@ -6663,16 +6663,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.9", + "version": "v0.12.10", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "1b801844becfe648985372cb4b12ad6840245ace" + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1b801844becfe648985372cb4b12ad6840245ace", - "reference": "1b801844becfe648985372cb4b12ad6840245ace", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", "shasum": "" }, "require": { @@ -6722,12 +6722,11 @@ "authors": [ { "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" + "email": "justin@justinhileman.info" } ], "description": "An interactive shell for modern PHP.", - "homepage": "http://psysh.org", + "homepage": "https://psysh.org", "keywords": [ "REPL", "console", @@ -6736,9 +6735,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.9" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" }, - "time": "2025-06-23T02:35:06+00:00" + "time": "2025-08-04T12:39:37+00:00" }, { "name": "purplepixie/phpdns", @@ -7247,16 +7246,16 @@ }, { "name": "sentry/sentry", - "version": "4.14.1", + "version": "4.14.2", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php.git", - "reference": "a28c4a6f5fda2bf730789a638501d7a737a64eda" + "reference": "bfeec74303d60d3f8bc33701ab3e86f8a8729f17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/a28c4a6f5fda2bf730789a638501d7a737a64eda", - "reference": "a28c4a6f5fda2bf730789a638501d7a737a64eda", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/bfeec74303d60d3f8bc33701ab3e86f8a8729f17", + "reference": "bfeec74303d60d3f8bc33701ab3e86f8a8729f17", "shasum": "" }, "require": { @@ -7320,7 +7319,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/4.14.1" + "source": "https://github.com/getsentry/sentry-php/tree/4.14.2" }, "funding": [ { @@ -7332,7 +7331,7 @@ "type": "custom" } ], - "time": "2025-06-23T15:25:52+00:00" + "time": "2025-07-21T08:28:29+00:00" }, { "name": "sentry/sentry-laravel", @@ -8779,16 +8778,16 @@ }, { "name": "symfony/console", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" + "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", + "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", + "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", "shasum": "" }, "require": { @@ -8853,7 +8852,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.1" + "source": "https://github.com/symfony/console/tree/v7.3.2" }, "funding": [ { @@ -8864,12 +8863,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-07-30T17:13:41+00:00" }, { "name": "symfony/css-selector", @@ -9005,16 +9008,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235" + "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/35b55b166f6752d6aaf21aa042fc5ed280fce235", - "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/0b31a944fcd8759ae294da4d2808cbc53aebd0c3", + "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3", "shasum": "" }, "require": { @@ -9062,7 +9065,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.1" + "source": "https://github.com/symfony/error-handler/tree/v7.3.2" }, "funding": [ { @@ -9073,12 +9076,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-13T07:48:40+00:00" + "time": "2025-07-07T08:17:57+00:00" }, { "name": "symfony/event-dispatcher", @@ -9238,16 +9245,16 @@ }, { "name": "symfony/finder", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", "shasum": "" }, "require": { @@ -9282,7 +9289,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.0" + "source": "https://github.com/symfony/finder/tree/v7.3.2" }, "funding": [ { @@ -9293,25 +9300,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-30T19:00:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "23dd60256610c86a3414575b70c596e5deff6ed9" + "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/23dd60256610c86a3414575b70c596e5deff6ed9", - "reference": "23dd60256610c86a3414575b70c596e5deff6ed9", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6", + "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6", "shasum": "" }, "require": { @@ -9361,7 +9372,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.1" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.2" }, "funding": [ { @@ -9372,25 +9383,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-23T15:07:14+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831" + "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1644879a66e4aa29c36fe33dfa6c54b450ce1831", - "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6ecc895559ec0097e221ed2fd5eb44d5fede083c", + "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c", "shasum": "" }, "require": { @@ -9475,7 +9490,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.1" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.2" }, "funding": [ { @@ -9486,25 +9501,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-28T08:24:55+00:00" + "time": "2025-07-31T10:45:04+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368" + "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/b5db5105b290bdbea5ab27b89c69effcf1cb3368", - "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "url": "https://api.github.com/repos/symfony/mailer/zipball/d43e84d9522345f96ad6283d5dfccc8c1cfc299b", + "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b", "shasum": "" }, "require": { @@ -9555,7 +9574,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.1" + "source": "https://github.com/symfony/mailer/tree/v7.3.2" }, "funding": [ { @@ -9566,25 +9585,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/mime", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9" + "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", + "url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1", + "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1", "shasum": "" }, "require": { @@ -9639,7 +9662,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.0" + "source": "https://github.com/symfony/mime/tree/v7.3.2" }, "funding": [ { @@ -9650,25 +9673,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-19T08:51:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca" + "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/afb9a8038025e5dbc657378bfab9198d75f10fca", - "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37", + "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37", "shasum": "" }, "require": { @@ -9706,7 +9733,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.3.2" }, "funding": [ { @@ -9717,12 +9744,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-04T13:12:05+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/polyfill-ctype", @@ -10587,16 +10618,16 @@ }, { "name": "symfony/routing", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8e213820c5fea844ecea29203d2a308019007c15" + "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8e213820c5fea844ecea29203d2a308019007c15", - "reference": "8e213820c5fea844ecea29203d2a308019007c15", + "url": "https://api.github.com/repos/symfony/routing/zipball/7614b8ca5fa89b9cd233e21b627bfc5774f586e4", + "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4", "shasum": "" }, "require": { @@ -10648,7 +10679,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.0" + "source": "https://github.com/symfony/routing/tree/v7.3.2" }, "funding": [ { @@ -10659,12 +10690,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-24T20:43:28+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/service-contracts", @@ -10813,16 +10848,16 @@ }, { "name": "symfony/string", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", "shasum": "" }, "require": { @@ -10880,7 +10915,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.0" + "source": "https://github.com/symfony/string/tree/v7.3.2" }, "funding": [ { @@ -10891,25 +10926,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-20T20:19:01+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { "name": "symfony/translation", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "241d5ac4910d256660238a7ecf250deba4c73063" + "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063", - "reference": "241d5ac4910d256660238a7ecf250deba4c73063", + "url": "https://api.github.com/repos/symfony/translation/zipball/81b48f4daa96272efcce9c7a6c4b58e629df3c90", + "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90", "shasum": "" }, "require": { @@ -10976,7 +11015,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.1" + "source": "https://github.com/symfony/translation/tree/v7.3.2" }, "funding": [ { @@ -10987,12 +11026,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-07-30T17:31:46+00:00" }, { "name": "symfony/translation-contracts", @@ -11148,16 +11191,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42" + "reference": "53205bea27450dc5c65377518b3275e126d45e75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", - "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/53205bea27450dc5c65377518b3275e126d45e75", + "reference": "53205bea27450dc5c65377518b3275e126d45e75", "shasum": "" }, "require": { @@ -11169,7 +11212,6 @@ "symfony/console": "<6.4" }, "require-dev": { - "ext-iconv": "*", "symfony/console": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/process": "^6.4|^7.0", @@ -11212,7 +11254,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.1" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.2" }, "funding": [ { @@ -11223,25 +11265,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-07-29T20:02:46+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb" + "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0c3555045a46ab3cd4cc5a69d161225195230edb", - "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b8d7d868da9eb0919e99c8830431ea087d6aae30", + "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30", "shasum": "" }, "require": { @@ -11284,7 +11330,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.1" + "source": "https://github.com/symfony/yaml/tree/v7.3.2" }, "funding": [ { @@ -11295,12 +11341,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-03T06:57:57+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -12039,16 +12089,16 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.15.4", + "version": "v3.16.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "c0667ea91f7185f1e074402c5788195e96bf8106" + "reference": "f265cf5e38577d42311f1a90d619bcd3740bea23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/c0667ea91f7185f1e074402c5788195e96bf8106", - "reference": "c0667ea91f7185f1e074402c5788195e96bf8106", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f265cf5e38577d42311f1a90d619bcd3740bea23", + "reference": "f265cf5e38577d42311f1a90d619bcd3740bea23", "shasum": "" }, "require": { @@ -12056,7 +12106,7 @@ "illuminate/session": "^9|^10|^11|^12", "illuminate/support": "^9|^10|^11|^12", "php": "^8.1", - "php-debugbar/php-debugbar": "~2.1.1", + "php-debugbar/php-debugbar": "~2.2.0", "symfony/finder": "^6|^7" }, "require-dev": { @@ -12076,7 +12126,7 @@ ] }, "branch-alias": { - "dev-master": "3.15-dev" + "dev-master": "3.16-dev" } }, "autoload": { @@ -12108,7 +12158,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.15.4" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.0" }, "funding": [ { @@ -12120,7 +12170,7 @@ "type": "github" } ], - "time": "2025-04-16T06:32:06+00:00" + "time": "2025-07-14T11:56:43+00:00" }, { "name": "brianium/paratest", @@ -12641,16 +12691,16 @@ }, { "name": "laravel/telescope", - "version": "v5.10.0", + "version": "v5.10.2", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "fc0a8662682c0375b534033873debb780c003486" + "reference": "6d249d93ab06dc147ac62ea02b4272c2e7a24b72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/fc0a8662682c0375b534033873debb780c003486", - "reference": "fc0a8662682c0375b534033873debb780c003486", + "url": "https://api.github.com/repos/laravel/telescope/zipball/6d249d93ab06dc147ac62ea02b4272c2e7a24b72", + "reference": "6d249d93ab06dc147ac62ea02b4272c2e7a24b72", "shasum": "" }, "require": { @@ -12704,9 +12754,9 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v5.10.0" + "source": "https://github.com/laravel/telescope/tree/v5.10.2" }, - "time": "2025-07-07T14:47:19+00:00" + "time": "2025-07-24T05:26:13+00:00" }, { "name": "mockery/mockery", @@ -12793,16 +12843,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.3", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -12841,7 +12891,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -12849,7 +12899,7 @@ "type": "tidelift" } ], - "time": "2025-07-05T12:25:42+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nunomaduro/collision", @@ -13394,16 +13444,16 @@ }, { "name": "php-debugbar/php-debugbar", - "version": "v2.1.6", + "version": "v2.2.4", "source": { "type": "git", "url": "https://github.com/php-debugbar/php-debugbar.git", - "reference": "16fa68da5617220594aa5e33fa9de415f94784a0" + "reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/16fa68da5617220594aa5e33fa9de415f94784a0", - "reference": "16fa68da5617220594aa5e33fa9de415f94784a0", + "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/3146d04671f51f69ffec2a4207ac3bdcf13a9f35", + "reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35", "shasum": "" }, "require": { @@ -13411,6 +13461,9 @@ "psr/log": "^1|^2|^3", "symfony/var-dumper": "^4|^5|^6|^7" }, + "replace": { + "maximebf/debugbar": "self.version" + }, "require-dev": { "dbrekelmans/bdi": "^1", "phpunit/phpunit": "^8|^9", @@ -13425,7 +13478,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -13458,9 +13511,9 @@ ], "support": { "issues": "https://github.com/php-debugbar/php-debugbar/issues", - "source": "https://github.com/php-debugbar/php-debugbar/tree/v2.1.6" + "source": "https://github.com/php-debugbar/php-debugbar/tree/v2.2.4" }, - "time": "2025-02-21T17:47:03+00:00" + "time": "2025-07-22T14:01:30+00:00" }, { "name": "php-webdriver/webdriver", @@ -13530,16 +13583,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.18", + "version": "2.1.21", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "ee1f390b7a70cdf74a2b737e554f68afea885db7" + "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ee1f390b7a70cdf74a2b737e554f68afea885db7", - "reference": "ee1f390b7a70cdf74a2b737e554f68afea885db7", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1ccf445757458c06a04eb3f803603cb118fe5fa6", + "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6", "shasum": "" }, "require": { @@ -13584,7 +13637,7 @@ "type": "github" } ], - "time": "2025-07-17T17:22:31+00:00" + "time": "2025-07-28T19:35:08+00:00" }, { "name": "phpunit/php-code-coverage", @@ -15436,16 +15489,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64" + "reference": "1c064a0c67749923483216b081066642751cc2c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4403d87a2c16f33345dca93407a8714ee8c05a64", - "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64", + "url": "https://api.github.com/repos/symfony/http-client/zipball/1c064a0c67749923483216b081066642751cc2c7", + "reference": "1c064a0c67749923483216b081066642751cc2c7", "shasum": "" }, "require": { @@ -15511,7 +15564,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.1" + "source": "https://github.com/symfony/http-client/tree/v7.3.2" }, "funding": [ { @@ -15522,12 +15575,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-28T07:58:39+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/http-client-contracts", diff --git a/package-lock.json b/package-lock.json index 10489a7d4..34b2c1dd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,9 +90,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -104,9 +104,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -121,9 +121,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -138,9 +138,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -155,9 +155,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -172,9 +172,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -189,9 +189,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -206,9 +206,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -223,9 +223,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -257,9 +257,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -274,9 +274,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -291,9 +291,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -308,9 +308,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -325,9 +325,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -342,9 +342,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -359,9 +359,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -376,9 +376,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -393,9 +393,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -410,9 +410,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -427,9 +427,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -444,9 +444,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", "cpu": [ "arm64" ], @@ -478,9 +478,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -495,9 +495,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -512,9 +512,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -529,9 +529,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", + "integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==", "license": "MIT" }, "node_modules/@isaacs/fs-minipass": { @@ -604,9 +604,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", - "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", "cpu": [ "arm" ], @@ -618,9 +618,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", - "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", "cpu": [ "arm64" ], @@ -632,9 +632,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", - "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", "cpu": [ "arm64" ], @@ -646,9 +646,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", - "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", "cpu": [ "x64" ], @@ -660,9 +660,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", - "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", "cpu": [ "arm64" ], @@ -674,9 +674,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", - "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", "cpu": [ "x64" ], @@ -688,9 +688,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", - "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", "cpu": [ "arm" ], @@ -702,9 +702,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", - "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", "cpu": [ "arm" ], @@ -716,9 +716,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", - "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", "cpu": [ "arm64" ], @@ -730,9 +730,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", - "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", "cpu": [ "arm64" ], @@ -744,9 +744,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", - "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", "cpu": [ "loong64" ], @@ -757,10 +757,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", - "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", "cpu": [ "ppc64" ], @@ -772,9 +772,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", - "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", "cpu": [ "riscv64" ], @@ -786,9 +786,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", - "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", "cpu": [ "riscv64" ], @@ -800,9 +800,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", - "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", "cpu": [ "s390x" ], @@ -814,9 +814,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", - "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", "cpu": [ "x64" ], @@ -828,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", - "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", "cpu": [ "x64" ], @@ -842,9 +842,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", - "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", "cpu": [ "arm64" ], @@ -856,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", - "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", "cpu": [ "ia32" ], @@ -870,9 +870,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", - "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", "cpu": [ "x64" ], @@ -1623,9 +1623,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1636,32 +1636,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/estree-walker": { @@ -1687,9 +1687,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { @@ -1875,9 +1875,9 @@ } }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "dev": true, "license": "MIT", "bin": { @@ -2397,9 +2397,9 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "dev": true, "license": "MIT", "peer": true, @@ -2429,9 +2429,9 @@ } }, "node_modules/rollup": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", - "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", "dev": true, "license": "MIT", "dependencies": { @@ -2445,26 +2445,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.45.1", - "@rollup/rollup-android-arm64": "4.45.1", - "@rollup/rollup-darwin-arm64": "4.45.1", - "@rollup/rollup-darwin-x64": "4.45.1", - "@rollup/rollup-freebsd-arm64": "4.45.1", - "@rollup/rollup-freebsd-x64": "4.45.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", - "@rollup/rollup-linux-arm-musleabihf": "4.45.1", - "@rollup/rollup-linux-arm64-gnu": "4.45.1", - "@rollup/rollup-linux-arm64-musl": "4.45.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", - "@rollup/rollup-linux-riscv64-gnu": "4.45.1", - "@rollup/rollup-linux-riscv64-musl": "4.45.1", - "@rollup/rollup-linux-s390x-gnu": "4.45.1", - "@rollup/rollup-linux-x64-gnu": "4.45.1", - "@rollup/rollup-linux-x64-musl": "4.45.1", - "@rollup/rollup-win32-arm64-msvc": "4.45.1", - "@rollup/rollup-win32-ia32-msvc": "4.45.1", - "@rollup/rollup-win32-x64-msvc": "4.45.1", + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" } }, From f06d80e80e364628125ce35e887b16f186f0419c Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Mon, 4 Aug 2025 15:32:15 +0200 Subject: [PATCH 0104/1717] feat(service): add sequin template (#6105) --- public/svgs/sequin.svg | 16 +++++++++ templates/compose/sequin.yaml | 68 +++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 public/svgs/sequin.svg create mode 100644 templates/compose/sequin.yaml diff --git a/public/svgs/sequin.svg b/public/svgs/sequin.svg new file mode 100644 index 000000000..623bc1159 --- /dev/null +++ b/public/svgs/sequin.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/templates/compose/sequin.yaml b/templates/compose/sequin.yaml new file mode 100644 index 000000000..18f88fd89 --- /dev/null +++ b/templates/compose/sequin.yaml @@ -0,0 +1,68 @@ +# documentation: https://sequinstream.com/docs/ +# slogan: The fastest Postgres change data capture +# tags: postgres, sync, data +# logo: svgs/sequin.svg +# port: 7376 + +services: + sequin: + image: sequin/sequin:latest + environment: + - SERVICE_FQDN_SEQUIN_7376 + - SERVER_HOST=${SERVICE_FQDN_SEQUIN} + - PG_HOSTNAME=postgres + - PG_DATABASE=${POSTGRES_DB:-sequin-db} + - PG_PORT=5432 + - PG_USERNAME=${SERVICE_USER_POSTGRES} + - PG_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - PG_POOL_SIZE=20 + - SECRET_KEY_BASE=${SERVICE_REALBASE64_64_SECRETKEY} + - VAULT_KEY=${SERVICE_REALBASE64_VAULTKEY} + - REDIS_URL=redis://redis:6379 + - CONFIG_FILE_PATH=/config/playground.yml + - FEATURE_ACCOUNT_SELF_SIGNUP=${FEATURE_ACCOUNT_SELF_SIGNUP:-false} + - SEQUIN_TELEMETRY_DISABLED=${SEQUIN_TELEMETRY_DISABLED:-false} + - CRASH_REPORTING_DISABLED=${CRASH_REPORTING_DISABLED:-false} + depends_on: + redis: + condition: service_started + postgres: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "http://localhost:7376/health"] + + postgres: + image: 'postgres:16' + environment: + - POSTGRES_DB=${POSTGRES_DB:-sequin-db} + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + command: + - postgres + - '-c' + - wal_level=logical + healthcheck: + test: + - CMD-SHELL + - 'pg_isready -U ${SERVICE_USER_POSTGRES} -d sequin' + interval: 10s + timeout: 2s + retries: 5 + start_period: 2s + start_interval: 1s + volumes: + - 'postgres_data:/var/lib/postgresql/data' + + redis: + image: 'redis:7' + command: + - redis-server + - '--port' + - '6379' + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 20s + retries: 10 + volumes: + - 'redis_data:/data' From b165d506c968480fed25219b7ab54cab1add813a Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:35:27 +0200 Subject: [PATCH 0105/1717] chore(service): improve sequin - make sure Redis is healthy - formatting --- templates/compose/sequin.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/templates/compose/sequin.yaml b/templates/compose/sequin.yaml index 18f88fd89..35e210bdf 100644 --- a/templates/compose/sequin.yaml +++ b/templates/compose/sequin.yaml @@ -9,7 +9,7 @@ services: image: sequin/sequin:latest environment: - SERVICE_FQDN_SEQUIN_7376 - - SERVER_HOST=${SERVICE_FQDN_SEQUIN} + - SERVER_HOST=${SERVICE_URL_SEQUIN} - PG_HOSTNAME=postgres - PG_DATABASE=${POSTGRES_DB:-sequin-db} - PG_PORT=5432 @@ -25,44 +25,44 @@ services: - CRASH_REPORTING_DISABLED=${CRASH_REPORTING_DISABLED:-false} depends_on: redis: - condition: service_started + condition: service_healthy postgres: condition: service_healthy healthcheck: test: ["CMD", "curl", "http://localhost:7376/health"] postgres: - image: 'postgres:16' + image: postgres:16 environment: - POSTGRES_DB=${POSTGRES_DB:-sequin-db} - POSTGRES_USER=${SERVICE_USER_POSTGRES} - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} command: - postgres - - '-c' + - "-c" - wal_level=logical healthcheck: test: - CMD-SHELL - - 'pg_isready -U ${SERVICE_USER_POSTGRES} -d sequin' + - "pg_isready -U ${SERVICE_USER_POSTGRES} -d sequin" interval: 10s timeout: 2s retries: 5 start_period: 2s start_interval: 1s volumes: - - 'postgres_data:/var/lib/postgresql/data' + - postgres_data:/var/lib/postgresql/data redis: - image: 'redis:7' + image: redis:7 command: - redis-server - - '--port' - - '6379' + - "--port" + - "6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 20s retries: 10 volumes: - - 'redis_data:/data' + - redis_data:/data From 10823666b2a0b032b96025b40c04e6ed7410e7b2 Mon Sep 17 00:00:00 2001 From: Jonas <56066318+OG-Jons@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:48:54 +0900 Subject: [PATCH 0106/1717] feat(service): add pi-hole template (#6020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🏔️ Peak <122374094+peaklabs-dev@users.noreply.github.com> --- public/svgs/pihole.svg | 1 + templates/compose/pi-hole.yaml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 public/svgs/pihole.svg create mode 100644 templates/compose/pi-hole.yaml diff --git a/public/svgs/pihole.svg b/public/svgs/pihole.svg new file mode 100644 index 000000000..a4efefcc8 --- /dev/null +++ b/public/svgs/pihole.svg @@ -0,0 +1 @@ +NewVortex \ No newline at end of file diff --git a/templates/compose/pi-hole.yaml b/templates/compose/pi-hole.yaml new file mode 100644 index 000000000..476067b16 --- /dev/null +++ b/templates/compose/pi-hole.yaml @@ -0,0 +1,30 @@ +# documentation: https://pi-hole.net/ +# slogan: Network-wide Ad Blocking +# tags: ad-block,dns,sinkhole,ntp,dhcp +# logo: svgs/pihole.svg +# port: 80 + +services: + pihole: + image: pihole/pihole:latest + ports: + # DNS Ports + - "53:53/tcp" + - "53:53/udp" + # Uncomment the below if using Pi-hole as your DHCP Server + - "67:67/udp" + # Uncomment the line below if you are using Pi-hole as your NTP server + - "123:123/udp" + environment: + - SERVICE_FQDN_PIHOLE_80 + - TZ=${TZ:-Europe/London} + - FTLCONF_webserver_api_password=${SERVICE_PASSWORD_PIHOLE} + - FTLCONF_dns_listeningMode=${FTLCONF_dns_listeningMode:-all} + volumes: + - pihole-data:/etc/pihole + cap_add: + # See https://github.com/pi-hole/docker-pi-hole#note-on-capabilities + # Required if you are using Pi-hole as your DHCP server, else not needed + - NET_ADMIN + # Required if you are using Pi-hole as your NTP client to be able to set the host's system time + - SYS_TIME From 2a526c54d5ed7d2e52a307266ec9789b8c1d5d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8F=94=EF=B8=8F=20Peak?= <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 21:15:56 +0200 Subject: [PATCH 0107/1717] fix(docker): volumes get delete when stopping a service if `Delete Unused Volumes` is activated (#6317) --- app/Actions/Application/StopApplication.php | 2 +- app/Actions/Database/StopDatabase.php | 2 +- app/Actions/Server/CleanupDocker.php | 6 ++--- app/Actions/Server/UpdateCoolify.php | 2 +- app/Actions/Service/DeleteService.php | 4 +-- app/Actions/Service/StopService.php | 2 +- .../Api/ApplicationsController.php | 6 ++--- .../Controllers/Api/DatabasesController.php | 6 ++--- .../Controllers/Api/ServicesController.php | 6 ++--- app/Jobs/DeleteResourceJob.php | 10 ++++---- app/Jobs/DockerCleanupJob.php | 25 ++++++++++++++++--- app/Jobs/ServerResourceManager.php | 2 +- app/Livewire/Project/Shared/Danger.php | 6 ++--- app/Livewire/Server/DockerCleanup.php | 2 +- 14 files changed, 49 insertions(+), 32 deletions(-) diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 0ca703fce..ee3398b04 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -49,7 +49,7 @@ public function handle(Application $application, bool $previewDeployments = fals } if ($dockerCleanup) { - CleanupDocker::dispatch($server, true); + CleanupDocker::dispatch($server, false, false); } } catch (\Exception $e) { return $e->getMessage(); diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index a03c9269e..6fcdedeeb 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -29,7 +29,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St $this->stopContainer($database, $database->uuid, 30); if ($dockerCleanup) { - CleanupDocker::dispatch($server, true); + CleanupDocker::dispatch($server, false, false); } if ($database->is_public) { diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 754feecb1..392562167 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -11,7 +11,7 @@ class CleanupDocker public string $jobQueue = 'high'; - public function handle(Server $server) + public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $deleteUnusedNetworks = false) { $settings = instanceSettings(); $realtimeImage = config('constants.coolify.realtime_image'); @@ -36,11 +36,11 @@ public function handle(Server $server) "docker images --filter before=$realtimeImageWithoutPrefixVersion --filter reference=$realtimeImageWithoutPrefix | grep $realtimeImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f", ]; - if ($server->settings->delete_unused_volumes) { + if ($deleteUnusedVolumes) { $commands[] = 'docker volume prune -af'; } - if ($server->settings->delete_unused_networks) { + if ($deleteUnusedNetworks) { $commands[] = 'docker network prune -f'; } diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 9a6cc140b..2a06428e2 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -29,7 +29,7 @@ public function handle($manual_update = false) if (! $this->server) { return; } - CleanupDocker::dispatch($this->server); + CleanupDocker::dispatch($this->server, false, false); $this->latestVersion = get_latest_version_of_coolify(); $this->currentVersion = config('constants.coolify.version'); if (! $manual_update) { diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 404e11559..8790901cd 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -11,7 +11,7 @@ class DeleteService { use AsAction; - public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks) + public function handle(Service $service, bool $deleteVolumes, bool $deleteConnectedNetworks, bool $deleteConfigurations, bool $dockerCleanup) { try { $server = data_get($service, 'server'); @@ -71,7 +71,7 @@ public function handle(Service $service, bool $deleteConfigurations, bool $delet $service->forceDelete(); if ($dockerCleanup) { - CleanupDocker::dispatch($server, true); + CleanupDocker::dispatch($server, false, false); } } } diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index a7fa4b8b2..190b8885d 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -40,7 +40,7 @@ public function handle(Service $service, bool $isDeleteOperation = false, bool $ $service->deleteConnectedNetworks(); } if ($dockerCleanup) { - CleanupDocker::dispatch($server, true); + CleanupDocker::dispatch($server, false, false); } } catch (\Exception $e) { return $e->getMessage(); diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 0860c7133..dcf0e5fbe 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1699,10 +1699,10 @@ public function delete_by_uuid(Request $request) DeleteResourceJob::dispatch( resource: $application, - deleteConfigurations: $request->query->get('delete_configurations', true), deleteVolumes: $request->query->get('delete_volumes', true), - dockerCleanup: $request->query->get('docker_cleanup', true), - deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true), + deleteConfigurations: $request->query->get('delete_configurations', true), + dockerCleanup: $request->query->get('docker_cleanup', true) ); return response()->json([ diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 504665f6a..6ac052b3c 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1608,10 +1608,10 @@ public function delete_by_uuid(Request $request) DeleteResourceJob::dispatch( resource: $database, - deleteConfigurations: $request->query->get('delete_configurations', true), deleteVolumes: $request->query->get('delete_volumes', true), - dockerCleanup: $request->query->get('docker_cleanup', true), - deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true), + deleteConfigurations: $request->query->get('delete_configurations', true), + dockerCleanup: $request->query->get('docker_cleanup', true) ); return response()->json([ diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 542be83de..34d8ee669 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -510,10 +510,10 @@ public function delete_by_uuid(Request $request) DeleteResourceJob::dispatch( resource: $service, - deleteConfigurations: $request->query->get('delete_configurations', true), deleteVolumes: $request->query->get('delete_volumes', true), - dockerCleanup: $request->query->get('docker_cleanup', true), - deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true), + deleteConfigurations: $request->query->get('delete_configurations', true), + dockerCleanup: $request->query->get('docker_cleanup', true) ); return response()->json([ diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 2750110f2..a725df52f 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -32,10 +32,10 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue public function __construct( public Application|ApplicationPreview|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, - public bool $deleteConfigurations = true, public bool $deleteVolumes = true, - public bool $dockerCleanup = true, - public bool $deleteConnectedNetworks = true + public bool $deleteConnectedNetworks = true, + public bool $deleteConfigurations = true, + public bool $dockerCleanup = true ) { $this->onQueue('high'); } @@ -66,7 +66,7 @@ public function handle() break; case 'service': StopService::run($this->resource, true); - DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks); + DeleteService::run($this->resource, $this->deleteVolumes, $this->deleteConnectedNetworks, $this->deleteConfigurations, $this->dockerCleanup); return; } @@ -106,7 +106,7 @@ public function handle() if ($this->dockerCleanup) { $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server'); if ($server) { - CleanupDocker::dispatch($server, true); + CleanupDocker::dispatch($server, false, false); } } Artisan::queue('cleanup:stucked-resources'); diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 519728ab0..f3f3a2ae4 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -34,7 +34,12 @@ public function middleware(): array return [(new WithoutOverlapping('docker-cleanup-'.$this->server->uuid))->expireAfter(600)->dontRelease()]; } - public function __construct(public Server $server, public bool $manualCleanup = false) {} + public function __construct( + public Server $server, + public bool $manualCleanup = false, + public bool $deleteUnusedVolumes = false, + public bool $deleteUnusedNetworks = false + ) {} public function handle(): void { @@ -50,7 +55,11 @@ public function handle(): void $this->usageBefore = $this->server->getDiskUsage(); if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) { - $cleanup_log = CleanupDocker::run(server: $this->server); + $cleanup_log = CleanupDocker::run( + server: $this->server, + deleteUnusedVolumes: $this->deleteUnusedVolumes, + deleteUnusedNetworks: $this->deleteUnusedNetworks + ); $usageAfter = $this->server->getDiskUsage(); $message = ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'; @@ -67,7 +76,11 @@ public function handle(): void } if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) { - $cleanup_log = CleanupDocker::run(server: $this->server); + $cleanup_log = CleanupDocker::run( + server: $this->server, + deleteUnusedVolumes: $this->deleteUnusedVolumes, + deleteUnusedNetworks: $this->deleteUnusedNetworks + ); $message = 'Docker cleanup job executed successfully, but no disk usage could be determined.'; $this->execution_log->update([ @@ -81,7 +94,11 @@ public function handle(): void } if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) { - $cleanup_log = CleanupDocker::run(server: $this->server); + $cleanup_log = CleanupDocker::run( + server: $this->server, + deleteUnusedVolumes: $this->deleteUnusedVolumes, + deleteUnusedNetworks: $this->deleteUnusedNetworks + ); $usageAfter = $this->server->getDiskUsage(); $diskSaved = $this->usageBefore - $usageAfter; diff --git a/app/Jobs/ServerResourceManager.php b/app/Jobs/ServerResourceManager.php index cdf8efc56..8a4b55a3f 100644 --- a/app/Jobs/ServerResourceManager.php +++ b/app/Jobs/ServerResourceManager.php @@ -133,7 +133,7 @@ private function processServer(Server $server): void $dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency]; } if ($this->shouldRunNow($dockerCleanupFrequency, $serverTimezone)) { - DockerCleanupJob::dispatch($server); + DockerCleanupJob::dispatch($server, false, $server->settings->delete_unused_volumes, $server->settings->delete_unused_networks); } // Dispatch ServerPatchCheckJob if due (weekly) diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index 7da48f9fb..94a4c161c 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -99,10 +99,10 @@ public function delete($password) $this->resource->delete(); DeleteResourceJob::dispatch( $this->resource, - $this->delete_configurations, $this->delete_volumes, - $this->docker_cleanup, - $this->delete_connected_networks + $this->delete_connected_networks, + $this->delete_configurations, + $this->docker_cleanup ); return redirect()->route('project.resource.index', [ diff --git a/app/Livewire/Server/DockerCleanup.php b/app/Livewire/Server/DockerCleanup.php index d3378d63f..c97a8f2c9 100644 --- a/app/Livewire/Server/DockerCleanup.php +++ b/app/Livewire/Server/DockerCleanup.php @@ -71,7 +71,7 @@ public function instantSave() public function manualCleanup() { try { - DockerCleanupJob::dispatch($this->server, true); + DockerCleanupJob::dispatch($this->server, true, $this->deleteUnusedVolumes, $this->deleteUnusedNetworks); $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); } catch (\Throwable $e) { return handleError($e, $this); From 0998be35036a69110e44ee5fa6ef13d9b0389889 Mon Sep 17 00:00:00 2001 From: Lucas Castelo Date: Mon, 4 Aug 2025 21:22:07 +0200 Subject: [PATCH 0108/1717] feat(services): add Chroma service (#6201) --- public/svgs/chroma.svg | 13 +++++++++++++ templates/compose/chroma.yaml | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 public/svgs/chroma.svg create mode 100644 templates/compose/chroma.yaml diff --git a/public/svgs/chroma.svg b/public/svgs/chroma.svg new file mode 100644 index 000000000..930288fbf --- /dev/null +++ b/public/svgs/chroma.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/templates/compose/chroma.yaml b/templates/compose/chroma.yaml new file mode 100644 index 000000000..11029113b --- /dev/null +++ b/templates/compose/chroma.yaml @@ -0,0 +1,20 @@ +# documentation: https://cookbook.chromadb.dev/ +# slogan: Chroma is the open-source search and retrieval database for AI applications. +# tags: ai,vector-database,semantic-search,machine-learning,bm25,embeddings,llm +# logo: svgs/chroma.svg +# port: 8000 + +services: + chromadb: + image: chromadb/chroma:1.0.15 + volumes: + - chroma-data:/data + environment: + - SERVICE_FQDN_CHROMA_8000 + - IS_PERSISTENT=TRUE + - PERSIST_DIRECTORY=/data + healthcheck: + test: [ "CMD", "/bin/bash", "-c", "cat < /dev/null > /dev/tcp/localhost/8000" ] + interval: 30s + timeout: 10s + retries: 3 From 1d97e3549452fed8d888f5786d8731a8e2ab4017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Andrade=20Guzm=C3=A1n?= Date: Mon, 4 Aug 2025 15:33:17 -0400 Subject: [PATCH 0109/1717] chore(service): add `NOT_SECURED` env to Postiz (#6243) --- templates/compose/postiz.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/compose/postiz.yaml b/templates/compose/postiz.yaml index 2631e16fe..5e4fbcf2f 100644 --- a/templates/compose/postiz.yaml +++ b/templates/compose/postiz.yaml @@ -77,6 +77,7 @@ services: - NEXT_PUBLIC_POLOTNO=${NEXT_PUBLIC_POLOTNO} - IS_GENERAL=true - NX_ADD_PLUGINS=${NX_ADD_PLUGINS:-false} + - NOT_SECURED=${NOT_SECURED:-false} # Payment Settings - FEE_AMOUNT=${FEE_AMOUNT:-0.05} From af22514cad4e6355d4e7a646a296a84c78a0a898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Gon=C3=A7alves=20de=20Andrade?= <0vetor0@gmail.com> Date: Mon, 4 Aug 2025 16:36:38 -0300 Subject: [PATCH 0110/1717] chore(service): improve evolution-api environment variables (#6283) --- templates/compose/evolution-api.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/compose/evolution-api.yaml b/templates/compose/evolution-api.yaml index 87db576f9..ad5718e9d 100644 --- a/templates/compose/evolution-api.yaml +++ b/templates/compose/evolution-api.yaml @@ -105,10 +105,11 @@ services: - WEBHOOK_EVENTS_ERRORS_WEBHOOK=${WEBHOOK_EVENTS_ERRORS_WEBHOOK:-} - 'CONFIG_SESSION_PHONE_CLIENT=${CONFIG_SESSION_PHONE_CLIENT:-Evolution API V2}' - CONFIG_SESSION_PHONE_NAME=${CONFIG_SESSION_PHONE_NAME:-Chrome} - - CONFIG_SESSION_PHONE_VERSION=${CONFIG_SESSION_PHONE_VERSION:-2.3000.1020885143} - QRCODE_LIMIT=${QRCODE_LIMIT:-30} - OPENAI_ENABLED=${OPENAI_ENABLED:-true} - DIFY_ENABLED=${DIFY_ENABLED:-true} + - FLOWISE_ENABLED=${FLOWISE_ENABLED:-true} + - N8N_ENABLED=${N8N_ENABLED:-true} - TYPEBOT_ENABLED=${TYPEBOT_ENABLED:-true} - TYPEBOT_API_VERSION=${TYPEBOT_API_VERSION:-latest} - CHATWOOT_ENABLED=${CHATWOOT_ENABLED:-true} From c483f1bdd7d2fb2a6a4197c08f5292cc0e26fa78 Mon Sep 17 00:00:00 2001 From: Slawa Gladkov Date: Mon, 4 Aug 2025 21:48:48 +0200 Subject: [PATCH 0111/1717] chore(service): update Langfuse template to v3 (#6301) --- public/svgs/langfuse.png | Bin 9788 -> 0 bytes public/svgs/langfuse.svg | 9 ++ templates/compose/langfuse.yaml | 151 +++++++++++++++++++++++++++----- 3 files changed, 136 insertions(+), 24 deletions(-) delete mode 100644 public/svgs/langfuse.png create mode 100644 public/svgs/langfuse.svg diff --git a/public/svgs/langfuse.png b/public/svgs/langfuse.png deleted file mode 100644 index 8dec0fe4ae1dd9c91ea9e8e0d6ed89227fef4143..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9788 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 KDb*-ghx{KZU0w?S diff --git a/public/svgs/langfuse.svg b/public/svgs/langfuse.svg new file mode 100644 index 000000000..b04e07490 --- /dev/null +++ b/public/svgs/langfuse.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/templates/compose/langfuse.yaml b/templates/compose/langfuse.yaml index 125a85351..5797dd4a1 100644 --- a/templates/compose/langfuse.yaml +++ b/templates/compose/langfuse.yaml @@ -1,30 +1,83 @@ # documentation: https://langfuse.com/docs # slogan: Langfuse is an open-source LLM engineering platform that helps teams collaboratively debug, analyze, and iterate on their LLM applications. # tags: ai, qdrant, weaviate, langchain, openai, gpt, llm, lmops, langfuse, llmops, tracing, observation, metrics -# logo: svgs/langfuse.png +# logo: svgs/langfuse.svg # port: 3000 +x-app-env: &app-env + - NEXTAUTH_URL=${SERVICE_FQDN_LANGFUSE} + - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-langfuse} + - SALT=${SERVICE_PASSWORD_SALT} + - ENCRYPTION_KEY=${SERVICE_PASSWORD_64_LANGFUSE} + - TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true} + - LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false} + - CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000 + - CLICKHOUSE_URL=http://clickhouse:8123 + - CLICKHOUSE_USER=clickhouse + - CLICKHOUSE_PASSWORD=$SERVICE_PASSWORD_CLICKHOUSE + - CLICKHOUSE_CLUSTER_ENABLED=false + - LANGFUSE_USE_AZURE_BLOB=false + - LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse + - LANGFUSE_S3_EVENT_UPLOAD_REGION=auto + - LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio + - LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=$SERVICE_PASSWORD_MINIO + - LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://minio:9000 + - LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true + - LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/ + - LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse + - LANGFUSE_S3_MEDIA_UPLOAD_REGION=auto + - LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio + - LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=$SERVICE_PASSWORD_MINIO + - LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://localhost:9090 + - LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true + - LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/ + - LANGFUSE_S3_BATCH_EXPORT_ENABLED=false + - LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse + - LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/ + - LANGFUSE_S3_BATCH_EXPORT_REGION=auto + - LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://minio:9000 + - LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT=http://localhost:9090 + - LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=minio + - LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=$SERVICE_PASSWORD_MINIO + - LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true + - LANGFUSE_INGESTION_QUEUE_DELAY_MS=1 + - LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1000 + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_AUTH=$SERVICE_PASSWORD_REDIS + - REDIS_TLS_ENABLED=false + - REDIS_TLS_CA=/certs/ca.crt + - REDIS_TLS_CERT=/certs/redis.crt + - REDIS_TLS_KEY=/certs/redis.key + - EMAIL_FROM_ADDRESS= + - SMTP_CONNECTION_URL= + - NEXTAUTH_SECRET=${SERVICE_BASE64_NEXTAUTHSECRET} # + - AUTH_DISABLE_SIGNUP=${AUTH_DISABLE_SIGNUP:-true} + - HOSTNAME=${HOSTNAME:-0.0.0.0} + - LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-my-org} + - LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-My Org} + - LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-my-project} + - LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-My Project} + - LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-admin@example.com} + - LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-Admin} + - LANGFUSE_INIT_USER_PASSWORD=${SERVICE_PASSWORD_LANGFUSE} + services: langfuse: - image: langfuse/langfuse:2 + image: docker.io/langfuse/langfuse:3 + restart: always + depends_on: &langfuse-depends-on + postgres: + condition: service_healthy + minio: + condition: service_healthy + redis: + condition: service_healthy + clickhouse: + condition: service_healthy environment: - - SERVICE_FQDN_LANGFUSE_3000 - - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-langfuse} - - DIRECT_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-langfuse} - - SALT=$SERVICE_PASSWORD_SALT - - AUTH_DISABLE_SIGNUP=${AUTH_DISABLE_SIGNUP:-false} - - NEXTAUTH_URL=$SERVICE_FQDN_LANGFUSE_3000 - - NEXTAUTH_SECRET=${SERVICE_BASE64_64_NEXTAUTHSECRET} - - TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-false} - - LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false} - - HOSTNAME=${HOSTNAME:-0.0.0.0} - - LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-Admin} - - LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-admin@example.com} - - LANGFUSE_INIT_USER_PASSWORD=${SERVICE_PASSWORD_LANGFUSE} - - LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-my-org} - - LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-My Org} - - LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-my-project} - - LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-My Project} + <<: *app-env + SERVICE_FQDN_LANGFUSE_3000: ${SERVICE_FQDN_LANGFUSE_3000} healthcheck: test: - CMD @@ -35,17 +88,21 @@ services: interval: 5s timeout: 5s retries: 3 - depends_on: - postgres: - condition: service_healthy + langfuse-worker: + image: langfuse/langfuse-worker:3 + restart: always + environment: + <<: *app-env + depends_on: *langfuse-depends-on postgres: - image: "postgres:16-alpine" + image: postgres:17-alpine + restart: always environment: - POSTGRES_DB=${POSTGRES_DB:-langfuse} - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES - POSTGRES_USER=$SERVICE_USER_POSTGRES volumes: - - "pg-data:/var/lib/postgresql/data" + - langfuse_postgres_data:/var/lib/postgresql/data healthcheck: test: - CMD-SHELL @@ -53,3 +110,49 @@ services: interval: 5s timeout: 5s retries: 10 + redis: + image: redis:8 + restart: always + command: ["sh", "-c", "redis-server --requirepass \"$SERVICE_PASSWORD_REDIS\""] + volumes: + - langfuse_redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "$SERVICE_PASSWORD_REDIS", "PING"] + interval: 3s + timeout: 10s + retries: 10 + minio: + image: minio/minio:latest + restart: always + entrypoint: sh + # create the 'langfuse' bucket before starting the service + command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data' + environment: + - MINIO_ROOT_USER=minio + - MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO + - MINIO_BROWSER=off + volumes: + - langfuse_minio_data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 1s + timeout: 5s + retries: 5 + start_period: 1s + clickhouse: + image: clickhouse/clickhouse-server:latest + restart: always + user: "101:101" + environment: + - CLICKHOUSE_DB=default + - CLICKHOUSE_USER=clickhouse + - CLICKHOUSE_PASSWORD=$SERVICE_PASSWORD_CLICKHOUSE + volumes: + - langfuse_clickhouse_data:/var/lib/clickhouse + - langfuse_clickhouse_logs:/var/log/clickhouse-server + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1 + interval: 5s + timeout: 5s + retries: 10 + start_period: 1s From dcf3b43b30589e855cf5d5296ea18454fef28773 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:10:47 +0200 Subject: [PATCH 0112/1717] refactor(service): improve langfuse - remove minio - adjust ENVs - autogenerate more stuff - formatting --- templates/compose/langfuse.yaml | 120 +++++++++++++------------------- 1 file changed, 48 insertions(+), 72 deletions(-) diff --git a/templates/compose/langfuse.yaml b/templates/compose/langfuse.yaml index 5797dd4a1..ddfa8b944 100644 --- a/templates/compose/langfuse.yaml +++ b/templates/compose/langfuse.yaml @@ -6,52 +6,48 @@ x-app-env: &app-env - NEXTAUTH_URL=${SERVICE_FQDN_LANGFUSE} - - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-langfuse} + - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-langfuse-db} - SALT=${SERVICE_PASSWORD_SALT} - ENCRYPTION_KEY=${SERVICE_PASSWORD_64_LANGFUSE} - - TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true} + - TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-false} - LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false} - CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000 - CLICKHOUSE_URL=http://clickhouse:8123 - - CLICKHOUSE_USER=clickhouse - - CLICKHOUSE_PASSWORD=$SERVICE_PASSWORD_CLICKHOUSE + - CLICKHOUSE_USER=${SERVICE_USER_CLICKHOUSE} + - CLICKHOUSE_PASSWORD=${SERVICE_PASSWORD_CLICKHOUSE} - CLICKHOUSE_CLUSTER_ENABLED=false - - LANGFUSE_USE_AZURE_BLOB=false - - LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse - - LANGFUSE_S3_EVENT_UPLOAD_REGION=auto - - LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio - - LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=$SERVICE_PASSWORD_MINIO - - LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://minio:9000 - - LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true - - LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/ - - LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse - - LANGFUSE_S3_MEDIA_UPLOAD_REGION=auto - - LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio - - LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=$SERVICE_PASSWORD_MINIO - - LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://localhost:9090 - - LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true - - LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/ - - LANGFUSE_S3_BATCH_EXPORT_ENABLED=false - - LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse - - LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/ - - LANGFUSE_S3_BATCH_EXPORT_REGION=auto - - LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://minio:9000 - - LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT=http://localhost:9090 - - LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=minio - - LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=$SERVICE_PASSWORD_MINIO - - LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true - - LANGFUSE_INGESTION_QUEUE_DELAY_MS=1 - - LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1000 + - LANGFUSE_USE_AZURE_BLOB=${LANGFUSE_USE_AZURE_BLOB:-false} + - LANGFUSE_S3_EVENT_UPLOAD_BUCKET=${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse} + - LANGFUSE_S3_EVENT_UPLOAD_REGION=${LANGFUSE_S3_EVENT_UPLOAD_REGION:-auto} + - LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID} + - LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY} + - LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT} + - LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true} + - LANGFUSE_S3_EVENT_UPLOAD_PREFIX=${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/} + - LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse} + - LANGFUSE_S3_MEDIA_UPLOAD_REGION=${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-auto} + - LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID} + - LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY} + - LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT} + - LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true} + - LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/} + - LANGFUSE_S3_BATCH_EXPORT_ENABLED=${LANGFUSE_S3_BATCH_EXPORT_ENABLED:-false} + - LANGFUSE_S3_BATCH_EXPORT_BUCKET=${LANGFUSE_S3_BATCH_EXPORT_BUCKET:-langfuse} + - LANGFUSE_S3_BATCH_EXPORT_PREFIX=${LANGFUSE_S3_BATCH_EXPORT_PREFIX:-exports/} + - LANGFUSE_S3_BATCH_EXPORT_REGION=${LANGFUSE_S3_BATCH_EXPORT_REGION:-auto} + - LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=${LANGFUSE_S3_BATCH_EXPORT_ENDPOINT} + - LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT=${LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT} + - LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=${LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID} + - LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=${LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY} + - LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=${LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE:-true} + - LANGFUSE_INGESTION_QUEUE_DELAY_MS=${LANGFUSE_INGESTION_QUEUE_DELAY_MS:-1} + - LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=${LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS:-1000} - REDIS_HOST=redis - REDIS_PORT=6379 - - REDIS_AUTH=$SERVICE_PASSWORD_REDIS - - REDIS_TLS_ENABLED=false - - REDIS_TLS_CA=/certs/ca.crt - - REDIS_TLS_CERT=/certs/redis.crt - - REDIS_TLS_KEY=/certs/redis.key - - EMAIL_FROM_ADDRESS= - - SMTP_CONNECTION_URL= - - NEXTAUTH_SECRET=${SERVICE_BASE64_NEXTAUTHSECRET} # + - REDIS_AUTH=${SERVICE_PASSWORD_REDIS} + - EMAIL_FROM_ADDRESS=${EMAIL_FROM_ADDRESS:-admin@example.com} + - SMTP_CONNECTION_URL=${SMTP_CONNECTION_URL:-} + - NEXTAUTH_SECRET=${SERVICE_BASE64_NEXTAUTHSECRET} - AUTH_DISABLE_SIGNUP=${AUTH_DISABLE_SIGNUP:-true} - HOSTNAME=${HOSTNAME:-0.0.0.0} - LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-my-org} @@ -59,18 +55,15 @@ x-app-env: &app-env - LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-my-project} - LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-My Project} - LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-admin@example.com} - - LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-Admin} + - LANGFUSE_INIT_USER_NAME=${SERVICE_USER_LANGFUSE} - LANGFUSE_INIT_USER_PASSWORD=${SERVICE_PASSWORD_LANGFUSE} services: langfuse: - image: docker.io/langfuse/langfuse:3 - restart: always + image: langfuse/langfuse:3 depends_on: &langfuse-depends-on postgres: condition: service_healthy - minio: - condition: service_healthy redis: condition: service_healthy clickhouse: @@ -88,19 +81,19 @@ services: interval: 5s timeout: 5s retries: 3 + langfuse-worker: image: langfuse/langfuse-worker:3 - restart: always environment: <<: *app-env depends_on: *langfuse-depends-on + postgres: image: postgres:17-alpine - restart: always environment: - - POSTGRES_DB=${POSTGRES_DB:-langfuse} - - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES - - POSTGRES_USER=$SERVICE_USER_POSTGRES + - POSTGRES_DB=${POSTGRES_DB:-langfuse-db} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_USER=${SERVICE_USER_POSTGRES} volumes: - langfuse_postgres_data:/var/lib/postgresql/data healthcheck: @@ -110,10 +103,12 @@ services: interval: 5s timeout: 5s retries: 10 + redis: image: redis:8 - restart: always command: ["sh", "-c", "redis-server --requirepass \"$SERVICE_PASSWORD_REDIS\""] + environment: + - REDIS_PASSWORD=${SERVICE_PASSWORD_REDIS} volumes: - langfuse_redis_data:/data healthcheck: @@ -121,32 +116,14 @@ services: interval: 3s timeout: 10s retries: 10 - minio: - image: minio/minio:latest - restart: always - entrypoint: sh - # create the 'langfuse' bucket before starting the service - command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data' - environment: - - MINIO_ROOT_USER=minio - - MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO - - MINIO_BROWSER=off - volumes: - - langfuse_minio_data:/data - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 1s - timeout: 5s - retries: 5 - start_period: 1s + clickhouse: image: clickhouse/clickhouse-server:latest - restart: always user: "101:101" environment: - - CLICKHOUSE_DB=default - - CLICKHOUSE_USER=clickhouse - - CLICKHOUSE_PASSWORD=$SERVICE_PASSWORD_CLICKHOUSE + - CLICKHOUSE_DB=${CLICKHOUSE_DB:-default} + - CLICKHOUSE_USER=${SERVICE_USER_CLICKHOUSE} + - CLICKHOUSE_PASSWORD=${SERVICE_PASSWORD_CLICKHOUSE} volumes: - langfuse_clickhouse_data:/var/lib/clickhouse - langfuse_clickhouse_logs:/var/log/clickhouse-server @@ -155,4 +132,3 @@ services: interval: 5s timeout: 5s retries: 10 - start_period: 1s From 36b2bb56c5969ebe63a695094a6c5e7697f2a007 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:11:09 +0200 Subject: [PATCH 0113/1717] Update service-templates.json --- templates/service-templates.json | 57 ++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/templates/service-templates.json b/templates/service-templates.json index 6aa4e1eb8..c037a8889 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -144,7 +144,7 @@ "authentik": { "documentation": "https://docs.goauthentik.io/docs/installation/docker-compose?utm_source=coolify.io", "slogan": "An open-source Identity Provider, focused on flexibility and versatility.", - "compose": "c2VydmljZXM6CiAgYXV0aGVudGlrLXNlcnZlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI1LjYuM30nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogc2VydmVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVVUSEVOVElLU0VSVkVSXzkwMDAKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEOi10cnVlfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL21lZGlhOi9tZWRpYScKICAgICAgLSAnLi9jdXN0b20tdGVtcGxhdGVzOi90ZW1wbGF0ZXMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgYXV0aGVudGlrLXdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI1LjYuM30nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB1c2VyOiByb290CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnLi9tZWRpYTovbWVkaWEnCiAgICAgIC0gJy4vY2VydHM6L2NlcnRzJwogICAgICAtICcuL2N1c3RvbS10ZW1wbGF0ZXM6L3RlbXBsYXRlcycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1kICQke1BPU1RHUkVTX0RCfSAtVSAkJHtQT1NUR1JFU19VU0VSfScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAnYXV0aGVudGlrLWRiOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gUE9TVEdSRVNfREI9YXV0aGVudGlrCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGNvbW1hbmQ6ICctLXNhdmUgNjAgMSAtLWxvZ2xldmVsIHdhcm5pbmcnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdyZWRpcy1jbGkgcGluZyB8IGdyZXAgUE9ORycKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXM6L2RhdGEnCg==", + "compose": "c2VydmljZXM6CiAgYXV0aGVudGlrLXNlcnZlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI1LjYuNH0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogc2VydmVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVVUSEVOVElLU0VSVkVSXzkwMDAKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEOi10cnVlfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL21lZGlhOi9tZWRpYScKICAgICAgLSAnLi9jdXN0b20tdGVtcGxhdGVzOi90ZW1wbGF0ZXMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgYXV0aGVudGlrLXdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI1LjYuNH0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB1c2VyOiByb290CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnLi9tZWRpYTovbWVkaWEnCiAgICAgIC0gJy4vY2VydHM6L2NlcnRzJwogICAgICAtICcuL2N1c3RvbS10ZW1wbGF0ZXM6L3RlbXBsYXRlcycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1kICQke1BPU1RHUkVTX0RCfSAtVSAkJHtQT1NUR1JFU19VU0VSfScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAnYXV0aGVudGlrLWRiOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gUE9TVEdSRVNfREI9YXV0aGVudGlrCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGNvbW1hbmQ6ICctLXNhdmUgNjAgMSAtLWxvZ2xldmVsIHdhcm5pbmcnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdyZWRpcy1jbGkgcGluZyB8IGdyZXAgUE9ORycKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXM6L2RhdGEnCg==", "tags": [ "identity", "login", @@ -402,6 +402,23 @@ "minversion": "0.0.0", "port": "80" }, + "chroma": { + "documentation": "https://cookbook.chromadb.dev/?utm_source=coolify.io", + "slogan": "Chroma is the open-source search and retrieval database for AI applications.", + "compose": "c2VydmljZXM6CiAgY2hyb21hZGI6CiAgICBpbWFnZTogJ2Nocm9tYWRiL2Nocm9tYToxLjAuMTUnCiAgICB2b2x1bWVzOgogICAgICAtICdjaHJvbWEtZGF0YTovZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9DSFJPTUFfODAwMAogICAgICAtIElTX1BFUlNJU1RFTlQ9VFJVRQogICAgICAtIFBFUlNJU1RfRElSRUNUT1JZPS9kYXRhCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL2Jpbi9iYXNoCiAgICAgICAgLSAnLWMnCiAgICAgICAgLSAnY2F0IDwgL2Rldi9udWxsID4gL2Rldi90Y3AvbG9jYWxob3N0LzgwMDAnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMK", + "tags": [ + "ai", + "vector-database", + "semantic-search", + "machine-learning", + "bm25", + "embeddings", + "llm" + ], + "logo": "svgs/chroma.svg", + "minversion": "0.0.0", + "port": "8000" + }, "classicpress-with-mariadb": { "documentation": "https://www.classicpress.net/?utm_source=coolify.io", "slogan": "A lightweight, stable, instantly familiar free open-source content management system, based on WordPress without the block editor (Gutenberg).", @@ -831,7 +848,7 @@ "evolution-api": { "documentation": "https://doc.evolution-api.com/v1/pt/get-started/introduction?utm_source=coolify.io", "slogan": "Evolution API Installation with Postgres and Redis", - "compose": "dmVyc2lvbjogJzMuOCcKc2VydmljZXM6CiAgYXBpOgogICAgaW1hZ2U6ICdldm9hcGljbG91ZC9ldm9sdXRpb24tYXBpOmxhdGVzdCcKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgZGVwZW5kc19vbjoKICAgICAgLSByZWRpcwogICAgICAtIHBvc3RncmVzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRVZPXzgwODAKICAgICAgLSBTRVJWRVJfVVJMPSRTRVJWSUNFX0ZRRE5fRVZPCiAgICAgIC0gJ0RCX1RZUEU9JHtEQl9UWVBFOi1wb3N0Z3Jlc2RifScKICAgICAgLSAnREJfUE9TVEdSRVNEQl9EQVRBQkFTRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0RCX1BPU1RHUkVTREJfSE9TVD0ke0RCX1BPU1RHUkVTREJfSE9TVDotcG9zdGdyZXN9JwogICAgICAtICdEQl9QT1NUR1JFU0RCX1BPUlQ9JHtEQl9QT1NUR1JFU0RCX1BPUlQ6LTU0MzJ9JwogICAgICAtICdEQl9QT1NUR1JFU0RCX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdEQl9QT1NUR1JFU0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0RBVEFCQVNFX1BST1ZJREVSPSR7REFUQUJBU0VfUFJPVklERVI6LXBvc3RncmVzcWx9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdEQVRBQkFTRV9DT05ORUNUSU9OX1VSST1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0RFTF9JTlNUQU5DRT0ke0RFTF9JTlNUQU5DRTotZmFsc2V9JwogICAgICAtICdEQVRBQkFTRV9TQVZFX0RBVEFfSU5TVEFOQ0U9JHtEQVRBQkFTRV9TQVZFX0RBVEFfSU5TVEFOQ0U6LXRydWV9JwogICAgICAtICdEQVRBQkFTRV9TQVZFX0RBVEFfTkVXX01FU1NBR0U9JHtEQVRBQkFTRV9TQVZFX0RBVEFfTkVXX01FU1NBR0U6LXRydWV9JwogICAgICAtICdEQVRBQkFTRV9TQVZFX01FU1NBR0VfVVBEQVRFPSR7REFUQUJBU0VfU0FWRV9NRVNTQUdFX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ0RBVEFCQVNFX1NBVkVfREFUQV9DT05UQUNUUz0ke0RBVEFCQVNFX1NBVkVfREFUQV9DT05UQUNUUzotdHJ1ZX0nCiAgICAgIC0gJ0RBVEFCQVNFX1NBVkVfREFUQV9DSEFUUz0ke0RBVEFCQVNFX1NBVkVfREFUQV9DSEFUUzotdHJ1ZX0nCiAgICAgIC0gJ0RBVEFCQVNFX1NBVkVfREFUQV9MQUJFTFM9JHtEQVRBQkFTRV9TQVZFX0RBVEFfTEFCRUxTOi10cnVlfScKICAgICAgLSAnREFUQUJBU0VfU0FWRV9EQVRBX0hJU1RPUklDPSR7REFUQUJBU0VfU0FWRV9EQVRBX0hJU1RPUklDOi10cnVlfScKICAgICAgLSAnREFUQUJBU0VfQ09OTkVDVElPTl9DTElFTlRfTkFNRT0ke0RBVEFCQVNFX0NPTk5FQ1RJT05fQ0xJRU5UX05BTUU6LWV2b2x1dGlvbl92Mn0nCiAgICAgIC0gJ1JBQkJJVE1RX0VOQUJMRUQ9JHtSQUJCSVRNUV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1JBQkJJVE1RX1VSST0ke1JBQkJJVE1RX1VSSTotYW1xcDovL2FkbWluOmFkbWluQHJhYmJpdG1xOjU2NzIvZGVmYXVsdH0nCiAgICAgIC0gJ1JBQkJJVE1RX0VYQ0hBTkdFX05BTUU9JHtSQUJCSVRNUV9FWENIQU5HRV9OQU1FOi1ldm9sdXRpb25fdjJ9JwogICAgICAtICdSQUJCSVRNUV9HTE9CQUxfRU5BQkxFRD0ke1JBQkJJVE1RX0dMT0JBTF9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0VWRU5UU19BUFBMSUNBVElPTl9TVEFSVFVQPSR7UkFCQklUTVFfRVZFTlRTX0FQUExJQ0FUSU9OX1NUQVJUVVA6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0lOU1RBTkNFX0NSRUFURT0ke1JBQkJJVE1RX0VWRU5UU19JTlNUQU5DRV9DUkVBVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0lOU1RBTkNFX0RFTEVURT0ke1JBQkJJVE1RX0VWRU5UU19JTlNUQU5DRV9ERUxFVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX1FSQ09ERV9VUERBVEVEPSR7UkFCQklUTVFfRVZFTlRTX1FSQ09ERV9VUERBVEVEOi1mYWxzZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0VWRU5UU19NRVNTQUdFU19TRVQ9JHtSQUJCSVRNUV9FVkVOVFNfTUVTU0FHRVNfU0VUOi1mYWxzZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0VWRU5UU19NRVNTQUdFU19VUFNFUlQ9JHtSQUJCSVRNUV9FVkVOVFNfTUVTU0FHRVNfVVBTRVJUOi10cnVlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX01FU1NBR0VTX0VESVRFRD0ke1JBQkJJVE1RX0VWRU5UU19NRVNTQUdFU19FRElURUQ6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX01FU1NBR0VTX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19NRVNTQUdFU19VUERBVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX01FU1NBR0VTX0RFTEVURT0ke1JBQkJJVE1RX0VWRU5UU19NRVNTQUdFU19ERUxFVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX1NFTkRfTUVTU0FHRT0ke1JBQkJJVE1RX0VWRU5UU19TRU5EX01FU1NBR0U6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NPTlRBQ1RTX1NFVD0ke1JBQkJJVE1RX0VWRU5UU19DT05UQUNUU19TRVQ6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NPTlRBQ1RTX1VQU0VSVD0ke1JBQkJJVE1RX0VWRU5UU19DT05UQUNUU19VUFNFUlQ6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NPTlRBQ1RTX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19DT05UQUNUU19VUERBVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX1BSRVNFTkNFX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19QUkVTRU5DRV9VUERBVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NIQVRTX1NFVD0ke1JBQkJJVE1RX0VWRU5UU19DSEFUU19TRVQ6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NIQVRTX1VQU0VSVD0ke1JBQkJJVE1RX0VWRU5UU19DSEFUU19VUFNFUlQ6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NIQVRTX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19DSEFUU19VUERBVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NIQVRTX0RFTEVURT0ke1JBQkJJVE1RX0VWRU5UU19DSEFUU19ERUxFVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0dST1VQU19VUFNFUlQ9JHtSQUJCSVRNUV9FVkVOVFNfR1JPVVBTX1VQU0VSVDotZmFsc2V9JwogICAgICAtICdSQUJCSVRNUV9FVkVOVFNfR1JPVVBfVVBEQVRFPSR7UkFCQklUTVFfRVZFTlRTX0dST1VQX1VQREFURTotZmFsc2V9JwogICAgICAtICdSQUJCSVRNUV9FVkVOVFNfR1JPVVBfUEFSVElDSVBBTlRTX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19HUk9VUF9QQVJUSUNJUEFOVFNfVVBEQVRFOi1mYWxzZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0VWRU5UU19DT05ORUNUSU9OX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19DT05ORUNUSU9OX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0VWRU5UU19DQUxMPSR7UkFCQklUTVFfRVZFTlRTX0NBTEw6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX1RZUEVCT1RfU1RBUlQ9JHtSQUJCSVRNUV9FVkVOVFNfVFlQRUJPVF9TVEFSVDotZmFsc2V9JwogICAgICAtICdSQUJCSVRNUV9FVkVOVFNfVFlQRUJPVF9DSEFOR0VfU1RBVFVTPSR7UkFCQklUTVFfRVZFTlRTX1RZUEVCT1RfQ0hBTkdFX1NUQVRVUzotZmFsc2V9JwogICAgICAtICdTUVNfRU5BQkxFRD0ke1NRU19FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1NRU19BQ0NFU1NfS0VZX0lEPSR7U1FTX0FDQ0VTU19LRVlfSUQ6LX0nCiAgICAgIC0gJ1NRU19TRUNSRVRfQUNDRVNTX0tFWT0ke1NRU19TRUNSRVRfQUNDRVNTX0tFWTotfScKICAgICAgLSAnU1FTX0FDQ09VTlRfSUQ9JHtTUVNfQUNDT1VOVF9JRDotfScKICAgICAgLSAnU1FTX1JFR0lPTj0ke1NRU19SRUdJT046LX0nCiAgICAgIC0gJ1dFQlNPQ0tFVF9FTkFCTEVEPSR7V0VCU09DS0VUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnV0VCU09DS0VUX0dMT0JBTF9FVkVOVFM9JHtXRUJTT0NLRVRfR0xPQkFMX0VWRU5UUzotZmFsc2V9JwogICAgICAtICdXQV9CVVNJTkVTU19UT0tFTl9XRUJIT09LPSR7V0FfQlVTSU5FU1NfVE9LRU5fV0VCSE9PSzotZXZvbHV0aW9ufScKICAgICAgLSAnV0FfQlVTSU5FU1NfVVJMPSR7V0FfQlVTSU5FU1NfVVJMOi1odHRwczovL2dyYXBoLmZhY2Vib29rLmNvbX0nCiAgICAgIC0gJ1dBX0JVU0lORVNTX1ZFUlNJT049JHtXQV9CVVNJTkVTU19WRVJTSU9OOi12MjAuMH0nCiAgICAgIC0gJ1dBX0JVU0lORVNTX0xBTkdVQUdFPSR7V0FfQlVTSU5FU1NfTEFOR1VBR0U6LXB0X0JSfScKICAgICAgLSAiV0VCSE9PS19HTE9CQUxfVVJMPSR7V0VCSE9PS19HTE9CQUxfVVJMOi0nJ30iCiAgICAgIC0gJ1dFQkhPT0tfR0xPQkFMX0VOQUJMRUQ9JHtXRUJIT09LX0dMT0JBTF9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1dFQkhPT0tfR0xPQkFMX1dFQkhPT0tfQllfRVZFTlRTPSR7V0VCSE9PS19HTE9CQUxfV0VCSE9PS19CWV9FVkVOVFM6LWZhbHNlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfQVBQTElDQVRJT05fU1RBUlRVUD0ke1dFQkhPT0tfRVZFTlRTX0FQUExJQ0FUSU9OX1NUQVJUVVA6LWZhbHNlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfUVJDT0RFX1VQREFURUQ9JHtXRUJIT09LX0VWRU5UU19RUkNPREVfVVBEQVRFRDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1NFVD0ke1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1NFVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1VQU0VSVD0ke1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1VQU0VSVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX0VESVRFRD0ke1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX0VESVRFRDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1VQREFURT0ke1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX0RFTEVURT0ke1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX0RFTEVURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX1NFTkRfTUVTU0FHRT0ke1dFQkhPT0tfRVZFTlRTX1NFTkRfTUVTU0FHRTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1NFVD0ke1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1NFVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1VQU0VSVD0ke1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1VQU0VSVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1VQREFURT0ke1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX1BSRVNFTkNFX1VQREFURT0ke1dFQkhPT0tfRVZFTlRTX1BSRVNFTkNFX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NIQVRTX1NFVD0ke1dFQkhPT0tfRVZFTlRTX0NIQVRTX1NFVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NIQVRTX1VQU0VSVD0ke1dFQkhPT0tfRVZFTlRTX0NIQVRTX1VQU0VSVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NIQVRTX1VQREFURT0ke1dFQkhPT0tfRVZFTlRTX0NIQVRTX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NIQVRTX0RFTEVURT0ke1dFQkhPT0tfRVZFTlRTX0NIQVRTX0RFTEVURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0dST1VQU19VUFNFUlQ9JHtXRUJIT09LX0VWRU5UU19HUk9VUFNfVVBTRVJUOi10cnVlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfR1JPVVBTX1VQREFURT0ke1dFQkhPT0tfRVZFTlRTX0dST1VQU19VUERBVEU6LXRydWV9JwogICAgICAtICdXRUJIT09LX0VWRU5UU19HUk9VUF9QQVJUSUNJUEFOVFNfVVBEQVRFPSR7V0VCSE9PS19FVkVOVFNfR1JPVVBfUEFSVElDSVBBTlRTX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NPTk5FQ1RJT05fVVBEQVRFPSR7V0VCSE9PS19FVkVOVFNfQ09OTkVDVElPTl9VUERBVEU6LXRydWV9JwogICAgICAtICdXRUJIT09LX0VWRU5UU19MQUJFTFNfRURJVD0ke1dFQkhPT0tfRVZFTlRTX0xBQkVMU19FRElUOi10cnVlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfTEFCRUxTX0FTU09DSUFUSU9OPSR7V0VCSE9PS19FVkVOVFNfTEFCRUxTX0FTU09DSUFUSU9OOi10cnVlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfQ0FMTD0ke1dFQkhPT0tfRVZFTlRTX0NBTEw6LXRydWV9JwogICAgICAtICdXRUJIT09LX0VWRU5UU19UWVBFQk9UX1NUQVJUPSR7V0VCSE9PS19FVkVOVFNfVFlQRUJPVF9TVEFSVDotZmFsc2V9JwogICAgICAtICdXRUJIT09LX0VWRU5UU19UWVBFQk9UX0NIQU5HRV9TVEFUVVM9JHtXRUJIT09LX0VWRU5UU19UWVBFQk9UX0NIQU5HRV9TVEFUVVM6LWZhbHNlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfRVJST1JTPSR7V0VCSE9PS19FVkVOVFNfRVJST1JTOi1mYWxzZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0VSUk9SU19XRUJIT09LPSR7V0VCSE9PS19FVkVOVFNfRVJST1JTX1dFQkhPT0s6LX0nCiAgICAgIC0gJ0NPTkZJR19TRVNTSU9OX1BIT05FX0NMSUVOVD0ke0NPTkZJR19TRVNTSU9OX1BIT05FX0NMSUVOVDotRXZvbHV0aW9uIEFQSSBWMn0nCiAgICAgIC0gJ0NPTkZJR19TRVNTSU9OX1BIT05FX05BTUU9JHtDT05GSUdfU0VTU0lPTl9QSE9ORV9OQU1FOi1DaHJvbWV9JwogICAgICAtICdDT05GSUdfU0VTU0lPTl9QSE9ORV9WRVJTSU9OPSR7Q09ORklHX1NFU1NJT05fUEhPTkVfVkVSU0lPTjotMi4zMDAwLjEwMjA4ODUxNDN9JwogICAgICAtICdRUkNPREVfTElNSVQ9JHtRUkNPREVfTElNSVQ6LTMwfScKICAgICAgLSAnT1BFTkFJX0VOQUJMRUQ9JHtPUEVOQUlfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ0RJRllfRU5BQkxFRD0ke0RJRllfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ1RZUEVCT1RfRU5BQkxFRD0ke1RZUEVCT1RfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ1RZUEVCT1RfQVBJX1ZFUlNJT049JHtUWVBFQk9UX0FQSV9WRVJTSU9OOi1sYXRlc3R9JwogICAgICAtICdDSEFUV09PVF9FTkFCTEVEPSR7Q0hBVFdPT1RfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ0NIQVRXT09UX01FU1NBR0VfUkVBRD0ke0NIQVRXT09UX01FU1NBR0VfUkVBRDotdHJ1ZX0nCiAgICAgIC0gJ0NIQVRXT09UX01FU1NBR0VfREVMRVRFPSR7Q0hBVFdPT1RfTUVTU0FHRV9ERUxFVEU6LXRydWV9JwogICAgICAtICdDSEFUV09PVF9JTVBPUlRfREFUQUJBU0VfQ09OTkVDVElPTl9VUkk9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtICdDSEFUV09PVF9JTVBPUlRfUExBQ0VIT0xERVJfTUVESUFfTUVTU0FHRT0ke0NIQVRXT09UX0lNUE9SVF9QTEFDRUhPTERFUl9NRURJQV9NRVNTQUdFOi10cnVlfScKICAgICAgLSAnQ0FDSEVfUkVESVNfRU5BQkxFRD0ke0NBQ0hFX1JFRElTX0VOQUJMRUQ6LXRydWV9JwogICAgICAtICdDQUNIRV9SRURJU19VUkk9JHtDQUNIRV9SRURJU19VUkk6LXJlZGlzOi8vcmVkaXM6NjM3OS82fScKICAgICAgLSAnQ0FDSEVfUkVESVNfUFJFRklYX0tFWT0ke0NBQ0hFX1JFRElTX1BSRUZJWF9LRVk6LWV2b2x1dGlvbl92Mn0nCiAgICAgIC0gJ0NBQ0hFX1JFRElTX1NBVkVfSU5TVEFOQ0VTPSR7Q0FDSEVfUkVESVNfU0FWRV9JTlNUQU5DRVM6LWZhbHNlfScKICAgICAgLSAnQ0FDSEVfTE9DQUxfRU5BQkxFRD0ke0NBQ0hFX0xPQ0FMX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnUzNfRU5BQkxFRD0ke1MzX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnUzNfQUNDRVNTX0tFWT0ke1MzX0FDQ0VTU19LRVk6LX0nCiAgICAgIC0gJ1MzX1NFQ1JFVF9LRVk9JHtTM19TRUNSRVRfS0VZOi19JwogICAgICAtICdTM19CVUNLRVQ9JHtTM19CVUNLRVQ6LWV2b2x1dGlvbn0nCiAgICAgIC0gJ1MzX1BPUlQ9JHtTM19QT1JUOi00NDN9JwogICAgICAtICdTM19SRUdJT049JHtTM19SRUdJT046LXVzLWVhc3QtMX0nCiAgICAgIC0gJ1MzX0VORFBPSU5UPSR7UzNfRU5EUE9JTlQ6LWZpbGVzLnNpdGUuY29tfScKICAgICAgLSAnUzNfVVNFX1NTTD0ke1MzX1VTRV9TU0w6LXRydWV9JwogICAgICAtICdBVVRIRU5USUNBVElPTl9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9BVVRIRU5USUNBVElPTkFQSUtFWX0nCiAgICAgIC0gJ0FVVEhFTlRJQ0FUSU9OX0VYUE9TRV9JTl9GRVRDSF9JTlNUQU5DRVM9JHtBVVRIRU5USUNBVElPTl9FWFBPU0VfSU5fRkVUQ0hfSU5TVEFOQ0VTOi10cnVlfScKICAgICAgLSAnTEFOR1VBR0U9JHtMQU5HVUFHRTotZW59JwogICAgdm9sdW1lczoKICAgICAgLSAnZXZvbHV0aW9uX2luc3RhbmNlczovZXZvbHV0aW9uL2luc3RhbmNlcycKICAgIGV4cG9zZToKICAgICAgLSA4MDgwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICJyZWRpcy1zZXJ2ZXIgLS1wb3J0IDYzNzkgLS1hcHBlbmRvbmx5IHllc1xuIgogICAgcmVzdGFydDogYWx3YXlzCiAgICB2b2x1bWVzOgogICAgICAtICdldm9sdXRpb25fcmVkaXM6L2RhdGEnCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGNvbW1hbmQ6CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSAnLWMnCiAgICAgIC0gbWF4X2Nvbm5lY3Rpb25zPTEwMDAKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdBVVRIRU5USUNBVElPTl9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9BVVRIRU5USUNBVElPTkFQSUtFWX0nCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwp2b2x1bWVzOgogIGV2b2x1dGlvbl9pbnN0YW5jZXM6IG51bGwKICBldm9sdXRpb25fcmVkaXM6IG51bGwKICBwb3N0Z3Jlc19kYXRhOiBudWxsCg==", + "compose": "dmVyc2lvbjogJzMuOCcKc2VydmljZXM6CiAgYXBpOgogICAgaW1hZ2U6ICdldm9hcGljbG91ZC9ldm9sdXRpb24tYXBpOmxhdGVzdCcKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgZGVwZW5kc19vbjoKICAgICAgLSByZWRpcwogICAgICAtIHBvc3RncmVzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRVZPXzgwODAKICAgICAgLSBTRVJWRVJfVVJMPSRTRVJWSUNFX0ZRRE5fRVZPCiAgICAgIC0gJ0RCX1RZUEU9JHtEQl9UWVBFOi1wb3N0Z3Jlc2RifScKICAgICAgLSAnREJfUE9TVEdSRVNEQl9EQVRBQkFTRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0RCX1BPU1RHUkVTREJfSE9TVD0ke0RCX1BPU1RHUkVTREJfSE9TVDotcG9zdGdyZXN9JwogICAgICAtICdEQl9QT1NUR1JFU0RCX1BPUlQ9JHtEQl9QT1NUR1JFU0RCX1BPUlQ6LTU0MzJ9JwogICAgICAtICdEQl9QT1NUR1JFU0RCX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdEQl9QT1NUR1JFU0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0RBVEFCQVNFX1BST1ZJREVSPSR7REFUQUJBU0VfUFJPVklERVI6LXBvc3RncmVzcWx9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdEQVRBQkFTRV9DT05ORUNUSU9OX1VSST1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0RFTF9JTlNUQU5DRT0ke0RFTF9JTlNUQU5DRTotZmFsc2V9JwogICAgICAtICdEQVRBQkFTRV9TQVZFX0RBVEFfSU5TVEFOQ0U9JHtEQVRBQkFTRV9TQVZFX0RBVEFfSU5TVEFOQ0U6LXRydWV9JwogICAgICAtICdEQVRBQkFTRV9TQVZFX0RBVEFfTkVXX01FU1NBR0U9JHtEQVRBQkFTRV9TQVZFX0RBVEFfTkVXX01FU1NBR0U6LXRydWV9JwogICAgICAtICdEQVRBQkFTRV9TQVZFX01FU1NBR0VfVVBEQVRFPSR7REFUQUJBU0VfU0FWRV9NRVNTQUdFX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ0RBVEFCQVNFX1NBVkVfREFUQV9DT05UQUNUUz0ke0RBVEFCQVNFX1NBVkVfREFUQV9DT05UQUNUUzotdHJ1ZX0nCiAgICAgIC0gJ0RBVEFCQVNFX1NBVkVfREFUQV9DSEFUUz0ke0RBVEFCQVNFX1NBVkVfREFUQV9DSEFUUzotdHJ1ZX0nCiAgICAgIC0gJ0RBVEFCQVNFX1NBVkVfREFUQV9MQUJFTFM9JHtEQVRBQkFTRV9TQVZFX0RBVEFfTEFCRUxTOi10cnVlfScKICAgICAgLSAnREFUQUJBU0VfU0FWRV9EQVRBX0hJU1RPUklDPSR7REFUQUJBU0VfU0FWRV9EQVRBX0hJU1RPUklDOi10cnVlfScKICAgICAgLSAnREFUQUJBU0VfQ09OTkVDVElPTl9DTElFTlRfTkFNRT0ke0RBVEFCQVNFX0NPTk5FQ1RJT05fQ0xJRU5UX05BTUU6LWV2b2x1dGlvbl92Mn0nCiAgICAgIC0gJ1JBQkJJVE1RX0VOQUJMRUQ9JHtSQUJCSVRNUV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1JBQkJJVE1RX1VSST0ke1JBQkJJVE1RX1VSSTotYW1xcDovL2FkbWluOmFkbWluQHJhYmJpdG1xOjU2NzIvZGVmYXVsdH0nCiAgICAgIC0gJ1JBQkJJVE1RX0VYQ0hBTkdFX05BTUU9JHtSQUJCSVRNUV9FWENIQU5HRV9OQU1FOi1ldm9sdXRpb25fdjJ9JwogICAgICAtICdSQUJCSVRNUV9HTE9CQUxfRU5BQkxFRD0ke1JBQkJJVE1RX0dMT0JBTF9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0VWRU5UU19BUFBMSUNBVElPTl9TVEFSVFVQPSR7UkFCQklUTVFfRVZFTlRTX0FQUExJQ0FUSU9OX1NUQVJUVVA6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0lOU1RBTkNFX0NSRUFURT0ke1JBQkJJVE1RX0VWRU5UU19JTlNUQU5DRV9DUkVBVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0lOU1RBTkNFX0RFTEVURT0ke1JBQkJJVE1RX0VWRU5UU19JTlNUQU5DRV9ERUxFVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX1FSQ09ERV9VUERBVEVEPSR7UkFCQklUTVFfRVZFTlRTX1FSQ09ERV9VUERBVEVEOi1mYWxzZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0VWRU5UU19NRVNTQUdFU19TRVQ9JHtSQUJCSVRNUV9FVkVOVFNfTUVTU0FHRVNfU0VUOi1mYWxzZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0VWRU5UU19NRVNTQUdFU19VUFNFUlQ9JHtSQUJCSVRNUV9FVkVOVFNfTUVTU0FHRVNfVVBTRVJUOi10cnVlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX01FU1NBR0VTX0VESVRFRD0ke1JBQkJJVE1RX0VWRU5UU19NRVNTQUdFU19FRElURUQ6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX01FU1NBR0VTX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19NRVNTQUdFU19VUERBVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX01FU1NBR0VTX0RFTEVURT0ke1JBQkJJVE1RX0VWRU5UU19NRVNTQUdFU19ERUxFVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX1NFTkRfTUVTU0FHRT0ke1JBQkJJVE1RX0VWRU5UU19TRU5EX01FU1NBR0U6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NPTlRBQ1RTX1NFVD0ke1JBQkJJVE1RX0VWRU5UU19DT05UQUNUU19TRVQ6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NPTlRBQ1RTX1VQU0VSVD0ke1JBQkJJVE1RX0VWRU5UU19DT05UQUNUU19VUFNFUlQ6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NPTlRBQ1RTX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19DT05UQUNUU19VUERBVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX1BSRVNFTkNFX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19QUkVTRU5DRV9VUERBVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NIQVRTX1NFVD0ke1JBQkJJVE1RX0VWRU5UU19DSEFUU19TRVQ6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NIQVRTX1VQU0VSVD0ke1JBQkJJVE1RX0VWRU5UU19DSEFUU19VUFNFUlQ6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NIQVRTX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19DSEFUU19VUERBVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0NIQVRTX0RFTEVURT0ke1JBQkJJVE1RX0VWRU5UU19DSEFUU19ERUxFVEU6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX0dST1VQU19VUFNFUlQ9JHtSQUJCSVRNUV9FVkVOVFNfR1JPVVBTX1VQU0VSVDotZmFsc2V9JwogICAgICAtICdSQUJCSVRNUV9FVkVOVFNfR1JPVVBfVVBEQVRFPSR7UkFCQklUTVFfRVZFTlRTX0dST1VQX1VQREFURTotZmFsc2V9JwogICAgICAtICdSQUJCSVRNUV9FVkVOVFNfR1JPVVBfUEFSVElDSVBBTlRTX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19HUk9VUF9QQVJUSUNJUEFOVFNfVVBEQVRFOi1mYWxzZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0VWRU5UU19DT05ORUNUSU9OX1VQREFURT0ke1JBQkJJVE1RX0VWRU5UU19DT05ORUNUSU9OX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0VWRU5UU19DQUxMPSR7UkFCQklUTVFfRVZFTlRTX0NBTEw6LWZhbHNlfScKICAgICAgLSAnUkFCQklUTVFfRVZFTlRTX1RZUEVCT1RfU1RBUlQ9JHtSQUJCSVRNUV9FVkVOVFNfVFlQRUJPVF9TVEFSVDotZmFsc2V9JwogICAgICAtICdSQUJCSVRNUV9FVkVOVFNfVFlQRUJPVF9DSEFOR0VfU1RBVFVTPSR7UkFCQklUTVFfRVZFTlRTX1RZUEVCT1RfQ0hBTkdFX1NUQVRVUzotZmFsc2V9JwogICAgICAtICdTUVNfRU5BQkxFRD0ke1NRU19FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1NRU19BQ0NFU1NfS0VZX0lEPSR7U1FTX0FDQ0VTU19LRVlfSUQ6LX0nCiAgICAgIC0gJ1NRU19TRUNSRVRfQUNDRVNTX0tFWT0ke1NRU19TRUNSRVRfQUNDRVNTX0tFWTotfScKICAgICAgLSAnU1FTX0FDQ09VTlRfSUQ9JHtTUVNfQUNDT1VOVF9JRDotfScKICAgICAgLSAnU1FTX1JFR0lPTj0ke1NRU19SRUdJT046LX0nCiAgICAgIC0gJ1dFQlNPQ0tFVF9FTkFCTEVEPSR7V0VCU09DS0VUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnV0VCU09DS0VUX0dMT0JBTF9FVkVOVFM9JHtXRUJTT0NLRVRfR0xPQkFMX0VWRU5UUzotZmFsc2V9JwogICAgICAtICdXQV9CVVNJTkVTU19UT0tFTl9XRUJIT09LPSR7V0FfQlVTSU5FU1NfVE9LRU5fV0VCSE9PSzotZXZvbHV0aW9ufScKICAgICAgLSAnV0FfQlVTSU5FU1NfVVJMPSR7V0FfQlVTSU5FU1NfVVJMOi1odHRwczovL2dyYXBoLmZhY2Vib29rLmNvbX0nCiAgICAgIC0gJ1dBX0JVU0lORVNTX1ZFUlNJT049JHtXQV9CVVNJTkVTU19WRVJTSU9OOi12MjAuMH0nCiAgICAgIC0gJ1dBX0JVU0lORVNTX0xBTkdVQUdFPSR7V0FfQlVTSU5FU1NfTEFOR1VBR0U6LXB0X0JSfScKICAgICAgLSAiV0VCSE9PS19HTE9CQUxfVVJMPSR7V0VCSE9PS19HTE9CQUxfVVJMOi0nJ30iCiAgICAgIC0gJ1dFQkhPT0tfR0xPQkFMX0VOQUJMRUQ9JHtXRUJIT09LX0dMT0JBTF9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1dFQkhPT0tfR0xPQkFMX1dFQkhPT0tfQllfRVZFTlRTPSR7V0VCSE9PS19HTE9CQUxfV0VCSE9PS19CWV9FVkVOVFM6LWZhbHNlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfQVBQTElDQVRJT05fU1RBUlRVUD0ke1dFQkhPT0tfRVZFTlRTX0FQUExJQ0FUSU9OX1NUQVJUVVA6LWZhbHNlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfUVJDT0RFX1VQREFURUQ9JHtXRUJIT09LX0VWRU5UU19RUkNPREVfVVBEQVRFRDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1NFVD0ke1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1NFVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1VQU0VSVD0ke1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1VQU0VSVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX0VESVRFRD0ke1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX0VESVRFRDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1VQREFURT0ke1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX0RFTEVURT0ke1dFQkhPT0tfRVZFTlRTX01FU1NBR0VTX0RFTEVURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX1NFTkRfTUVTU0FHRT0ke1dFQkhPT0tfRVZFTlRTX1NFTkRfTUVTU0FHRTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1NFVD0ke1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1NFVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1VQU0VSVD0ke1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1VQU0VSVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1VQREFURT0ke1dFQkhPT0tfRVZFTlRTX0NPTlRBQ1RTX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX1BSRVNFTkNFX1VQREFURT0ke1dFQkhPT0tfRVZFTlRTX1BSRVNFTkNFX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NIQVRTX1NFVD0ke1dFQkhPT0tfRVZFTlRTX0NIQVRTX1NFVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NIQVRTX1VQU0VSVD0ke1dFQkhPT0tfRVZFTlRTX0NIQVRTX1VQU0VSVDotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NIQVRTX1VQREFURT0ke1dFQkhPT0tfRVZFTlRTX0NIQVRTX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NIQVRTX0RFTEVURT0ke1dFQkhPT0tfRVZFTlRTX0NIQVRTX0RFTEVURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0dST1VQU19VUFNFUlQ9JHtXRUJIT09LX0VWRU5UU19HUk9VUFNfVVBTRVJUOi10cnVlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfR1JPVVBTX1VQREFURT0ke1dFQkhPT0tfRVZFTlRTX0dST1VQU19VUERBVEU6LXRydWV9JwogICAgICAtICdXRUJIT09LX0VWRU5UU19HUk9VUF9QQVJUSUNJUEFOVFNfVVBEQVRFPSR7V0VCSE9PS19FVkVOVFNfR1JPVVBfUEFSVElDSVBBTlRTX1VQREFURTotdHJ1ZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0NPTk5FQ1RJT05fVVBEQVRFPSR7V0VCSE9PS19FVkVOVFNfQ09OTkVDVElPTl9VUERBVEU6LXRydWV9JwogICAgICAtICdXRUJIT09LX0VWRU5UU19MQUJFTFNfRURJVD0ke1dFQkhPT0tfRVZFTlRTX0xBQkVMU19FRElUOi10cnVlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfTEFCRUxTX0FTU09DSUFUSU9OPSR7V0VCSE9PS19FVkVOVFNfTEFCRUxTX0FTU09DSUFUSU9OOi10cnVlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfQ0FMTD0ke1dFQkhPT0tfRVZFTlRTX0NBTEw6LXRydWV9JwogICAgICAtICdXRUJIT09LX0VWRU5UU19UWVBFQk9UX1NUQVJUPSR7V0VCSE9PS19FVkVOVFNfVFlQRUJPVF9TVEFSVDotZmFsc2V9JwogICAgICAtICdXRUJIT09LX0VWRU5UU19UWVBFQk9UX0NIQU5HRV9TVEFUVVM9JHtXRUJIT09LX0VWRU5UU19UWVBFQk9UX0NIQU5HRV9TVEFUVVM6LWZhbHNlfScKICAgICAgLSAnV0VCSE9PS19FVkVOVFNfRVJST1JTPSR7V0VCSE9PS19FVkVOVFNfRVJST1JTOi1mYWxzZX0nCiAgICAgIC0gJ1dFQkhPT0tfRVZFTlRTX0VSUk9SU19XRUJIT09LPSR7V0VCSE9PS19FVkVOVFNfRVJST1JTX1dFQkhPT0s6LX0nCiAgICAgIC0gJ0NPTkZJR19TRVNTSU9OX1BIT05FX0NMSUVOVD0ke0NPTkZJR19TRVNTSU9OX1BIT05FX0NMSUVOVDotRXZvbHV0aW9uIEFQSSBWMn0nCiAgICAgIC0gJ0NPTkZJR19TRVNTSU9OX1BIT05FX05BTUU9JHtDT05GSUdfU0VTU0lPTl9QSE9ORV9OQU1FOi1DaHJvbWV9JwogICAgICAtICdRUkNPREVfTElNSVQ9JHtRUkNPREVfTElNSVQ6LTMwfScKICAgICAgLSAnT1BFTkFJX0VOQUJMRUQ9JHtPUEVOQUlfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ0RJRllfRU5BQkxFRD0ke0RJRllfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ0ZMT1dJU0VfRU5BQkxFRD0ke0ZMT1dJU0VfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ044Tl9FTkFCTEVEPSR7TjhOX0VOQUJMRUQ6LXRydWV9JwogICAgICAtICdUWVBFQk9UX0VOQUJMRUQ9JHtUWVBFQk9UX0VOQUJMRUQ6LXRydWV9JwogICAgICAtICdUWVBFQk9UX0FQSV9WRVJTSU9OPSR7VFlQRUJPVF9BUElfVkVSU0lPTjotbGF0ZXN0fScKICAgICAgLSAnQ0hBVFdPT1RfRU5BQkxFRD0ke0NIQVRXT09UX0VOQUJMRUQ6LXRydWV9JwogICAgICAtICdDSEFUV09PVF9NRVNTQUdFX1JFQUQ9JHtDSEFUV09PVF9NRVNTQUdFX1JFQUQ6LXRydWV9JwogICAgICAtICdDSEFUV09PVF9NRVNTQUdFX0RFTEVURT0ke0NIQVRXT09UX01FU1NBR0VfREVMRVRFOi10cnVlfScKICAgICAgLSAnQ0hBVFdPT1RfSU1QT1JUX0RBVEFCQVNFX0NPTk5FQ1RJT05fVVJJPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWNoYXR3b290fScKICAgICAgLSAnQ0hBVFdPT1RfSU1QT1JUX1BMQUNFSE9MREVSX01FRElBX01FU1NBR0U9JHtDSEFUV09PVF9JTVBPUlRfUExBQ0VIT0xERVJfTUVESUFfTUVTU0FHRTotdHJ1ZX0nCiAgICAgIC0gJ0NBQ0hFX1JFRElTX0VOQUJMRUQ9JHtDQUNIRV9SRURJU19FTkFCTEVEOi10cnVlfScKICAgICAgLSAnQ0FDSEVfUkVESVNfVVJJPSR7Q0FDSEVfUkVESVNfVVJJOi1yZWRpczovL3JlZGlzOjYzNzkvNn0nCiAgICAgIC0gJ0NBQ0hFX1JFRElTX1BSRUZJWF9LRVk9JHtDQUNIRV9SRURJU19QUkVGSVhfS0VZOi1ldm9sdXRpb25fdjJ9JwogICAgICAtICdDQUNIRV9SRURJU19TQVZFX0lOU1RBTkNFUz0ke0NBQ0hFX1JFRElTX1NBVkVfSU5TVEFOQ0VTOi1mYWxzZX0nCiAgICAgIC0gJ0NBQ0hFX0xPQ0FMX0VOQUJMRUQ9JHtDQUNIRV9MT0NBTF9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1MzX0VOQUJMRUQ9JHtTM19FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1MzX0FDQ0VTU19LRVk9JHtTM19BQ0NFU1NfS0VZOi19JwogICAgICAtICdTM19TRUNSRVRfS0VZPSR7UzNfU0VDUkVUX0tFWTotfScKICAgICAgLSAnUzNfQlVDS0VUPSR7UzNfQlVDS0VUOi1ldm9sdXRpb259JwogICAgICAtICdTM19QT1JUPSR7UzNfUE9SVDotNDQzfScKICAgICAgLSAnUzNfUkVHSU9OPSR7UzNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdTM19FTkRQT0lOVD0ke1MzX0VORFBPSU5UOi1maWxlcy5zaXRlLmNvbX0nCiAgICAgIC0gJ1MzX1VTRV9TU0w9JHtTM19VU0VfU1NMOi10cnVlfScKICAgICAgLSAnQVVUSEVOVElDQVRJT05fQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfQVVUSEVOVElDQVRJT05BUElLRVl9JwogICAgICAtICdBVVRIRU5USUNBVElPTl9FWFBPU0VfSU5fRkVUQ0hfSU5TVEFOQ0VTPSR7QVVUSEVOVElDQVRJT05fRVhQT1NFX0lOX0ZFVENIX0lOU1RBTkNFUzotdHJ1ZX0nCiAgICAgIC0gJ0xBTkdVQUdFPSR7TEFOR1VBR0U6LWVufScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2V2b2x1dGlvbl9pbnN0YW5jZXM6L2V2b2x1dGlvbi9pbnN0YW5jZXMnCiAgICBleHBvc2U6CiAgICAgIC0gODA4MAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczpsYXRlc3QnCiAgICBjb21tYW5kOiAicmVkaXMtc2VydmVyIC0tcG9ydCA2Mzc5IC0tYXBwZW5kb25seSB5ZXNcbiIKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgdm9sdW1lczoKICAgICAgLSAnZXZvbHV0aW9uX3JlZGlzOi9kYXRhJwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICBjb21tYW5kOgogICAgICAtIHBvc3RncmVzCiAgICAgIC0gJy1jJwogICAgICAtIG1heF9jb25uZWN0aW9ucz0xMDAwCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnQVVUSEVOVElDQVRJT05fQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfQVVUSEVOVElDQVRJT05BUElLRVl9JwogICAgcmVzdGFydDogYWx3YXlzCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc19kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKdm9sdW1lczoKICBldm9sdXRpb25faW5zdGFuY2VzOiBudWxsCiAgZXZvbHV0aW9uX3JlZGlzOiBudWxsCiAgcG9zdGdyZXNfZGF0YTogbnVsbAo=", "tags": [ "evolution-api", "evo-api", @@ -1753,7 +1770,7 @@ "langfuse": { "documentation": "https://langfuse.com/docs?utm_source=coolify.io", "slogan": "Langfuse is an open-source LLM engineering platform that helps teams collaboratively debug, analyze, and iterate on their LLM applications.", - "compose": "c2VydmljZXM6CiAgbGFuZ2Z1c2U6CiAgICBpbWFnZTogJ2xhbmdmdXNlL2xhbmdmdXNlOjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTEFOR0ZVU0VfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGFuZ2Z1c2V9JwogICAgICAtICdESVJFQ1RfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlfScKICAgICAgLSBTQUxUPSRTRVJWSUNFX1BBU1NXT1JEX1NBTFQKICAgICAgLSAnQVVUSF9ESVNBQkxFX1NJR05VUD0ke0FVVEhfRElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSBORVhUQVVUSF9VUkw9JFNFUlZJQ0VfRlFETl9MQU5HRlVTRV8zMDAwCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0XzY0X05FWFRBVVRIU0VDUkVUfScKICAgICAgLSAnVEVMRU1FVFJZX0VOQUJMRUQ9JHtURUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogICAgICAtICdIT1NUTkFNRT0ke0hPU1ROQU1FOi0wLjAuMC4wfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX05BTUU9JHtMQU5HRlVTRV9JTklUX1VTRVJfTkFNRTotQWRtaW59JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw9JHtMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9MQU5HRlVTRX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfT1JHX0lEPSR7TEFOR0ZVU0VfSU5JVF9PUkdfSUQ6LW15LW9yZ30nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfT1JHX05BTUU9JHtMQU5HRlVTRV9JTklUX09SR19OQU1FOi1NeSBPcmd9JwogICAgICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ6LW15LXByb2plY3R9JwogICAgICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRT0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FOi1NeSBQcm9qZWN0fScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9wdWJsaWMvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWxhbmdmdXNlfScKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAncGctZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLWggbG9jYWxob3N0IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "eC1hcHAtZW52OgogIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfRlFETl9MQU5HRlVTRX0nCiAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAtICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAtICdFTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfTEFOR0ZVU0V9JwogIC0gJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogIC0gJ0NMSUNLSE9VU0VfTUlHUkFUSU9OX1VSTD1jbGlja2hvdXNlOi8vY2xpY2tob3VzZTo5MDAwJwogIC0gJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogIC0gJ0NMSUNLSE9VU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0NMSUNLSE9VU0V9JwogIC0gQ0xJQ0tIT1VTRV9DTFVTVEVSX0VOQUJMRUQ9ZmFsc2UKICAtICdMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQj0ke0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9COi1mYWxzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ6LWxhbmdmdXNlfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYOi1ldmVudHMvfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg6LW1lZGlhL30nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRDotZmFsc2V9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUOi1sYW5nZnVzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg6LWV4cG9ydHMvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTjotYXV0b30nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VORFBPSU5UfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TOi0xfScKICAtICdMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TOi0xMDAwfScKICAtIFJFRElTX0hPU1Q9cmVkaXMKICAtIFJFRElTX1BPUlQ9NjM3OQogIC0gJ1JFRElTX0FVVEg9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1M6LWFkbWluQGV4YW1wbGUuY29tfScKICAtICdTTVRQX0NPTk5FQ1RJT05fVVJMPSR7U01UUF9DT05ORUNUSU9OX1VSTDotfScKICAtICdORVhUQVVUSF9TRUNSRVQ9JHtTRVJWSUNFX0JBU0U2NF9ORVhUQVVUSFNFQ1JFVH0nCiAgLSAnQVVUSF9ESVNBQkxFX1NJR05VUD0ke0FVVEhfRElTQUJMRV9TSUdOVVA6LXRydWV9JwogIC0gJ0hPU1ROQU1FPSR7SE9TVE5BTUU6LTAuMC4wLjB9JwogIC0gJ0xBTkdGVVNFX0lOSVRfT1JHX0lEPSR7TEFOR0ZVU0VfSU5JVF9PUkdfSUQ6LW15LW9yZ30nCiAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfTkFNRT0ke0xBTkdGVVNFX0lOSVRfT1JHX05BTUU6LU15IE9yZ30nCiAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEOi1teS1wcm9qZWN0fScKICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRT0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FOi1NeSBQcm9qZWN0fScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw9JHtMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfTkFNRT0ke1NFUlZJQ0VfVVNFUl9MQU5HRlVTRX0nCiAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9MQU5HRlVTRX0nCnNlcnZpY2VzOgogIGxhbmdmdXNlOgogICAgaW1hZ2U6ICdsYW5nZnVzZS9sYW5nZnVzZTozJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIDA6ICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fTEFOR0ZVU0V9JwogICAgICAxOiAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgMjogJ1NBTFQ9JHtTRVJWSUNFX1BBU1NXT1JEX1NBTFR9JwogICAgICAzOiAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0xBTkdGVVNFfScKICAgICAgNDogJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgNTogJ0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM9JHtMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTOi1mYWxzZX0nCiAgICAgIDY6ICdDTElDS0hPVVNFX01JR1JBVElPTl9VUkw9Y2xpY2tob3VzZTovL2NsaWNraG91c2U6OTAwMCcKICAgICAgNzogJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgICAgIDg6ICdDTElDS0hPVVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfQ0xJQ0tIT1VTRX0nCiAgICAgIDk6ICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgMTA6IENMSUNLSE9VU0VfQ0xVU1RFUl9FTkFCTEVEPWZhbHNlCiAgICAgIDExOiAnTEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I9JHtMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQjotZmFsc2V9JwogICAgICAxMjogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIDEzOiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT046LWF1dG99JwogICAgICAxNDogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAxNTogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIDE2OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0VORFBPSU5UfScKICAgICAgMTc6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMTg6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWDotZXZlbnRzL30nCiAgICAgIDE5OiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgMjA6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIDIxOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIDIyOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgMjM6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlR9JwogICAgICAyNDogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAyNTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYOi1tZWRpYS99JwogICAgICAyNjogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgMjc6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAyODogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYOi1leHBvcnRzL30nCiAgICAgIDI5OiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT046LWF1dG99JwogICAgICAzMDogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVH0nCiAgICAgIDMxOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VYVEVSTkFMX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VYVEVSTkFMX0VORFBPSU5UfScKICAgICAgMzI6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEfScKICAgICAgMzM6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAzNDogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAzNTogJ0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUzotMX0nCiAgICAgIDM2OiAnTEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUzotMTAwMH0nCiAgICAgIDM3OiBSRURJU19IT1NUPXJlZGlzCiAgICAgIDM4OiBSRURJU19QT1JUPTYzNzkKICAgICAgMzk6ICdSRURJU19BVVRIPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgIDQwOiAnRU1BSUxfRlJPTV9BRERSRVNTPSR7RU1BSUxfRlJPTV9BRERSRVNTOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIDQxOiAnU01UUF9DT05ORUNUSU9OX1VSTD0ke1NNVFBfQ09OTkVDVElPTl9VUkw6LX0nCiAgICAgIDQyOiAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfTkVYVEFVVEhTRUNSRVR9JwogICAgICA0MzogJ0FVVEhfRElTQUJMRV9TSUdOVVA9JHtBVVRIX0RJU0FCTEVfU0lHTlVQOi10cnVlfScKICAgICAgNDQ6ICdIT1NUTkFNRT0ke0hPU1ROQU1FOi0wLjAuMC4wfScKICAgICAgNDU6ICdMQU5HRlVTRV9JTklUX09SR19JRD0ke0xBTkdGVVNFX0lOSVRfT1JHX0lEOi1teS1vcmd9JwogICAgICA0NjogJ0xBTkdGVVNFX0lOSVRfT1JHX05BTUU9JHtMQU5HRlVTRV9JTklUX09SR19OQU1FOi1NeSBPcmd9JwogICAgICA0NzogJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRD0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRDotbXktcHJvamVjdH0nCiAgICAgIDQ4OiAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRTotTXkgUHJvamVjdH0nCiAgICAgIDQ5OiAnTEFOR0ZVU0VfSU5JVF9VU0VSX0VNQUlMPSR7TEFOR0ZVU0VfSU5JVF9VU0VSX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIDUwOiAnTEFOR0ZVU0VfSU5JVF9VU0VSX05BTUU9JHtTRVJWSUNFX1VTRVJfTEFOR0ZVU0V9JwogICAgICA1MTogJ0xBTkdGVVNFX0lOSVRfVVNFUl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTEFOR0ZVU0V9JwogICAgICBTRVJWSUNFX0ZRRE5fTEFOR0ZVU0VfMzAwMDogJyR7U0VSVklDRV9GUUROX0xBTkdGVVNFXzMwMDB9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL3B1YmxpYy9oZWFsdGgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAzCiAgbGFuZ2Z1c2Utd29ya2VyOgogICAgaW1hZ2U6ICdsYW5nZnVzZS9sYW5nZnVzZS13b3JrZXI6MycKICAgIGVudmlyb25tZW50OgogICAgICAtICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fTEFOR0ZVU0V9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogICAgICAtICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAgICAgLSAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0xBTkdGVVNFfScKICAgICAgLSAnVEVMRU1FVFJZX0VOQUJMRUQ9JHtURUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogICAgICAtICdDTElDS0hPVVNFX01JR1JBVElPTl9VUkw9Y2xpY2tob3VzZTovL2NsaWNraG91c2U6OTAwMCcKICAgICAgLSAnQ0xJQ0tIT1VTRV9VUkw9aHR0cDovL2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogICAgICAtICdMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQj0ke0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9COi1mYWxzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWDotZXZlbnRzL30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TOi0xfScKICAgICAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUzotMTAwMH0nCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19BVVRIPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdTTVRQX0NPTk5FQ1RJT05fVVJMPSR7U01UUF9DT05ORUNUSU9OX1VSTDotfScKICAgICAgLSAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfTkVYVEFVVEhTRUNSRVR9JwogICAgICAtICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0hPU1ROQU1FPSR7SE9TVE5BTUU6LTAuMC4wLjB9JwogICAgICAtICdMQU5HRlVTRV9JTklUX09SR19JRD0ke0xBTkdGVVNFX0lOSVRfT1JHX0lEOi1teS1vcmd9JwogICAgICAtICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEOi1teS1wcm9qZWN0fScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRTotTXkgUHJvamVjdH0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfTkFNRT0ke1NFUlZJQ0VfVVNFUl9MQU5HRlVTRX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTEFOR0ZVU0V9JwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNy1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX3Bvc3RncmVzX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1oIGxvY2FsaG9zdCAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo4JwogICAgY29tbWFuZDoKICAgICAgLSBzaAogICAgICAtICctYycKICAgICAgLSAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgIiRTRVJWSUNFX1BBU1NXT1JEX1JFRElTIicKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAnbGFuZ2Z1c2VfcmVkaXNfZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiAzcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBjbGlja2hvdXNlOgogICAgaW1hZ2U6ICdjbGlja2hvdXNlL2NsaWNraG91c2Utc2VydmVyOmxhdGVzdCcKICAgIHVzZXI6ICcxMDE6MTAxJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX2NsaWNraG91c2VfZGF0YTovdmFyL2xpYi9jbGlja2hvdXNlJwogICAgICAtICdsYW5nZnVzZV9jbGlja2hvdXNlX2xvZ3M6L3Zhci9sb2cvY2xpY2tob3VzZS1zZXJ2ZXInCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLS1uby12ZXJib3NlIC0tdHJpZXM9MSAtLXNwaWRlciBodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZyB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "ai", "qdrant", @@ -1769,7 +1786,7 @@ "observation", "metrics" ], - "logo": "svgs/langfuse.png", + "logo": "svgs/langfuse.svg", "minversion": "0.0.0", "port": "3000" }, @@ -1949,7 +1966,7 @@ "matrix": { "documentation": "https://matrix.org/docs/chat_basics/matrix-for-im/?utm_source=coolify.io", "slogan": "Chat securely with your family, friends, community, or build great apps with Matrix!", - "compose": "c2VydmljZXM6CiAgbWF0cml4OgogICAgaW1hZ2U6ICdtYXRyaXhkb3Rvcmcvc3luYXBzZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUFUUklYXzgwMDgKICAgICAgLSAnU1lOQVBTRV9TRVJWRVJfTkFNRT0ke1NFUlZJQ0VfVVJMX01BVFJJWH0nCiAgICAgIC0gJ1NZTkFQU0VfUkVQT1JUX1NUQVRTPSR7U1lOQVBTRV9SRVBPUlRfU1RBVFM6LW5vfScKICAgICAgLSAnRU5BQkxFX1JFR0lTVFJBVElPTj0ke0VOQUJMRV9SRUdJU1RSQVRJT046LWZhbHNlfScKICAgICAgLSAnUkVDQVBUQ0hBX1BVQkxJQ19LRVk9JHtSRUNBUFRDSEFfUFVCTElDX0tFWX0nCiAgICAgIC0gJ1JFQ0FQVENIQV9QUklWQVRFX0tFWT0ke1JFQ0FQVENIQV9QUklWQVRFX0tFWX0nCiAgICAgIC0gJ19TRVJWRVJfTkFNRT0ke1NFUlZJQ0VfVVJMX01BVFJJWH0nCiAgICAgIC0gJ19BRE1JTl9OQU1FPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnX0FETUlOX1BBU1M9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21hdHJpeC1kYXRhOi9kYXRhJwogICAgZW50cnlwb2ludDoKICAgICAgLSAvYmluL2Jhc2gKICAgICAgLSAnLWMnCiAgICAgIC0gIiEgdGVzdCAtZiAvZGF0YS9ob21lc2VydmVyLnlhbWwgJiYgL3N0YXJ0LnB5IGdlbmVyYXRlXG5cbiMgcmVnaXN0cmF0aW9uX3NoYXJlZF9zZWNyZXRcbmdyZXAgXCJyZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldFwiIC9kYXRhL2hvbWVzZXJ2ZXIueWFtbCBcXFxufCBhd2sgJ3twcmludCAkMn0nID4gLi9yZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldFxuXG4jIG1hY2Fyb29uX3NlY3JldF9rZXlcbmdyZXAgXCJtYWNhcm9vbl9zZWNyZXRfa2V5XCIgL2RhdGEvaG9tZXNlcnZlci55YW1sIFxcXG58IGF3ayAne3ByaW50ICQyfScgPiAuL21hY2Fyb29uX3NlY3JldF9rZXlcblxuIyBmb3JtX3NlY3JldFxuZ3JlcCBcImZvcm1fc2VjcmV0XCIgL2RhdGEvaG9tZXNlcnZlci55YW1sIFxcXG58IGF3ayAne3ByaW50ICQyfScgPiAuL2Zvcm1fc2VjcmV0XG5cbiMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjXG4jICAgICAgICAgICAgICAgICAgICAgICAgI1xuIyBob21lc2VydmVyLnlhbWw6IHN0YXJ0ICNcbiMgICAgICAgICAgICAgICAgICAgICAgICAjXG4jIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjI1xuY2F0IDw8RU9GID4gL2RhdGEvaG9tZXNlcnZlci55YW1sXG5zZXJ2ZXJfbmFtZTogXCIke1NFUlZJQ0VfVVJMX01BVFJJWH1cIlxucGlkX2ZpbGU6IC9kYXRhL2hvbWVzZXJ2ZXIucGlkXG5cbiMgc2VydmVyXG5saXN0ZW5lcnM6XG4gIC0gcG9ydDogODAwOFxuICAgIHRsczogZmFsc2VcbiAgICB0eXBlOiBodHRwXG4gICAgeF9mb3J3YXJkZWQ6IHRydWVcbiAgICByZXNvdXJjZXM6XG4gICAgICAtIG5hbWVzOiBbY2xpZW50LCBmZWRlcmF0aW9uXVxuICAgICAgICBjb21wcmVzczogZmFsc2VcblxuIyBkYXRhYmFzZVxuZGF0YWJhc2U6XG4gIG5hbWU6IHNxbGl0ZTNcbiAgYXJnczpcbiAgICBkYXRhYmFzZTogL2RhdGEvaG9tZXNlcnZlci5kYlxuXG4jIGdlbmVyYWxcbmxvZ19jb25maWc6IFwiL2RhdGEvJHtTRVJWSUNFX1VSTF9NQVRSSVh9LmxvZy5jb25maWdcIlxubWVkaWFfc3RvcmVfcGF0aDogL2RhdGEvbWVkaWFfc3RvcmVcbnJlcG9ydF9zdGF0czogZmFsc2VcblxuIyBzZWNyZXRzXG5yZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldDogJCg8Li9yZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldClcbm1hY2Fyb29uX3NlY3JldF9rZXk6ICQoPC4vbWFjYXJvb25fc2VjcmV0X2tleSlcbmZvcm1fc2VjcmV0OiAkKDwuL2Zvcm1fc2VjcmV0KVxuc2lnbmluZ19rZXlfcGF0aDogXCIvZGF0YS8ke1NFUlZJQ0VfVVJMX01BVFJJWH0uc2lnbmluZy5rZXlcIlxuXG4jcm9vbXNcbmF1dG9fam9pbl9yb29tczpcbiAgLSBcIiNnZW5lcmFsOiR7U0VSVklDRV9VUkxfTUFUUklYfVwiXG5cbiMgZmVkZXJhdGlvblxudHJ1c3RlZF9rZXlfc2VydmVyczpcbiAgLSBzZXJ2ZXJfbmFtZTogXCJtYXRyaXgub3JnXCJcbmF1dG9jcmVhdGVfYXV0b19qb2luX3Jvb21zX2ZlZGVyYXRlZDogZmFsc2VcbmFsbG93X3B1YmxpY19yb29tc19vdmVyX2ZlZGVyYXRpb246IGZhbHNlXG5FT0ZcbiMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjI1xuIyAgICAgICAgICAgICAgICAgICAgICAjXG4jIGhvbWVzZXJ2ZXIueWFtbDogZW5kICNcbiMgICAgICAgICAgICAgICAgICAgICAgI1xuIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjXG5cbnRlc3QgLW4gXCIke0VOQUJMRV9SRUdJU1RSQVRJT059XCIgJiYgISBncmVwIFwiI3JlZ2lzdHJhdGlvblwiIC9kYXRhL2hvbWVzZXJ2ZXIueWFtbCAmPi9kZXYvbnVsbCBcXFxuJiYgZWNobyA+PiAvZGF0YS9ob21lc2VydmVyLnlhbWwgXFxcbiYmIGNhdCA8PEVPRiA+PiAvZGF0YS9ob21lc2VydmVyLnlhbWxcbiNyZWdpc3RyYXRpb25cbmVuYWJsZV9yZWdpc3RyYXRpb246IHRydWUgICMgQWxsb3dzIHVzZXJzIHRvIHJlZ2lzdGVyIG9uIHlvdXIgc2VydmVyLlxuRU9GXG5cbiEgZ3JlcCAke1JFQ0FQVENIQV9QVUJMSUNfS0VZfSAvZGF0YS9ob21lc2VydmVyLnlhbWwgJj4vZGV2L251bGwgXFxcbiYmIGVjaG8gPj4gL2RhdGEvaG9tZXNlcnZlci55YW1sIFxcXG4mJiBjYXQgPDxFT0YgPj4gL2RhdGEvaG9tZXNlcnZlci55YW1sXG4jIHJlQ0FQVENIQSBzZXR0aW5nc1xuZW5hYmxlX3JlZ2lzdHJhdGlvbl9jYXB0Y2hhOiB0cnVlICAjIEVuYWJsZXMgQ0FQVENIQSBmb3IgcmVnaXN0cmF0aW9ucy5cbnJlY2FwdGNoYV9wdWJsaWNfa2V5OiBcIiR7UkVDQVBUQ0hBX1BVQkxJQ19LRVl9XCJcbnJlY2FwdGNoYV9wcml2YXRlX2tleTogXCIke1JFQ0FQVENIQV9QUklWQVRFX0tFWX1cIlxucmVjYXB0Y2hhX3NpdGV2ZXJpZnlfYXBpOiBcImh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vcmVjYXB0Y2hhL2FwaS9zaXRldmVyaWZ5XCJcbkVPRlxuXG5yZWdpc3Rlcl9hZG1pbigpe1xuICB3aGlsZSAhIGN1cmwgLUkgbG9jYWxob3N0OjgwMDggJj4vZGV2L251bGw7IGRvXG4gICAgc2xlZXAgMVxuICBkb25lXG4gIHJlZ2lzdGVyX25ld19tYXRyaXhfdXNlciBcXFxuICAgIC1hIFxcXG4gICAgLXUgJHtTRVJWSUNFX1VTRVJfQURNSU59IFxcXG4gICAgLXAgJHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfSBcXFxuICAgIC1jIC9kYXRhL2hvbWVzZXJ2ZXIueWFtbCBcXFxuICAgIGh0dHA6Ly9sb2NhbGhvc3Q6ODAwOCAmPi9kZXYvbnVsbFxufVxucmVnaXN0ZXJfYWRtaW4gJlxuXG4vc3RhcnQucHlcbiIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLUknCiAgICAgICAgLSAnbG9jYWxob3N0OjgwMDgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAzcwogICAgICByZXRyaWVzOiA1Cg==", + "compose": "c2VydmljZXM6CiAgbWF0cml4OgogICAgaW1hZ2U6ICdtYXRyaXhkb3Rvcmcvc3luYXBzZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUFUUklYXzgwMDgKICAgICAgLSAnU1lOQVBTRV9TRVJWRVJfTkFNRT0ke1NFUlZJQ0VfVVJMX01BVFJJWH0nCiAgICAgIC0gJ1NZTkFQU0VfUkVQT1JUX1NUQVRTPSR7U1lOQVBTRV9SRVBPUlRfU1RBVFM6LW5vfScKICAgICAgLSAnRU5BQkxFX1JFR0lTVFJBVElPTj0ke0VOQUJMRV9SRUdJU1RSQVRJT046LWZhbHNlfScKICAgICAgLSAnUkVDQVBUQ0hBX1BVQkxJQ19LRVk9JHtSRUNBUFRDSEFfUFVCTElDX0tFWX0nCiAgICAgIC0gJ1JFQ0FQVENIQV9QUklWQVRFX0tFWT0ke1JFQ0FQVENIQV9QUklWQVRFX0tFWX0nCiAgICAgIC0gJ19TRVJWRVJfTkFNRT0ke1NFUlZJQ0VfVVJMX01BVFJJWH0nCiAgICAgIC0gJ19BRE1JTl9OQU1FPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnX0FETUlOX1BBU1M9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21hdHJpeC1kYXRhOi9kYXRhJwogICAgZW50cnlwb2ludDoKICAgICAgLSAvYmluL2Jhc2gKICAgICAgLSAnLWMnCiAgICAgIC0gIiEgdGVzdCAtZiAvZGF0YS9ob21lc2VydmVyLnlhbWwgJiYgL3N0YXJ0LnB5IGdlbmVyYXRlXG5cbiMgcmVnaXN0cmF0aW9uX3NoYXJlZF9zZWNyZXRcbmdyZXAgXCJyZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldFwiIC9kYXRhL2hvbWVzZXJ2ZXIueWFtbCBcXFxufCBhd2sgJ3twcmludCAkMn0nID4gLi9yZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldFxuXG4jIG1hY2Fyb29uX3NlY3JldF9rZXlcbmdyZXAgXCJtYWNhcm9vbl9zZWNyZXRfa2V5XCIgL2RhdGEvaG9tZXNlcnZlci55YW1sIFxcXG58IGF3ayAne3ByaW50ICQyfScgPiAuL21hY2Fyb29uX3NlY3JldF9rZXlcblxuIyBmb3JtX3NlY3JldFxuZ3JlcCBcImZvcm1fc2VjcmV0XCIgL2RhdGEvaG9tZXNlcnZlci55YW1sIFxcXG58IGF3ayAne3ByaW50ICQyfScgPiAuL2Zvcm1fc2VjcmV0XG5cbiMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjXG4jICAgICAgICAgICAgICAgICAgICAgICAgI1xuIyBob21lc2VydmVyLnlhbWw6IHN0YXJ0ICNcbiMgICAgICAgICAgICAgICAgICAgICAgICAjXG4jIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjI1xuY2F0IDw8RU9GID4gL2RhdGEvaG9tZXNlcnZlci55YW1sXG5zZXJ2ZXJfbmFtZTogXCIke1NFUlZJQ0VfVVJMX01BVFJJWH1cIlxucGlkX2ZpbGU6IC9kYXRhL2hvbWVzZXJ2ZXIucGlkXG5cbiMgc2VydmVyXG5saXN0ZW5lcnM6XG4gIC0gcG9ydDogODAwOFxuICAgIHRsczogZmFsc2VcbiAgICB0eXBlOiBodHRwXG4gICAgeF9mb3J3YXJkZWQ6IHRydWVcbiAgICByZXNvdXJjZXM6XG4gICAgICAtIG5hbWVzOiBbY2xpZW50LCBmZWRlcmF0aW9uXVxuICAgICAgICBjb21wcmVzczogZmFsc2VcblxuIyBkYXRhYmFzZVxuZGF0YWJhc2U6XG4gIG5hbWU6IHNxbGl0ZTNcbiAgYXJnczpcbiAgICBkYXRhYmFzZTogL2RhdGEvaG9tZXNlcnZlci5kYlxuXG4jIGdlbmVyYWxcbmxvZ19jb25maWc6IFwiL2RhdGEvJHtTRVJWSUNFX1VSTF9NQVRSSVh9LmxvZy5jb25maWdcIlxubWVkaWFfc3RvcmVfcGF0aDogL2RhdGEvbWVkaWFfc3RvcmVcbnJlcG9ydF9zdGF0czogZmFsc2VcblxuIyBzZWNyZXRzXG5yZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldDogJCg8Li9yZWdpc3RyYXRpb25fc2hhcmVkX3NlY3JldClcbm1hY2Fyb29uX3NlY3JldF9rZXk6ICQoPC4vbWFjYXJvb25fc2VjcmV0X2tleSlcbmZvcm1fc2VjcmV0OiAkKDwuL2Zvcm1fc2VjcmV0KVxuc2lnbmluZ19rZXlfcGF0aDogXCIvZGF0YS8ke1NFUlZJQ0VfVVJMX01BVFJJWH0uc2lnbmluZy5rZXlcIlxuXG4jcm9vbXNcbmF1dG9fam9pbl9yb29tczpcbiAgLSBcIiNnZW5lcmFsOiR7U0VSVklDRV9VUkxfTUFUUklYfVwiXG5cbiMgZmVkZXJhdGlvblxudHJ1c3RlZF9rZXlfc2VydmVyczpcbiAgLSBzZXJ2ZXJfbmFtZTogXCJtYXRyaXgub3JnXCJcbmF1dG9jcmVhdGVfYXV0b19qb2luX3Jvb21zX2ZlZGVyYXRlZDogZmFsc2VcbmFsbG93X3B1YmxpY19yb29tc19vdmVyX2ZlZGVyYXRpb246IGZhbHNlXG5FT0ZcbiMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjI1xuIyAgICAgICAgICAgICAgICAgICAgICAjXG4jIGhvbWVzZXJ2ZXIueWFtbDogZW5kICNcbiMgICAgICAgICAgICAgICAgICAgICAgI1xuIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjXG5cblsgXCIke0VOQUJMRV9SRUdJU1RSQVRJT059XCIgPSBcInRydWVcIiBdICYmICEgZ3JlcCBcIiNyZWdpc3RyYXRpb25cIiAvZGF0YS9ob21lc2VydmVyLnlhbWwgJj4vZGV2L251bGwgXFxcbiYmIGVjaG8gPj4gL2RhdGEvaG9tZXNlcnZlci55YW1sIFxcXG4mJiBjYXQgPDxFT0YgPj4gL2RhdGEvaG9tZXNlcnZlci55YW1sXG4jcmVnaXN0cmF0aW9uXG5lbmFibGVfcmVnaXN0cmF0aW9uOiB0cnVlICAjIEFsbG93cyB1c2VycyB0byByZWdpc3RlciBvbiB5b3VyIHNlcnZlci5cbkVPRlxuXG5bIC1uIFwiJHtSRUNBUFRDSEFfUFVCTElDX0tFWX1cIiBdICYmICEgZ3JlcCBcIiR7UkVDQVBUQ0hBX1BVQkxJQ19LRVl9XCIgL2RhdGEvaG9tZXNlcnZlci55YW1sICY+L2Rldi9udWxsIFxcXG4mJiBlY2hvID4+IC9kYXRhL2hvbWVzZXJ2ZXIueWFtbCBcXFxuJiYgY2F0IDw8RU9GID4+IC9kYXRhL2hvbWVzZXJ2ZXIueWFtbFxuIyByZUNBUFRDSEEgc2V0dGluZ3NcbmVuYWJsZV9yZWdpc3RyYXRpb25fY2FwdGNoYTogdHJ1ZSAgIyBFbmFibGVzIENBUFRDSEEgZm9yIHJlZ2lzdHJhdGlvbnMuXG5yZWNhcHRjaGFfcHVibGljX2tleTogXCIke1JFQ0FQVENIQV9QVUJMSUNfS0VZfVwiXG5yZWNhcHRjaGFfcHJpdmF0ZV9rZXk6IFwiJHtSRUNBUFRDSEFfUFJJVkFURV9LRVl9XCJcbnJlY2FwdGNoYV9zaXRldmVyaWZ5X2FwaTogXCJodHRwczovL3d3dy5nb29nbGUuY29tL3JlY2FwdGNoYS9hcGkvc2l0ZXZlcmlmeVwiXG5FT0ZcblxucmVnaXN0ZXJfYWRtaW4oKXtcbiAgd2hpbGUgISBjdXJsIC1JIGxvY2FsaG9zdDo4MDA4ICY+L2Rldi9udWxsOyBkb1xuICAgIHNsZWVwIDFcbiAgZG9uZVxuICByZWdpc3Rlcl9uZXdfbWF0cml4X3VzZXIgXFxcbiAgICAtYSBcXFxuICAgIC11ICR7U0VSVklDRV9VU0VSX0FETUlOfSBcXFxuICAgIC1wICR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0gXFxcbiAgICAtYyAvZGF0YS9ob21lc2VydmVyLnlhbWwgXFxcbiAgICBodHRwOi8vbG9jYWxob3N0OjgwMDggJj4vZGV2L251bGxcbn1cbnJlZ2lzdGVyX2FkbWluICZcblxuL3N0YXJ0LnB5XG4iCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1JJwogICAgICAgIC0gJ2xvY2FsaG9zdDo4MDA4JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogM3MKICAgICAgcmV0cmllczogNQo=", "tags": [ "chat", "slack", @@ -2717,6 +2734,21 @@ "logo": "svgs/phpmyadmin.svg", "minversion": "0.0.0" }, + "pi-hole": { + "documentation": "https://pi-hole.net/?utm_source=coolify.io", + "slogan": "Network-wide Ad Blocking", + "compose": "c2VydmljZXM6CiAgcGlob2xlOgogICAgaW1hZ2U6ICdwaWhvbGUvcGlob2xlOmxhdGVzdCcKICAgIHBvcnRzOgogICAgICAtICc1Mzo1My90Y3AnCiAgICAgIC0gJzUzOjUzL3VkcCcKICAgICAgLSAnNjc6NjcvdWRwJwogICAgICAtICcxMjM6MTIzL3VkcCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QSUhPTEVfODAKICAgICAgLSAnVFo9JHtUWjotRXVyb3BlL0xvbmRvbn0nCiAgICAgIC0gJ0ZUTENPTkZfd2Vic2VydmVyX2FwaV9wYXNzd29yZD0ke1NFUlZJQ0VfUEFTU1dPUkRfUElIT0xFfScKICAgICAgLSAnRlRMQ09ORl9kbnNfbGlzdGVuaW5nTW9kZT0ke0ZUTENPTkZfZG5zX2xpc3RlbmluZ01vZGU6LWFsbH0nCiAgICB2b2x1bWVzOgogICAgICAtICdwaWhvbGUtZGF0YTovZXRjL3BpaG9sZScKICAgIGNhcF9hZGQ6CiAgICAgIC0gTkVUX0FETUlOCiAgICAgIC0gU1lTX1RJTUUK", + "tags": [ + "ad-block", + "dns", + "sinkhole", + "ntp", + "dhcp" + ], + "logo": "svgs/pihole.svg", + "minversion": "0.0.0", + "port": "80" + }, "pingvinshare-with-clamav": { "documentation": "https://github.com/stonith404/pingvin-share?utm_source=coolify.io", "slogan": "A self-hosted file sharing platform that combines lightness and beauty, perfect for seamless and efficient file sharing.", @@ -2842,7 +2874,7 @@ "postiz": { "documentation": "https://docs.postiz.com?utm_source=coolify.io", "slogan": "Open source social media scheduling tool.", - "compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOnYxLjYwLjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUE9TVElaXzUwMDAKICAgICAgLSAnTUFJTl9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafScKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX1BPU1RJWn0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0JBQ0tFTkRfVVJMPSR7U0VSVklDRV9GUUROX1BPU1RJWn0vYXBpJwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1RTRUNSRVR9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovL3Bvc3RncmVzOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotcG9zdGl6LWRifScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdDoke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9QHJlZGlzOjYzNzknCiAgICAgIC0gJ0JBQ0tFTkRfSU5URVJOQUxfVVJMPWh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICAgICAgLSAnQ0xPVURGTEFSRV9BQ0NPVU5UX0lEPSR7Q0xPVURGTEFSRV9BQ0NPVU5UX0lEfScKICAgICAgLSAnQ0xPVURGTEFSRV9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnQ0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWT0ke0NMT1VERkxBUkVfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX0JVQ0tFVE5BTUU9JHtDTE9VREZMQVJFX0JVQ0tFVE5BTUV9JwogICAgICAtICdDTE9VREZMQVJFX0JVQ0tFVF9VUkw9JHtDTE9VREZMQVJFX0JVQ0tFVF9VUkx9JwogICAgICAtICdDTE9VREZMQVJFX1JFR0lPTj0ke0NMT1VERkxBUkVfUkVHSU9OfScKICAgICAgLSAnU1RPUkFHRV9QUk9WSURFUj0ke1NUT1JBR0VfUFJPVklERVI6LWxvY2FsfScKICAgICAgLSAnVVBMT0FEX0RJUkVDVE9SWT0ke1VQTE9BRF9ESVJFQ1RPUlk6LS91cGxvYWRzfScKICAgICAgLSAnTkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9ESVJFQ1RPUlk6LS91cGxvYWRzfScKICAgICAgLSAnTkVYVF9QVUJMSUNfVVBMT0FEX1NUQVRJQ19ESVJFQ1RPUlk9JHtORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWX0nCiAgICAgIC0gJ1JFU0VORF9BUElfS0VZPSR7UkVTRU5EX0FQSV9LRVl9JwogICAgICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1N9JwogICAgICAtICdFTUFJTF9GUk9NX05BTUU9JHtFTUFJTF9GUk9NX05BTUV9JwogICAgICAtICdFTUFJTF9QUk9WSURFUj0ke0VNQUlMX1BST1ZJREVSfScKICAgICAgLSAnWF9BUElfS0VZPSR7U0VSVklDRV9YX0FQSX0nCiAgICAgIC0gJ1hfQVBJX1NFQ1JFVD0ke1NFUlZJQ0VfWF9TRUNSRVR9JwogICAgICAtICdMSU5LRURJTl9DTElFTlRfSUQ9JHtTRVJWSUNFX0xJTktFRElOX0lEfScKICAgICAgLSAnTElOS0VESU5fQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfTElOS0VESU5fU0VDUkVUfScKICAgICAgLSAnUkVERElUX0NMSUVOVF9JRD0ke1NFUlZJQ0VfUkVERElUX0FQSX0nCiAgICAgIC0gJ1JFRERJVF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9SRURESVRfU0VDUkVUfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9JRD0ke1NFUlZJQ0VfR0lUSFVCX0lEfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0dJVEhVQl9TRUNSRVR9JwogICAgICAtICdUSFJFQURTX0FQUF9JRD0ke1NFUlZJQ0VfVEhSRUFEU19JRH0nCiAgICAgIC0gJ1RIUkVBRFNfQVBQX1NFQ1JFVD0ke1NFUlZJQ0VfVEhSRUFEU19TRUNSRVR9JwogICAgICAtICdGQUNFQk9PS19BUFBfSUQ9JHtTRVJWSUNFX0ZBQ0VCT09LX0lEfScKICAgICAgLSAnRkFDRUJPT0tfQVBQX1NFQ1JFVD0ke1NFUlZJQ0VfRkFDRUJPT0tfU0VDUkVUfScKICAgICAgLSAnWU9VVFVCRV9DTElFTlRfSUQ9JHtTRVJWSUNFX1lPVVRVQkVfSUR9JwogICAgICAtICdZT1VUVUJFX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1lPVVRVQkVfU0VDUkVUfScKICAgICAgLSAnVElLVE9LX0NMSUVOVF9JRD0ke1NFUlZJQ0VfVElLVE9LX0lEfScKICAgICAgLSAnVElLVE9LX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1RJS1RPS19TRUNSRVR9JwogICAgICAtICdQSU5URVJFU1RfQ0xJRU5UX0lEPSR7U0VSVklDRV9QSU5URVJFU1RfSUR9JwogICAgICAtICdQSU5URVJFU1RfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfUElOVEVSRVNUX1NFQ1JFVH0nCiAgICAgIC0gJ0RSSUJCQkxFX0NMSUVOVF9JRD0ke1NFUlZJQ0VfRFJJQkJMRV9JRH0nCiAgICAgIC0gJ0RSSUJCQkxFX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0RSSUJCTEVfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfSUQ9JHtTRVJWSUNFX0RJU0NPUkRfSUR9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0RJU0NPUkRfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9CT1RfVE9LRU5fSUQ9JHtTRVJWSUNFX0RJU0NPUkRfVE9LRU59JwogICAgICAtICdTTEFDS19JRD0ke1NFUlZJQ0VfU0xBQ0tfSUR9JwogICAgICAtICdTTEFDS19TRUNSRVQ9JHtTRVJWSUNFX1NMQUNLX1NFQ1JFVH0nCiAgICAgIC0gJ1NMQUNLX1NJR05JTkdfU0VDUkVUPSR7U0xBQ0tfU0lHTklOR19TRUNSRVR9JwogICAgICAtICdNQVNUT0RPTl9DTElFTlRfSUQ9JHtTRVJWSUNFX01BU1RPRE9OX0lEfScKICAgICAgLSAnTUFTVE9ET05fQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfTUFTVE9ET05fU0VDUkVUfScKICAgICAgLSAnQkVFSElJVkVfQVBJX0tFWT0ke1NFUlZJQ0VfQkVFSElJVkVfS0VZfScKICAgICAgLSAnQkVFSElJVkVfUFVCTElDQVRJT05fSUQ9JHtTRVJWSUNFX0JFRUhJSVZFX1BVQklEfScKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtTRVJWSUNFX09QRU5BSV9LRVl9JwogICAgICAtICdORVhUX1BVQkxJQ19ESVNDT1JEX1NVUFBPUlQ9JHtORVhUX1BVQkxJQ19ESVNDT1JEX1NVUFBPUlR9JwogICAgICAtICdORVhUX1BVQkxJQ19QT0xPVE5PPSR7TkVYVF9QVUJMSUNfUE9MT1ROT30nCiAgICAgIC0gSVNfR0VORVJBTD10cnVlCiAgICAgIC0gJ05YX0FERF9QTFVHSU5TPSR7TlhfQUREX1BMVUdJTlM6LWZhbHNlfScKICAgICAgLSAnRkVFX0FNT1VOVD0ke0ZFRV9BTU9VTlQ6LTAuMDV9JwogICAgICAtICdTVFJJUEVfUFVCTElTSEFCTEVfS0VZPSR7U1RSSVBFX1BVQkxJU0hBQkxFX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TRUNSRVRfS0VZPSR7U1RSSVBFX1NFQ1JFVF9LRVl9JwogICAgICAtICdTVFJJUEVfU0lHTklOR19LRVk9JHtTVFJJUEVfU0lHTklOR19LRVl9JwogICAgICAtICdTVFJJUEVfU0lHTklOR19LRVlfQ09OTkVDVD0ke1NUUklQRV9TSUdOSU5HX0tFWV9DT05ORUNUfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rpel9jb25maWc6L2NvbmZpZy8nCiAgICAgIC0gJ3Bvc3Rpel91cGxvYWRzOi91cGxvYWRzLycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjUwMDAvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE0LjUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0aXpfcG9zdGdyZXNxbF9kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9cG9zdGdyZXMKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LXBvc3Rpei1kYn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREI6LXBvc3Rpei1kYn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1JFRElTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rpel9yZWRpc19kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gJy1hJwogICAgICAgIC0gJyR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAK", + "compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOnYxLjYwLjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUE9TVElaXzUwMDAKICAgICAgLSAnTUFJTl9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafScKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX1BPU1RJWn0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0JBQ0tFTkRfVVJMPSR7U0VSVklDRV9GUUROX1BPU1RJWn0vYXBpJwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1RTRUNSRVR9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovL3Bvc3RncmVzOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotcG9zdGl6LWRifScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdDoke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9QHJlZGlzOjYzNzknCiAgICAgIC0gJ0JBQ0tFTkRfSU5URVJOQUxfVVJMPWh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICAgICAgLSAnQ0xPVURGTEFSRV9BQ0NPVU5UX0lEPSR7Q0xPVURGTEFSRV9BQ0NPVU5UX0lEfScKICAgICAgLSAnQ0xPVURGTEFSRV9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnQ0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWT0ke0NMT1VERkxBUkVfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX0JVQ0tFVE5BTUU9JHtDTE9VREZMQVJFX0JVQ0tFVE5BTUV9JwogICAgICAtICdDTE9VREZMQVJFX0JVQ0tFVF9VUkw9JHtDTE9VREZMQVJFX0JVQ0tFVF9VUkx9JwogICAgICAtICdDTE9VREZMQVJFX1JFR0lPTj0ke0NMT1VERkxBUkVfUkVHSU9OfScKICAgICAgLSAnU1RPUkFHRV9QUk9WSURFUj0ke1NUT1JBR0VfUFJPVklERVI6LWxvY2FsfScKICAgICAgLSAnVVBMT0FEX0RJUkVDVE9SWT0ke1VQTE9BRF9ESVJFQ1RPUlk6LS91cGxvYWRzfScKICAgICAgLSAnTkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9ESVJFQ1RPUlk6LS91cGxvYWRzfScKICAgICAgLSAnTkVYVF9QVUJMSUNfVVBMT0FEX1NUQVRJQ19ESVJFQ1RPUlk9JHtORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWX0nCiAgICAgIC0gJ1JFU0VORF9BUElfS0VZPSR7UkVTRU5EX0FQSV9LRVl9JwogICAgICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1N9JwogICAgICAtICdFTUFJTF9GUk9NX05BTUU9JHtFTUFJTF9GUk9NX05BTUV9JwogICAgICAtICdFTUFJTF9QUk9WSURFUj0ke0VNQUlMX1BST1ZJREVSfScKICAgICAgLSAnWF9BUElfS0VZPSR7U0VSVklDRV9YX0FQSX0nCiAgICAgIC0gJ1hfQVBJX1NFQ1JFVD0ke1NFUlZJQ0VfWF9TRUNSRVR9JwogICAgICAtICdMSU5LRURJTl9DTElFTlRfSUQ9JHtTRVJWSUNFX0xJTktFRElOX0lEfScKICAgICAgLSAnTElOS0VESU5fQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfTElOS0VESU5fU0VDUkVUfScKICAgICAgLSAnUkVERElUX0NMSUVOVF9JRD0ke1NFUlZJQ0VfUkVERElUX0FQSX0nCiAgICAgIC0gJ1JFRERJVF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9SRURESVRfU0VDUkVUfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9JRD0ke1NFUlZJQ0VfR0lUSFVCX0lEfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0dJVEhVQl9TRUNSRVR9JwogICAgICAtICdUSFJFQURTX0FQUF9JRD0ke1NFUlZJQ0VfVEhSRUFEU19JRH0nCiAgICAgIC0gJ1RIUkVBRFNfQVBQX1NFQ1JFVD0ke1NFUlZJQ0VfVEhSRUFEU19TRUNSRVR9JwogICAgICAtICdGQUNFQk9PS19BUFBfSUQ9JHtTRVJWSUNFX0ZBQ0VCT09LX0lEfScKICAgICAgLSAnRkFDRUJPT0tfQVBQX1NFQ1JFVD0ke1NFUlZJQ0VfRkFDRUJPT0tfU0VDUkVUfScKICAgICAgLSAnWU9VVFVCRV9DTElFTlRfSUQ9JHtTRVJWSUNFX1lPVVRVQkVfSUR9JwogICAgICAtICdZT1VUVUJFX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1lPVVRVQkVfU0VDUkVUfScKICAgICAgLSAnVElLVE9LX0NMSUVOVF9JRD0ke1NFUlZJQ0VfVElLVE9LX0lEfScKICAgICAgLSAnVElLVE9LX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1RJS1RPS19TRUNSRVR9JwogICAgICAtICdQSU5URVJFU1RfQ0xJRU5UX0lEPSR7U0VSVklDRV9QSU5URVJFU1RfSUR9JwogICAgICAtICdQSU5URVJFU1RfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfUElOVEVSRVNUX1NFQ1JFVH0nCiAgICAgIC0gJ0RSSUJCQkxFX0NMSUVOVF9JRD0ke1NFUlZJQ0VfRFJJQkJMRV9JRH0nCiAgICAgIC0gJ0RSSUJCQkxFX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0RSSUJCTEVfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfSUQ9JHtTRVJWSUNFX0RJU0NPUkRfSUR9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0RJU0NPUkRfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9CT1RfVE9LRU5fSUQ9JHtTRVJWSUNFX0RJU0NPUkRfVE9LRU59JwogICAgICAtICdTTEFDS19JRD0ke1NFUlZJQ0VfU0xBQ0tfSUR9JwogICAgICAtICdTTEFDS19TRUNSRVQ9JHtTRVJWSUNFX1NMQUNLX1NFQ1JFVH0nCiAgICAgIC0gJ1NMQUNLX1NJR05JTkdfU0VDUkVUPSR7U0xBQ0tfU0lHTklOR19TRUNSRVR9JwogICAgICAtICdNQVNUT0RPTl9DTElFTlRfSUQ9JHtTRVJWSUNFX01BU1RPRE9OX0lEfScKICAgICAgLSAnTUFTVE9ET05fQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfTUFTVE9ET05fU0VDUkVUfScKICAgICAgLSAnQkVFSElJVkVfQVBJX0tFWT0ke1NFUlZJQ0VfQkVFSElJVkVfS0VZfScKICAgICAgLSAnQkVFSElJVkVfUFVCTElDQVRJT05fSUQ9JHtTRVJWSUNFX0JFRUhJSVZFX1BVQklEfScKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtTRVJWSUNFX09QRU5BSV9LRVl9JwogICAgICAtICdORVhUX1BVQkxJQ19ESVNDT1JEX1NVUFBPUlQ9JHtORVhUX1BVQkxJQ19ESVNDT1JEX1NVUFBPUlR9JwogICAgICAtICdORVhUX1BVQkxJQ19QT0xPVE5PPSR7TkVYVF9QVUJMSUNfUE9MT1ROT30nCiAgICAgIC0gSVNfR0VORVJBTD10cnVlCiAgICAgIC0gJ05YX0FERF9QTFVHSU5TPSR7TlhfQUREX1BMVUdJTlM6LWZhbHNlfScKICAgICAgLSAnTk9UX1NFQ1VSRUQ9JHtOT1RfU0VDVVJFRDotZmFsc2V9JwogICAgICAtICdGRUVfQU1PVU5UPSR7RkVFX0FNT1VOVDotMC4wNX0nCiAgICAgIC0gJ1NUUklQRV9QVUJMSVNIQUJMRV9LRVk9JHtTVFJJUEVfUFVCTElTSEFCTEVfS0VZfScKICAgICAgLSAnU1RSSVBFX1NFQ1JFVF9LRVk9JHtTVFJJUEVfU0VDUkVUX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWT0ke1NUUklQRV9TSUdOSU5HX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWV9DT05ORUNUPSR7U1RSSVBFX1NJR05JTkdfS0VZX0NPTk5FQ1R9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGl6X2NvbmZpZzovY29uZmlnLycKICAgICAgLSAncG9zdGl6X3VwbG9hZHM6L3VwbG9hZHMvJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTAwMC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTQuNScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rpel9wb3N0Z3Jlc3FsX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj1wb3N0Z3JlcwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotcG9zdGl6LWRifScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQjotcG9zdGl6LWRifScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1yZXF1aXJlcGFzcyAke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGl6X3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyMAo=", "tags": [ "post everywhere", "social media", @@ -3083,6 +3115,19 @@ "minversion": "0.0.0", "port": "8080" }, + "sequin": { + "documentation": "https://sequinstream.com/docs/?utm_source=coolify.io", + "slogan": "The fastest Postgres change data capture", + "compose": "c2VydmljZXM6CiAgc2VxdWluOgogICAgaW1hZ2U6ICdzZXF1aW4vc2VxdWluOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TRVFVSU5fNzM3NgogICAgICAtICdTRVJWRVJfSE9TVD0ke1NFUlZJQ0VfVVJMX1NFUVVJTn0nCiAgICAgIC0gUEdfSE9TVE5BTUU9cG9zdGdyZXMKICAgICAgLSAnUEdfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotc2VxdWluLWRifScKICAgICAgLSBQR19QT1JUPTU0MzIKICAgICAgLSAnUEdfVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQR19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtIFBHX1BPT0xfU0laRT0yMAogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX1JFQUxCQVNFNjRfNjRfU0VDUkVUS0VZfScKICAgICAgLSAnVkFVTFRfS0VZPSR7U0VSVklDRV9SRUFMQkFTRTY0X1ZBVUxUS0VZfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OScKICAgICAgLSBDT05GSUdfRklMRV9QQVRIPS9jb25maWcvcGxheWdyb3VuZC55bWwKICAgICAgLSAnRkVBVFVSRV9BQ0NPVU5UX1NFTEZfU0lHTlVQPSR7RkVBVFVSRV9BQ0NPVU5UX1NFTEZfU0lHTlVQOi1mYWxzZX0nCiAgICAgIC0gJ1NFUVVJTl9URUxFTUVUUllfRElTQUJMRUQ9JHtTRVFVSU5fVEVMRU1FVFJZX0RJU0FCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ0NSQVNIX1JFUE9SVElOR19ESVNBQkxFRD0ke0NSQVNIX1JFUE9SVElOR19ESVNBQkxFRDotZmFsc2V9JwogICAgZGVwZW5kc19vbjoKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo3Mzc2L2hlYWx0aCcKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotc2VxdWluLWRifScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICBjb21tYW5kOgogICAgICAtIHBvc3RncmVzCiAgICAgIC0gJy1jJwogICAgICAtIHdhbF9sZXZlbD1sb2dpY2FsCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9IC1kIHNlcXVpbicKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAycwogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogMnMKICAgICAgc3RhcnRfaW50ZXJ2YWw6IDFzCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc19kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6NycKICAgIGNvbW1hbmQ6CiAgICAgIC0gcmVkaXMtc2VydmVyCiAgICAgIC0gJy0tcG9ydCcKICAgICAgLSAnNjM3OScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXNfZGF0YTovZGF0YScK", + "tags": [ + "postgres", + "sync", + "data" + ], + "logo": "svgs/sequin.svg", + "minversion": "0.0.0", + "port": "7376" + }, "shlink": { "documentation": "https://shlink.io/?utm_source=coolify.io", "slogan": "The definitive self-hosted URL shortener", From c551be9be8b3bb6daac24543de7a5398157bd14c Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:11:29 +0200 Subject: [PATCH 0114/1717] chore(core): remove unused argument --- app/Actions/Database/StopDatabase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 6fcdedeeb..5c881e743 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -18,7 +18,7 @@ class StopDatabase { use AsAction; - public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true) + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $dockerCleanup = true) { try { $server = $database->destination->server; From daa96c27a704de6b4628ddacee8974ceb05c96c5 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:12:04 +0200 Subject: [PATCH 0115/1717] chore(deletion): rename isDeleteOperation to deleteConnectedNetworks --- app/Actions/Service/StopService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 190b8885d..3f4e96479 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -14,7 +14,7 @@ class StopService public string $jobQueue = 'high'; - public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true) + public function handle(Service $service, bool $deleteConnectedNetworks = false, bool $dockerCleanup = true) { try { $server = $service->destination->server; @@ -36,7 +36,7 @@ public function handle(Service $service, bool $isDeleteOperation = false, bool $ $this->stopContainersInParallel($containersToStop, $server); } - if ($isDeleteOperation) { + if ($deleteConnectedNetworks) { $service->deleteConnectedNetworks(); } if ($dockerCleanup) { From 279edf696ce90b90497933526a1e775b95cceafc Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:12:56 +0200 Subject: [PATCH 0116/1717] chore(docker): remove unused arguments on StopService --- app/Livewire/Project/CloneMe.php | 4 ++-- app/Livewire/Project/Shared/ResourceOperations.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index a7c44577c..a5d80a11a 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -454,7 +454,7 @@ public function clone(string $type) if ($this->cloneVolumeData) { try { - StopService::dispatch($application, false, false); + StopService::dispatch($application); $sourceVolume = $volume->name; $targetVolume = $newPersistentVolume->name; $sourceServer = $application->service->destination->server; @@ -508,7 +508,7 @@ public function clone(string $type) if ($this->cloneVolumeData) { try { - StopService::dispatch($database->service, false, false); + StopService::dispatch($database->service); $sourceVolume = $volume->name; $targetVolume = $newPersistentVolume->name; $sourceServer = $database->service->destination->server; diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index fb19acb55..c8916bf19 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -412,7 +412,7 @@ public function cloneTo($destination_id) if ($this->cloneVolumeData) { try { - StopService::dispatch($application, false, false); + StopService::dispatch($application); $sourceVolume = $volume->name; $targetVolume = $newPersistentVolume->name; $sourceServer = $application->service->destination->server; @@ -454,7 +454,7 @@ public function cloneTo($destination_id) if ($this->cloneVolumeData) { try { - StopService::dispatch($database->service, false, false); + StopService::dispatch($database->service); $sourceVolume = $volume->name; $targetVolume = $newPersistentVolume->name; $sourceServer = $database->service->destination->server; From 36961d8ae840e9e4632bf9fc0aee5b1d10fd1494 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:14:19 +0200 Subject: [PATCH 0117/1717] fix(docker): cleanup always running on deletion - docker cleanup was always running on deletion instead of using the settings set in the deletion modal --- app/Jobs/DeleteResourceJob.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index a725df52f..b9fbebcc9 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -52,7 +52,7 @@ public function handle() switch ($this->resource->type()) { case 'application': - StopApplication::run($this->resource, previewDeployments: true); + StopApplication::run($this->resource, previewDeployments: true, dockerCleanup: $this->dockerCleanup); break; case 'standalone-postgresql': case 'standalone-redis': @@ -62,10 +62,10 @@ public function handle() case 'standalone-keydb': case 'standalone-dragonfly': case 'standalone-clickhouse': - StopDatabase::run($this->resource, true); + StopDatabase::run($this->resource, dockerCleanup: $this->dockerCleanup); break; case 'service': - StopService::run($this->resource, true); + StopService::run($this->resource, $this->deleteConnectedNetworks, $this->dockerCleanup); DeleteService::run($this->resource, $this->deleteVolumes, $this->deleteConnectedNetworks, $this->deleteConfigurations, $this->dockerCleanup); return; @@ -78,7 +78,7 @@ public function handle() $this->resource->deleteVolumes(); $this->resource->persistentStorages()->delete(); } - $this->resource->fileStorages()->delete(); + $this->resource->fileStorages()->delete(); // these are file mounts which should probably have their own flag $isDatabase = $this->resource instanceof StandalonePostgresql || $this->resource instanceof StandaloneRedis From a40dd8880d49ef691ca4322f86395f363b85ecf4 Mon Sep 17 00:00:00 2001 From: Cynthia Ebert <54354036+Cinzya@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:16:49 +0200 Subject: [PATCH 0118/1717] fix(proxy): remove hardcoded port 80/443 checks (#6275) --- app/Actions/Proxy/CheckProxy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index d4b03ffc1..a06e547c5 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -66,7 +66,7 @@ public function handle(Server $server, $fromUI = false): bool if ($server->id === 0) { $ip = 'host.docker.internal'; } - $portsToCheck = ['80', '443']; + $portsToCheck = []; try { if ($server->proxyType() !== ProxyTypes::NONE->value) { From ed0f2c1c59ab7a98202a796df7d7ab5ae5bd9dd0 Mon Sep 17 00:00:00 2001 From: Gurvan <32047986+gurvancampion@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:31:43 +0200 Subject: [PATCH 0119/1717] feat(service): add OpenPanel template (#5310) --- public/svgs/openpanel.svg | 1 + templates/compose/openpanel.yaml | 196 +++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 public/svgs/openpanel.svg create mode 100644 templates/compose/openpanel.yaml diff --git a/public/svgs/openpanel.svg b/public/svgs/openpanel.svg new file mode 100644 index 000000000..8508fc69e --- /dev/null +++ b/public/svgs/openpanel.svg @@ -0,0 +1 @@ + diff --git a/templates/compose/openpanel.yaml b/templates/compose/openpanel.yaml new file mode 100644 index 000000000..6d3738dc0 --- /dev/null +++ b/templates/compose/openpanel.yaml @@ -0,0 +1,196 @@ +# documentation: https://openpanel.dev/docs +# slogan: Open source alternative to Mixpanel and Plausible for product analytics +# tags: analytics, insights, privacy, mixpanel, plausible, google, alternative +# logo: svgs/openpanel.svg +# port: 3000 + +services: + opdb: + image: postgres:16-alpine + restart: always + volumes: + - opdb-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=${OPENPANEL_POSTGRES_DB:-openpanel-db} + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + healthcheck: + test: [CMD-SHELL, "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + + opkv: + image: redis:7.4-alpine + restart: always + volumes: + - opkv-data:/data + command: redis-server --requirepass ${SERVICE_PASSWORD_REDIS} --maxmemory-policy noeviction + healthcheck: + test: [CMD, redis-cli, -a, "${SERVICE_PASSWORD_REDIS}", ping] + interval: 10s + timeout: 5s + retries: 5 + + opch: + image: clickhouse/clickhouse-server:24.3.2-alpine + restart: always + volumes: + - opch-data:/var/lib/clickhouse + - opch-logs:/var/log/clickhouse-server + - type: bind + source: ./clickhouse-config.xml + target: /etc/clickhouse-server/config.d/op-config.xml + read_only: true + content: | + + + warning + true + + 10 + + + + + + + + + + 0.0.0.0 + 0.0.0.0 + opch + + 0 + + + 1 + replica1 + openpanel_cluster + + + - type: bind + source: ./clickhouse-user-config.xml + target: /etc/clickhouse-server/users.d/op-user-config.xml + read_only: true + content: | + + + + 0 + 0 + + + + - type: bind + source: ./init-db.sh + target: /docker-entrypoint-initdb.d/init-db.sh + content: | + #!/bin/sh + set -e + + clickhouse client -n <<-EOSQL + CREATE DATABASE IF NOT EXISTS openpanel; + EOSQL + healthcheck: + test: [CMD-SHELL, 'clickhouse-client --query "SELECT 1"'] + interval: 10s + timeout: 5s + retries: 5 + ulimits: + nofile: + soft: 262144 + hard: 262144 + + opapi: + image: lindesvard/openpanel-api:latest + restart: always + command: > + sh -c " + echo 'Running migrations...' + CI=true pnpm -r run migrate:deploy + + pnpm start + " + depends_on: + opdb: + condition: service_healthy + opch: + condition: service_healthy + opkv: + condition: service_healthy + environment: + # Common + - NODE_ENV=production + - NEXT_PUBLIC_SELF_HOSTED=true + # URLs + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@opdb:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public + - DATABASE_URL_DIRECT=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@opdb:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public + - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@opkv:6379 + - CLICKHOUSE_URL=${OPENPANEL_CLICKHOUSE_URL:-http://opch:8123/openpanel} + - SERVICE_FQDN_OPAPI + # Set coolify FQDN domain + - NEXT_PUBLIC_API_URL=$SERVICE_FQDN_OPAPI + - NEXT_PUBLIC_DASHBOARD_URL=$SERVICE_FQDN_OPDASHBOARD + # Others + - COOKIE_SECRET=${SERVICE_BASE64_COOKIESECRET} + - ALLOW_REGISTRATION=${OPENPANEL_ALLOW_REGISTRATION:-false} + - ALLOW_INVITATION=${OPENPANEL_ALLOW_INVITATION:-true} + - EMAIL_SENDER=${OPENPANEL_EMAIL_SENDER} + - RESEND_API_KEY=${RESEND_API_KEY} + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/healthcheck || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + + opdashboard: + image: lindesvard/openpanel-dashboard:latest + restart: always + depends_on: + opapi: + condition: service_healthy + environment: + # Common + - NODE_ENV=production + - NEXT_PUBLIC_SELF_HOSTED=true + # URLs + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@opdb:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public + - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@opkv:6379 + - CLICKHOUSE_URL=${OPENPANEL_CLICKHOUSE_URL:-http://opch:8123/openpanel} + - SERVICE_FQDN_OPDASHBOARD + # Set coolify FQDN domain + - NEXT_PUBLIC_API_URL=$SERVICE_FQDN_OPAPI + - NEXT_PUBLIC_DASHBOARD_URL=$SERVICE_FQDN_OPDASHBOARD + healthcheck: + test: + ["CMD-SHELL", "curl -f http://localhost:3000/api/healthcheck || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + + opworker: + image: lindesvard/openpanel-worker:latest + restart: always + depends_on: + opapi: + condition: service_healthy + environment: + # FQDN + - SERVICE_FQDN_OPBULLBOARD + # Common + - NODE_ENV=production + - NEXT_PUBLIC_SELF_HOSTED=true + # URLs + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@opdb:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public + - DATABASE_URL_DIRECT=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@opdb:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public + - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@opkv:6379 + - CLICKHOUSE_URL=${OPENPANEL_CLICKHOUSE_URL:-http://opch:8123/openpanel} + # Set coolify FQDN domain + - NEXT_PUBLIC_API_URL=$SERVICE_FQDN_OPAPI + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/healthcheck || exit 1"] + interval: 10s + timeout: 5s + retries: 5 From b52c414fe481d87aa10f14ad2e7575b500031eb4 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:11:34 +0200 Subject: [PATCH 0120/1717] refactor(service): improve openpanel template - rename all services - add more depends_on checks to ensure everything works properly and is healthy - reorder and re-format the template, ENVs and the individual sections - remove comments --- templates/compose/openpanel.yaml | 213 ++++++++++++++++--------------- 1 file changed, 107 insertions(+), 106 deletions(-) diff --git a/templates/compose/openpanel.yaml b/templates/compose/openpanel.yaml index 6d3738dc0..7eb2615e4 100644 --- a/templates/compose/openpanel.yaml +++ b/templates/compose/openpanel.yaml @@ -5,11 +5,104 @@ # port: 3000 services: - opdb: + openpanel-dashboard: + image: lindesvard/openpanel-dashboard:latest + environment: + - NODE_ENV=production + - NEXT_PUBLIC_SELF_HOSTED=true + - SERVICE_FQDN_OPDASHBOARD_3000 + - NEXT_PUBLIC_API_URL=${SERVICE_FQDN_OPAPI} + - NEXT_PUBLIC_DASHBOARD_URL=${SERVICE_FQDN_OPDASHBOARD} + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public + - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@redis:6379 + - CLICKHOUSE_URL=http://clickhouse:8123/openpanel + depends_on: + openpanel-api: + condition: service_healthy + openpanel-worker: + condition: service_healthy + postgres: + condition: service_healthy + redis: + condition: service_healthy + clickhouse: + condition: service_healthy + healthcheck: + test: + ["CMD-SHELL", "curl -f http://localhost:3000/api/healthcheck || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + + openpanel-api: + image: lindesvard/openpanel-api:latest + command: > + sh -c " + echo 'Running migrations...' + CI=true pnpm -r run migrate:deploy + + pnpm start + " + environment: + - NODE_ENV=production + - NEXT_PUBLIC_SELF_HOSTED=true + - SERVICE_FQDN_OPAPI + - NEXT_PUBLIC_API_URL=${SERVICE_FQDN_OPAPI} + - NEXT_PUBLIC_DASHBOARD_URL=${SERVICE_FQDN_OPDASHBOARD} + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public + - DATABASE_URL_DIRECT=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public + - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@redis:6379 + - CLICKHOUSE_URL=http://clickhouse:8123/openpanel + - COOKIE_SECRET=${SERVICE_BASE64_COOKIESECRET} + - ALLOW_REGISTRATION=${OPENPANEL_ALLOW_REGISTRATION:-false} + - ALLOW_INVITATION=${OPENPANEL_ALLOW_INVITATION:-false} + - EMAIL_SENDER=${OPENPANEL_EMAIL_SENDER} + - RESEND_API_KEY=${RESEND_API_KEY} + depends_on: + postgres: + condition: service_healthy + clickhouse: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/healthcheck || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + + openpanel-worker: + image: lindesvard/openpanel-worker:latest + environment: + - NODE_ENV=production + - NEXT_PUBLIC_SELF_HOSTED=true + - SERVICE_FQDN_OPBULLBOARD + - NEXT_PUBLIC_API_URL=${SERVICE_FQDN_OPAPI} + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public + - DATABASE_URL_DIRECT=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public + - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@redis:6379 + - CLICKHOUSE_URL=http://clickhouse:8123/openpanel + depends_on: + openpanel-api: + condition: service_healthy + postgres: + condition: service_healthy + redis: + condition: service_healthy + clickhouse: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/healthcheck || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + + postgres: image: postgres:16-alpine - restart: always volumes: - - opdb-data:/var/lib/postgresql/data + - openpanel_postgres_data:/var/lib/postgresql/data environment: - POSTGRES_DB=${OPENPANEL_POSTGRES_DB:-openpanel-db} - POSTGRES_USER=${SERVICE_USER_POSTGRES} @@ -20,11 +113,12 @@ services: timeout: 5s retries: 5 - opkv: + redis: image: redis:7.4-alpine - restart: always volumes: - - opkv-data:/data + - openpanel_redis_data:/data + environment: + - REDIS_PASSWORD=${SERVICE_PASSWORD_REDIS} command: redis-server --requirepass ${SERVICE_PASSWORD_REDIS} --maxmemory-policy noeviction healthcheck: test: [CMD, redis-cli, -a, "${SERVICE_PASSWORD_REDIS}", ping] @@ -32,12 +126,11 @@ services: timeout: 5s retries: 5 - opch: + clickhouse: image: clickhouse/clickhouse-server:24.3.2-alpine - restart: always volumes: - - opch-data:/var/lib/clickhouse - - opch-logs:/var/log/clickhouse-server + - openpanel_clickhouse_data:/var/lib/clickhouse + - openpanel_clickhouse_logs:/var/log/clickhouse-server - type: bind source: ./clickhouse-config.xml target: /etc/clickhouse-server/config.d/op-config.xml @@ -93,104 +186,12 @@ services: clickhouse client -n <<-EOSQL CREATE DATABASE IF NOT EXISTS openpanel; EOSQL + ulimits: + nofile: + soft: 262144 + hard: 262144 healthcheck: test: [CMD-SHELL, 'clickhouse-client --query "SELECT 1"'] interval: 10s timeout: 5s retries: 5 - ulimits: - nofile: - soft: 262144 - hard: 262144 - - opapi: - image: lindesvard/openpanel-api:latest - restart: always - command: > - sh -c " - echo 'Running migrations...' - CI=true pnpm -r run migrate:deploy - - pnpm start - " - depends_on: - opdb: - condition: service_healthy - opch: - condition: service_healthy - opkv: - condition: service_healthy - environment: - # Common - - NODE_ENV=production - - NEXT_PUBLIC_SELF_HOSTED=true - # URLs - - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@opdb:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public - - DATABASE_URL_DIRECT=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@opdb:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public - - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@opkv:6379 - - CLICKHOUSE_URL=${OPENPANEL_CLICKHOUSE_URL:-http://opch:8123/openpanel} - - SERVICE_FQDN_OPAPI - # Set coolify FQDN domain - - NEXT_PUBLIC_API_URL=$SERVICE_FQDN_OPAPI - - NEXT_PUBLIC_DASHBOARD_URL=$SERVICE_FQDN_OPDASHBOARD - # Others - - COOKIE_SECRET=${SERVICE_BASE64_COOKIESECRET} - - ALLOW_REGISTRATION=${OPENPANEL_ALLOW_REGISTRATION:-false} - - ALLOW_INVITATION=${OPENPANEL_ALLOW_INVITATION:-true} - - EMAIL_SENDER=${OPENPANEL_EMAIL_SENDER} - - RESEND_API_KEY=${RESEND_API_KEY} - healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:3000/healthcheck || exit 1"] - interval: 10s - timeout: 5s - retries: 5 - - opdashboard: - image: lindesvard/openpanel-dashboard:latest - restart: always - depends_on: - opapi: - condition: service_healthy - environment: - # Common - - NODE_ENV=production - - NEXT_PUBLIC_SELF_HOSTED=true - # URLs - - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@opdb:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public - - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@opkv:6379 - - CLICKHOUSE_URL=${OPENPANEL_CLICKHOUSE_URL:-http://opch:8123/openpanel} - - SERVICE_FQDN_OPDASHBOARD - # Set coolify FQDN domain - - NEXT_PUBLIC_API_URL=$SERVICE_FQDN_OPAPI - - NEXT_PUBLIC_DASHBOARD_URL=$SERVICE_FQDN_OPDASHBOARD - healthcheck: - test: - ["CMD-SHELL", "curl -f http://localhost:3000/api/healthcheck || exit 1"] - interval: 10s - timeout: 5s - retries: 5 - - opworker: - image: lindesvard/openpanel-worker:latest - restart: always - depends_on: - opapi: - condition: service_healthy - environment: - # FQDN - - SERVICE_FQDN_OPBULLBOARD - # Common - - NODE_ENV=production - - NEXT_PUBLIC_SELF_HOSTED=true - # URLs - - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@opdb:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public - - DATABASE_URL_DIRECT=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@opdb:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public - - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@opkv:6379 - - CLICKHOUSE_URL=${OPENPANEL_CLICKHOUSE_URL:-http://opch:8123/openpanel} - # Set coolify FQDN domain - - NEXT_PUBLIC_API_URL=$SERVICE_FQDN_OPAPI - healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:3000/healthcheck || exit 1"] - interval: 10s - timeout: 5s - retries: 5 From f34225dfd6e3325093a08d902aaf37b0faffdd62 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 6 Aug 2025 03:02:10 +0200 Subject: [PATCH 0121/1717] Update service-templates.json --- templates/service-templates.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/templates/service-templates.json b/templates/service-templates.json index c037a8889..9f2c0a773 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -2574,6 +2574,23 @@ "minversion": "0.0.0", "port": "8080" }, + "openpanel": { + "documentation": "https://openpanel.dev/docs?utm_source=coolify.io", + "slogan": "Open source alternative to Mixpanel and Plausible for product analytics", + "compose": "c2VydmljZXM6CiAgb3BlbnBhbmVsLWRhc2hib2FyZDoKICAgIGltYWdlOiAnbGluZGVzdmFyZC9vcGVucGFuZWwtZGFzaGJvYXJkOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBORVhUX1BVQkxJQ19TRUxGX0hPU1RFRD10cnVlCiAgICAgIC0gU0VSVklDRV9GUUROX09QREFTSEJPQVJEXzMwMDAKICAgICAgLSAnTkVYVF9QVUJMSUNfQVBJX1VSTD0ke1NFUlZJQ0VfRlFETl9PUEFQSX0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RBU0hCT0FSRF9VUkw9JHtTRVJWSUNFX0ZRRE5fT1BEQVNIQk9BUkR9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7T1BFTlBBTkVMX1BPU1RHUkVTX0RCOi1vcGVucGFuZWwtZGJ9P3NjaGVtYT1wdWJsaWMnCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHQ6JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfUByZWRpczo2Mzc5JwogICAgICAtICdDTElDS0hPVVNFX1VSTD1odHRwOi8vY2xpY2tob3VzZTo4MTIzL29wZW5wYW5lbCcKICAgIGRlcGVuZHNfb246CiAgICAgIG9wZW5wYW5lbC1hcGk6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgb3BlbnBhbmVsLXdvcmtlcjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBjbGlja2hvdXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ2N1cmwgLWYgaHR0cDovL2xvY2FsaG9zdDozMDAwL2FwaS9oZWFsdGhjaGVjayB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDE1cwogIG9wZW5wYW5lbC1hcGk6CiAgICBpbWFnZTogJ2xpbmRlc3ZhcmQvb3BlbnBhbmVsLWFwaTpsYXRlc3QnCiAgICBjb21tYW5kOiAic2ggLWMgXCJcbiAgZWNobyAnUnVubmluZyBtaWdyYXRpb25zLi4uJ1xuICBDST10cnVlIHBucG0gLXIgcnVuIG1pZ3JhdGU6ZGVwbG95XG5cbiAgcG5wbSBzdGFydFxuXCJcbiIKICAgIGVudmlyb25tZW50OgogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBORVhUX1BVQkxJQ19TRUxGX0hPU1RFRD10cnVlCiAgICAgIC0gU0VSVklDRV9GUUROX09QQVBJCiAgICAgIC0gJ05FWFRfUFVCTElDX0FQSV9VUkw9JHtTRVJWSUNFX0ZRRE5fT1BBUEl9JwogICAgICAtICdORVhUX1BVQkxJQ19EQVNIQk9BUkRfVVJMPSR7U0VSVklDRV9GUUROX09QREFTSEJPQVJEfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke09QRU5QQU5FTF9QT1NUR1JFU19EQjotb3BlbnBhbmVsLWRifT9zY2hlbWE9cHVibGljJwogICAgICAtICdEQVRBQkFTRV9VUkxfRElSRUNUPXBvc3RncmVzOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke09QRU5QQU5FTF9QT1NUR1JFU19EQjotb3BlbnBhbmVsLWRifT9zY2hlbWE9cHVibGljJwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OScKICAgICAgLSAnQ0xJQ0tIT1VTRV9VUkw9aHR0cDovL2NsaWNraG91c2U6ODEyMy9vcGVucGFuZWwnCiAgICAgIC0gJ0NPT0tJRV9TRUNSRVQ9JHtTRVJWSUNFX0JBU0U2NF9DT09LSUVTRUNSRVR9JwogICAgICAtICdBTExPV19SRUdJU1RSQVRJT049JHtPUEVOUEFORUxfQUxMT1dfUkVHSVNUUkFUSU9OOi1mYWxzZX0nCiAgICAgIC0gJ0FMTE9XX0lOVklUQVRJT049JHtPUEVOUEFORUxfQUxMT1dfSU5WSVRBVElPTjotZmFsc2V9JwogICAgICAtICdFTUFJTF9TRU5ERVI9JHtPUEVOUEFORUxfRU1BSUxfU0VOREVSfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBjbGlja2hvdXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ2N1cmwgLWYgaHR0cDovL2xvY2FsaG9zdDozMDAwL2hlYWx0aGNoZWNrIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgb3BlbnBhbmVsLXdvcmtlcjoKICAgIGltYWdlOiAnbGluZGVzdmFyZC9vcGVucGFuZWwtd29ya2VyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBORVhUX1BVQkxJQ19TRUxGX0hPU1RFRD10cnVlCiAgICAgIC0gU0VSVklDRV9GUUROX09QQlVMTEJPQVJECiAgICAgIC0gJ05FWFRfUFVCTElDX0FQSV9VUkw9JHtTRVJWSUNFX0ZRRE5fT1BBUEl9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7T1BFTlBBTkVMX1BPU1RHUkVTX0RCOi1vcGVucGFuZWwtZGJ9P3NjaGVtYT1wdWJsaWMnCiAgICAgIC0gJ0RBVEFCQVNFX1VSTF9ESVJFQ1Q9cG9zdGdyZXM6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7T1BFTlBBTkVMX1BPU1RHUkVTX0RCOi1vcGVucGFuZWwtZGJ9P3NjaGVtYT1wdWJsaWMnCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHQ6JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfUByZWRpczo2Mzc5JwogICAgICAtICdDTElDS0hPVVNFX1VSTD1odHRwOi8vY2xpY2tob3VzZTo4MTIzL29wZW5wYW5lbCcKICAgIGRlcGVuZHNfb246CiAgICAgIG9wZW5wYW5lbC1hcGk6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdjdXJsIC1mIGh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC9oZWFsdGhjaGVjayB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDVzCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ29wZW5wYW5lbF9wb3N0Z3Jlc19kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19EQj0ke09QRU5QQU5FTF9QT1NUR1JFU19EQjotb3BlbnBhbmVsLWRifScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny40LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ29wZW5wYW5lbF9yZWRpc19kYXRhOi9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1JFRElTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfSAtLW1heG1lbW9yeS1wb2xpY3kgbm9ldmljdGlvbicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICcke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUKICBjbGlja2hvdXNlOgogICAgaW1hZ2U6ICdjbGlja2hvdXNlL2NsaWNraG91c2Utc2VydmVyOjI0LjMuMi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdvcGVucGFuZWxfY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0gJ29wZW5wYW5lbF9jbGlja2hvdXNlX2xvZ3M6L3Zhci9sb2cvY2xpY2tob3VzZS1zZXJ2ZXInCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2UtY29uZmlnLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9vcC1jb25maWcueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8a2VlcF9hbGl2ZV90aW1lb3V0PjEwPC9rZWVwX2FsaXZlX3RpbWVvdXQ+XG4gICAgPCEtLSBTdG9wIGFsbCB0aGUgdW5uZWNlc3NhcnkgbG9nZ2luZyAtLT5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxpc3Rlbl9ob3N0PjAuMC4wLjA8L2xpc3Rlbl9ob3N0PlxuICAgIDxpbnRlcnNlcnZlcl9saXN0ZW5faG9zdD4wLjAuMC4wPC9pbnRlcnNlcnZlcl9saXN0ZW5faG9zdD5cbiAgICA8aW50ZXJzZXJ2ZXJfaHR0cF9ob3N0Pm9wY2g8L2ludGVyc2VydmVyX2h0dHBfaG9zdD5cbiAgICA8IS0tIERpc2FibGUgY2dyb3VwIG1lbW9yeSBvYnNlcnZlciAtLT5cbiAgICA8Y2dyb3Vwc19tZW1vcnlfdXNhZ2Vfb2JzZXJ2ZXJfd2FpdF90aW1lPjA8L2Nncm91cHNfbWVtb3J5X3VzYWdlX29ic2VydmVyX3dhaXRfdGltZT5cbiAgICA8IS0tIE5vdCB1c2VkIGFueW1vcmUsIGJ1dCBrZXB0IGZvciBiYWNrd2FyZHMgY29tcGF0aWJpbGl0eSAtLT5cbiAgICA8bWFjcm9zPlxuICAgICAgICA8c2hhcmQ+MTwvc2hhcmQ+XG4gICAgICAgIDxyZXBsaWNhPnJlcGxpY2ExPC9yZXBsaWNhPlxuICAgICAgICA8Y2x1c3Rlcj5vcGVucGFuZWxfY2x1c3RlcjwvY2x1c3Rlcj5cbiAgICA8L21hY3Jvcz5cbjwvY2xpY2tob3VzZT4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2UtdXNlci1jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLmQvb3AtdXNlci1jb25maWcueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxwcm9maWxlcz5cbiAgICAgICAgPGRlZmF1bHQ+XG4gICAgICAgICAgICA8bG9nX3F1ZXJpZXM+MDwvbG9nX3F1ZXJpZXM+XG4gICAgICAgICAgICA8bG9nX3F1ZXJ5X3RocmVhZHM+MDwvbG9nX3F1ZXJ5X3RocmVhZHM+XG4gICAgICAgIDwvZGVmYXVsdD5cbiAgICA8L3Byb2ZpbGVzPlxuPC9jbGlja2hvdXNlPlxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pbml0LWRiLnNoCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1kYi5zaAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vc2hcbnNldCAtZVxuXG5jbGlja2hvdXNlIGNsaWVudCAtbiA8PC1FT1NRTFxuICBDUkVBVEUgREFUQUJBU0UgSUYgTk9UIEVYSVNUUyBvcGVucGFuZWw7XG5FT1NRTCIKICAgIHVsaW1pdHM6CiAgICAgIG5vZmlsZToKICAgICAgICBzb2Z0OiAyNjIxNDQKICAgICAgICBoYXJkOiAyNjIxNDQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY2xpY2tob3VzZS1jbGllbnQgLS1xdWVyeSAiU0VMRUNUIDEiJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUK", + "tags": [ + "analytics", + "insights", + "privacy", + "mixpanel", + "plausible", + "google", + "alternative" + ], + "logo": "svgs/openpanel.svg", + "minversion": "0.0.0", + "port": "3000" + }, "orangehrm": { "documentation": "https://starterhelp.orangehrm.com/hc/en-us?utm_source=coolify.io", "slogan": "OrangeHRM open source HR management software.", From 4f77bd43472335403814efc87579330092f7b022 Mon Sep 17 00:00:00 2001 From: Trung-DV Date: Tue, 5 Aug 2025 15:20:22 +0700 Subject: [PATCH 0122/1717] Fix volume target, use the last part Signed-off-by: Trung-DV --- bootstrap/helpers/shared.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7ce511f2c..a6b8fe55b 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3420,7 +3420,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } if (is_string($volume)) { $source = str($volume)->before(':'); - $target = str($volume)->after(':')->beforeLast(':'); + $target = str($volume)->afterLast(':'); $source = $name; $volume = "$source:$target"; } elseif (is_array($volume)) { From 1d4a19fb6104bf6f8b475e4aaa299f375e98f356 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Mon, 11 Aug 2025 12:08:17 +0200 Subject: [PATCH 0123/1717] fix(service): update healthcheck of penpot backend container (#6272) --- templates/compose/penpot.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/penpot.yaml b/templates/compose/penpot.yaml index fa92abb7f..9a4e2de8d 100644 --- a/templates/compose/penpot.yaml +++ b/templates/compose/penpot.yaml @@ -55,7 +55,7 @@ services: - PENPOT_SMTP_TLS=${PENPOT_SMTP_TLS:-false} - PENPOT_SMTP_SSL=${PENPOT_SMTP_SSL:-false} healthcheck: - test: ['CMD', 'curl', '-f', 'http://127.0.0.1:6060/readyz'] + test: ['CMD', 'node', '-e', "require('http').get({host:'127.0.0.1', port:6060, path:'/readyz'}, res => process.exit(res.statusCode===200 ? 0 : 1)).on('error', () => process.exit(1));"] interval: 10s timeout: 30s retries: 15 From 03040d6bc87c6c9c296c4d7de567ce3dbe3d9634 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Mon, 11 Aug 2025 12:13:25 +0200 Subject: [PATCH 0124/1717] feat(service): add librechat template (#5654) --- public/svgs/librechat.svg | 32 +++++++ templates/compose/librechat.yaml | 157 +++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 public/svgs/librechat.svg create mode 100644 templates/compose/librechat.yaml diff --git a/public/svgs/librechat.svg b/public/svgs/librechat.svg new file mode 100644 index 000000000..36a536d65 --- /dev/null +++ b/public/svgs/librechat.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/compose/librechat.yaml b/templates/compose/librechat.yaml new file mode 100644 index 000000000..e5647bb2d --- /dev/null +++ b/templates/compose/librechat.yaml @@ -0,0 +1,157 @@ +# documentation: https://docs.librechat.ai/install/configuration/dotenv.html +# slogan: Self-hosted, powerful, and privacy-focused chat UI for multiple AI models +# tags: ai,chat,gpt,claude,palm,openai,azure,huggingface,anthropic,ollama,llm +# logo: svgs/librechat.svg +# port: 3080 + +services: + librechat: + image: ghcr.io/danny-avila/librechat-dev-api:latest + depends_on: + mongodb: + condition: service_healthy + rag_api: + condition: service_healthy + environment: + - HOST=0.0.0.0 + - PORT=3080 + - SERVICE_FQDN_LIBRECHAT_3080 + # MongoDB settings + - MONGO_URI=mongodb://${SERVICE_USER_MONGO}:${SERVICE_PASSWORD_MONGO}@mongodb:27017/librechat?authSource=admin + # Meilisearch settings + - MEILI_HOST=http://meilisearch:7700 + - MEILI_MASTER_KEY=${SERVICE_PASSWORD_MEILI} + # RAG settings + - RAG_PORT=8000 + - RAG_API_URL=http://rag_api:8000 + # Auth settings + - DOMAIN_CLIENT=${SERVICE_FQDN_LIBRECHAT} + - DOMAIN_SERVER=${SERVICE_FQDN_LIBRECHAT} + - JWT_SECRET=${SERVICE_PASSWORD_JWT} + - JWT_REFRESH_SECRET=${SERVICE_PASSWORD_64_JWT} + # App settings + - APP_TITLE=${APP_TITLE:-LibreChat} + - ALLOW_EMAIL_LOGIN=${ALLOW_EMAIL_LOGIN:-true} + - ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-true} + - ALLOW_SOCIAL_LOGIN=${ALLOW_SOCIAL_LOGIN:-false} + - ALLOW_SOCIAL_REGISTRATION=${ALLOW_SOCIAL_REGISTRATION:-false} + - ALLOW_PASSWORD_RESET=${ALLOW_PASSWORD_RESET:-false} + - ALLOW_UNVERIFIED_EMAIL_LOGIN=${ALLOW_UNVERIFIED_EMAIL_LOGIN:-true} + # Encryption settings + - CREDS_KEY=${SERVICE_PASSWORD_64_CREDS} + - CREDS_IV=${SERVICE_PASSWORD_CREDS} + # API Keys + - ANTHROPIC_API_KEY=${SERVICE_ANTHROPIC_API_KEY:-user_provided} + - GOOGLE_KEY=${SERVICE_GOOGLE_API_KEY:-user_provided} + - OPENAI_API_KEY=${SERVICE_OPENAI_API_KEY:-user_provided} + - ASSISTANTS_API_KEY=${SERVICE_ASSISTANTS_API_KEY:-user_provided} + # Debug settings + - DEBUG_LOGGING=${DEBUG_LOGGING:-false} + - DEBUG_OPENAI=${DEBUG_OPENAI:-false} + - DEBUG_PLUGINS=${DEBUG_OPENAI:-false} + - NO_INDEX=${NO_INDEX:-true} + healthcheck: + test: + [ + 'CMD', + 'wget', + '--no-verbose', + '--tries=1', + '--spider', + 'http://127.0.0.1:3080/api/health', + ] + interval: 5s + timeout: 10s + retries: 3 + volumes: + - librechat-images:/app/client/public/images + - librechat-logs:/app/api/logs + - librechat-uploads:/app/uploads + - type: bind + source: ./librechat.yaml + target: /app/librechat.yaml + content: | + # For more information, see the Configuration Guide: + # https://www.librechat.ai/docs/configuration/librechat_yaml + + # Configuration version (required) + version: 1.2.8 + + mongodb: + environment: + - MONGO_INITDB_ROOT_USERNAME=${SERVICE_USER_MONGO} + - MONGO_INITDB_ROOT_PASSWORD=${SERVICE_PASSWORD_MONGO} + image: mongo:8 + volumes: + - mongodb-data:/data/db + healthcheck: + test: + [ + 'CMD', + 'mongosh', + '--eval', + "db.runCommand('ping').ok", + '127.0.0.1:27017/test', + '--quiet', + ] + interval: 5s + timeout: 10s + retries: 3 + + meilisearch: + image: getmeili/meilisearch:v1.12.3 + environment: + - MEILI_MASTER_KEY=${SERVICE_PASSWORD_MEILI} + - MEILI_NO_ANALYTICS=${MEILI_NO_ANALYTICS:-false} + - MEILI_ENV=production + - MEILI_HOST=http://meilisearch:7700 + volumes: + - meilisearch-data:/meili_data + healthcheck: + test: ['CMD', 'curl', '-f', 'http://127.0.0.1:7700/health'] + interval: 2s + timeout: 10s + retries: 15 + + vectordb: + image: ankane/pgvector:latest + environment: + - POSTGRES_DB=rag + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - vectordb-data:/var/lib/postgresql/data + healthcheck: + test: + - CMD + - pg_isready + - '--username=$SERVICE_USER_POSTGRES' + - '--host=127.0.0.1' + - '--port=5432' + - '--dbname=rag' + interval: 2s + timeout: 1m + retries: 5 + start_period: 10s + + rag_api: + image: ghcr.io/danny-avila/librechat-rag-api-dev-lite:latest + depends_on: + vectordb: + condition: service_healthy + environment: + - POSTGRES_DB=rag + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - DB_HOST=vectordb + - DB_USER=${SERVICE_USER_POSTGRES} + - DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - DB_NAME=rag + - RAG_PORT=8000 + - RAG_OPENAI_API_KEY=${SERVICE_OPENAI_API_KEY:-user_provided} + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"] + interval: 5s + timeout: 10s + retries: 10 From bee98e02bc7e6335f3882162266aaeb2b3579aa7 Mon Sep 17 00:00:00 2001 From: howardshand Date: Mon, 11 Aug 2025 05:29:18 -0500 Subject: [PATCH 0125/1717] feat(service): add Homebox service (#6116) --- public/svgs/homebox.svg | 11 +++++++++++ templates/compose/homebox.yaml | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 public/svgs/homebox.svg create mode 100644 templates/compose/homebox.yaml diff --git a/public/svgs/homebox.svg b/public/svgs/homebox.svg new file mode 100644 index 000000000..08670bbb9 --- /dev/null +++ b/public/svgs/homebox.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/templates/compose/homebox.yaml b/templates/compose/homebox.yaml new file mode 100644 index 000000000..0f645aa93 --- /dev/null +++ b/templates/compose/homebox.yaml @@ -0,0 +1,27 @@ +# documentation: https://github.com/sysadminsmedia/homebox +# slogan: Homebox is the inventory and organization system built for the Home User. +# tags: inventory, home, organize +# logo: svgs/homebox.svg +# port: 7745 + +services: + homebox: + image: 'ghcr.io/sysadminsmedia/homebox:latest' + environment: + - SERVICE_FQDN_HOMEBOX_7745 + - HBOX_OPTIONS_ALLOW_REGISTRATION=${HBOX_OPTIONS_ALLOW_REGISTRATION:-false} + - HBOX_LOG_LEVEL=${HBOX_LOG_LEVEL:-info} + - HBOX_LOG_FORMAT=${HBOX_LOG_FORMAT:-text} + - HBOX_WEB_MAX_UPLOAD_SIZE=${HBOX_WEB_MAX_UPLOAD_SIZE:-10} + - HBOX_MAILER_HOST=${HBOX_MAILER_HOST} + - HBOX_MAILER_PORT=${HBOX_MAILER_PORT:-587} + - HBOX_MAILER_USERNAME=${HBOX_MAILER_USERNAME} + - HBOX_MAILER_PASSWORD=${HBOX_MAILER_PASSWORD} + - HBOX_MAILER_FROM=${HBOX_MAILER_FROM} + volumes: + - 'homebox-data:/data/' + healthcheck: + test: ["CMD", "sh", "-c", "wget --method=GET -qO- http://localhost:7745/api/v1/status > /dev/null || exit 1"] + interval: 30s + timeout: 5s + retries: 10 From 7dcb5c43ae91aded4d50c60c596a43d84fc10878 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:31:09 +0200 Subject: [PATCH 0126/1717] chore(service): homebox formatting --- templates/compose/homebox.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/compose/homebox.yaml b/templates/compose/homebox.yaml index 0f645aa93..6d3f3e597 100644 --- a/templates/compose/homebox.yaml +++ b/templates/compose/homebox.yaml @@ -6,7 +6,7 @@ services: homebox: - image: 'ghcr.io/sysadminsmedia/homebox:latest' + image: ghcr.io/sysadminsmedia/homebox:latest environment: - SERVICE_FQDN_HOMEBOX_7745 - HBOX_OPTIONS_ALLOW_REGISTRATION=${HBOX_OPTIONS_ALLOW_REGISTRATION:-false} @@ -19,7 +19,7 @@ services: - HBOX_MAILER_PASSWORD=${HBOX_MAILER_PASSWORD} - HBOX_MAILER_FROM=${HBOX_MAILER_FROM} volumes: - - 'homebox-data:/data/' + - homebox-data:/data healthcheck: test: ["CMD", "sh", "-c", "wget --method=GET -qO- http://localhost:7745/api/v1/status > /dev/null || exit 1"] interval: 30s From 9ec72e8769fc75b82d29bcf9f288215cdd6cdd61 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:32:01 +0200 Subject: [PATCH 0127/1717] refactor(service): improve librechat - remove comments - format and reorder service --- templates/compose/librechat.yaml | 104 +++++++++++++++---------------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/templates/compose/librechat.yaml b/templates/compose/librechat.yaml index e5647bb2d..fcb8f8c6d 100644 --- a/templates/compose/librechat.yaml +++ b/templates/compose/librechat.yaml @@ -7,29 +7,19 @@ services: librechat: image: ghcr.io/danny-avila/librechat-dev-api:latest - depends_on: - mongodb: - condition: service_healthy - rag_api: - condition: service_healthy environment: - - HOST=0.0.0.0 - - PORT=3080 - SERVICE_FQDN_LIBRECHAT_3080 - # MongoDB settings - - MONGO_URI=mongodb://${SERVICE_USER_MONGO}:${SERVICE_PASSWORD_MONGO}@mongodb:27017/librechat?authSource=admin - # Meilisearch settings - - MEILI_HOST=http://meilisearch:7700 - - MEILI_MASTER_KEY=${SERVICE_PASSWORD_MEILI} - # RAG settings - - RAG_PORT=8000 - - RAG_API_URL=http://rag_api:8000 - # Auth settings - DOMAIN_CLIENT=${SERVICE_FQDN_LIBRECHAT} - DOMAIN_SERVER=${SERVICE_FQDN_LIBRECHAT} + - HOST=0.0.0.0 + - PORT=3080 + - MONGO_URI=mongodb://${SERVICE_USER_MONGO}:${SERVICE_PASSWORD_MONGO}@mongodb:27017/librechat?authSource=admin + - MEILI_HOST=http://meilisearch:7700 + - MEILI_MASTER_KEY=${SERVICE_PASSWORD_MEILI} + - RAG_PORT=8000 + - RAG_API_URL=http://rag-api:8000 - JWT_SECRET=${SERVICE_PASSWORD_JWT} - JWT_REFRESH_SECRET=${SERVICE_PASSWORD_64_JWT} - # App settings - APP_TITLE=${APP_TITLE:-LibreChat} - ALLOW_EMAIL_LOGIN=${ALLOW_EMAIL_LOGIN:-true} - ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-true} @@ -37,32 +27,16 @@ services: - ALLOW_SOCIAL_REGISTRATION=${ALLOW_SOCIAL_REGISTRATION:-false} - ALLOW_PASSWORD_RESET=${ALLOW_PASSWORD_RESET:-false} - ALLOW_UNVERIFIED_EMAIL_LOGIN=${ALLOW_UNVERIFIED_EMAIL_LOGIN:-true} - # Encryption settings - CREDS_KEY=${SERVICE_PASSWORD_64_CREDS} - CREDS_IV=${SERVICE_PASSWORD_CREDS} - # API Keys - ANTHROPIC_API_KEY=${SERVICE_ANTHROPIC_API_KEY:-user_provided} - GOOGLE_KEY=${SERVICE_GOOGLE_API_KEY:-user_provided} - OPENAI_API_KEY=${SERVICE_OPENAI_API_KEY:-user_provided} - ASSISTANTS_API_KEY=${SERVICE_ASSISTANTS_API_KEY:-user_provided} - # Debug settings - DEBUG_LOGGING=${DEBUG_LOGGING:-false} - DEBUG_OPENAI=${DEBUG_OPENAI:-false} - DEBUG_PLUGINS=${DEBUG_OPENAI:-false} - NO_INDEX=${NO_INDEX:-true} - healthcheck: - test: - [ - 'CMD', - 'wget', - '--no-verbose', - '--tries=1', - '--spider', - 'http://127.0.0.1:3080/api/health', - ] - interval: 5s - timeout: 10s - retries: 3 volumes: - librechat-images:/app/client/public/images - librechat-logs:/app/api/logs @@ -71,28 +45,46 @@ services: source: ./librechat.yaml target: /app/librechat.yaml content: | - # For more information, see the Configuration Guide: - # https://www.librechat.ai/docs/configuration/librechat_yaml - - # Configuration version (required) version: 1.2.8 + depends_on: + mongodb: + condition: service_healthy + meilisearch: + condition: service_healthy + vectordb: + condition: service_healthy + rag-api: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://127.0.0.1:3080/api/health", + ] + interval: 5s + timeout: 10s + retries: 5 mongodb: + image: mongo:8 environment: - MONGO_INITDB_ROOT_USERNAME=${SERVICE_USER_MONGO} - MONGO_INITDB_ROOT_PASSWORD=${SERVICE_PASSWORD_MONGO} - image: mongo:8 volumes: - mongodb-data:/data/db healthcheck: test: [ - 'CMD', - 'mongosh', - '--eval', + "CMD", + "mongosh", + "--eval", "db.runCommand('ping').ok", - '127.0.0.1:27017/test', - '--quiet', + "127.0.0.1:27017/test", + "--quiet", ] interval: 5s timeout: 10s @@ -108,7 +100,7 @@ services: volumes: - meilisearch-data:/meili_data healthcheck: - test: ['CMD', 'curl', '-f', 'http://127.0.0.1:7700/health'] + test: ["CMD", "curl", "-f", "http://127.0.0.1:7700/health"] interval: 2s timeout: 10s retries: 15 @@ -126,20 +118,17 @@ services: test: - CMD - pg_isready - - '--username=$SERVICE_USER_POSTGRES' - - '--host=127.0.0.1' - - '--port=5432' - - '--dbname=rag' + - "--username=$SERVICE_USER_POSTGRES" + - "--host=127.0.0.1" + - "--port=5432" + - "--dbname=rag" interval: 2s timeout: 1m retries: 5 start_period: 10s - rag_api: + rag-api: image: ghcr.io/danny-avila/librechat-rag-api-dev-lite:latest - depends_on: - vectordb: - condition: service_healthy environment: - POSTGRES_DB=rag - POSTGRES_USER=${SERVICE_USER_POSTGRES} @@ -150,8 +139,17 @@ services: - DB_NAME=rag - RAG_PORT=8000 - RAG_OPENAI_API_KEY=${SERVICE_OPENAI_API_KEY:-user_provided} + depends_on: + vectordb: + condition: service_healthy healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"] + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')", + ] interval: 5s timeout: 10s retries: 10 From e572017d27be55e56adf19d2351bfbab7cd1f679 Mon Sep 17 00:00:00 2001 From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com> Date: Mon, 11 Aug 2025 08:03:46 -0400 Subject: [PATCH 0128/1717] fix(api): duplicated logs in application endpoint (#6292) --- bootstrap/helpers/docker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 944c51e3c..739f98f22 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -1101,7 +1101,7 @@ function getContainerLogs(Server $server, string $container_id, int $lines = 100 ], $server); } - $output .= removeAnsiColors($output); + $output = removeAnsiColors($output); return $output; } From d53e493dcc601988490a210c90781e8c07d7c69e Mon Sep 17 00:00:00 2001 From: Aaryan meena <134821046+aaryan359@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:03:31 +0530 Subject: [PATCH 0129/1717] chore: clarify usage of custom redis configuration (#6321) --- .../project/database/redis/general.blade.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php index 577c0d3e9..b4876f325 100644 --- a/resources/views/livewire/project/database/redis/general.blade.php +++ b/resources/views/livewire/project/database/redis/general.blade.php @@ -132,8 +132,16 @@ id="database.public_port" label="Public Port" />
+ placeholder="# maxmemory 256mb +# maxmemory-policy allkeys-lru +# timeout 300" + helper="You only need to provide the Redis directives you want to override — Redis will use default values for everything else.

+⚠️ Important: Coolify automatically applies the requirepass directive using the password shown in the Password field above. If you override requirepass in your custom configuration, make sure it matches the password field to avoid authentication issues.

+🔗 Tip: View the full Redis default configuration to see what options are available." + label="Custom Redis Configuration" rows="10" id="database.redis_conf" /> + + +

Advanced

Date: Mon, 11 Aug 2025 09:03:13 -0400 Subject: [PATCH 0130/1717] fix(service): documenso signees always pending (#6334) --- templates/compose/documenso.yaml | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/templates/compose/documenso.yaml b/templates/compose/documenso.yaml index 6a3873799..97ae6f918 100644 --- a/templates/compose/documenso.yaml +++ b/templates/compose/documenso.yaml @@ -26,6 +26,16 @@ services: - NEXT_PRIVATE_SMTP_FROM_ADDRESS=${NEXT_PRIVATE_SMTP_FROM_ADDRESS} - NEXT_PRIVATE_DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@database/${POSTGRES_DB:-documenso-db}?schema=public - NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@database/${POSTGRES_DB:-documenso-db}?schema=public + - NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/app/apps/remix/certs/certificate.p12 + - NEXT_PRIVATE_SIGNING_PASSPHRASE=${SERVICE_PASSWORD_DOCUMENSO} + - CERT_VALID_DAYS=${CERT_VALID_DAYS:-365} + - CERT_INFO_COUNTRY_NAME=${CERT_INFO_COUNTRY_NAME:-DO} + - CERT_INFO_STATE_OR_PROVIDENCE=${CERT_INFO_STATE_OR_PROVIDENCE:-Santiago} + - CERT_INFO_LOCALITY_NAME=${CERT_INFO_LOCALITY_NAME:-Santiago} + - CERT_INFO_ORGANIZATION_NAME=${CERT_INFO_ORGANIZATION_NAME:-Example INC} + - CERT_INFO_ORGANIZATIONAL_UNIT=${CERT_INFO_ORGANIZATIONAL_UNIT:-IT Department} + - CERT_INFO_EMAIL=${CERT_INFO_EMAIL:-example@gmail.com} + - NEXT_PUBLIC_DISABLE_SIGNUP=${DISABLE_LOGIN:-false} healthcheck: test: - CMD-SHELL @@ -33,6 +43,55 @@ services: interval: 2s timeout: 10s retries: 20 + entrypoint: + - /bin/sh + - -c + - | + echo "./certs" > /tmp/certs_dir_path + echo "./make-certs.sh" > /tmp/cert_script_path + echo "${SERVICE_PASSWORD_DOCUMENSO}" > /tmp/cert_pass + + touch /tmp/cert_info_path + cat < /tmp/cert_info_path + [ req ] + distinguished_name = req_distinguished_name + prompt = no + [ req_distinguished_name ] + C = ${CERT_INFO_COUNTRY_NAME} + ST = ${CERT_INFO_STATE_OR_PROVIDENCE} + L = ${CERT_INFO_LOCALITY_NAME} + O = ${CERT_INFO_ORGANIZATION_NAME} + OU = ${CERT_INFO_ORGANIZATIONAL_UNIT} + CN = ${SERVICE_FQDN_DOCUMENSO} + emailAddress = ${CERT_INFO_EMAIL} + EOF + + cat < "$(cat /tmp/cert_script_path)" + mkdir -p "$(cat /tmp/certs_dir_path)" && cd "$(cat /tmp/certs_dir_path)" + + openssl genrsa -out private.key 2048 + + openssl req \ + -new \ + -x509 \ + -key private.key \ + -out certificate.crt \ + -days ${CERT_VALID_DAYS} \ + -config /tmp/cert_info_path + + openssl pkcs12 \ + -export \ + -out certificate.p12 \ + -inkey private.key \ + -in certificate.crt \ + -legacy \ + -password file:/tmp/cert_pass + EOF + chmod +x "$(cat /tmp/cert_script_path)" + + sh "$(cat /tmp/cert_script_path)" + + ./start.sh database: image: postgres:17 From 0d461e1218e02e33caf481f77edd9ec65f953562 Mon Sep 17 00:00:00 2001 From: Gabriel Peralta Date: Mon, 11 Aug 2025 10:22:12 -0300 Subject: [PATCH 0131/1717] Update and rename newt-pangolin to newt-pangolin.yaml --- templates/compose/{newt-pangolin => newt-pangolin.yaml} | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) rename templates/compose/{newt-pangolin => newt-pangolin.yaml} (78%) diff --git a/templates/compose/newt-pangolin b/templates/compose/newt-pangolin.yaml similarity index 78% rename from templates/compose/newt-pangolin rename to templates/compose/newt-pangolin.yaml index 1e52330f9..695766887 100644 --- a/templates/compose/newt-pangolin +++ b/templates/compose/newt-pangolin.yaml @@ -5,11 +5,9 @@ services: newt: - image: fosrl/newt - container_name: newt - restart: unless-stopped + image: fosrl/newt:latest environment: - - 'PANGOLIN_ENDPOINT=${PANGOLIN_ENDPOINT:-domain.tld}' + - 'PANGOLIN_ENDPOINT=$SERVICE_FQDN_PANGOLIN' - 'NEWT_ID=${NEWT_ID}' - 'NEWT_SECRET=${NEWT_SECRET}' healthcheck: From 8ba2bf9dbdbe2b728839c51794473dc05daecd53 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Mon, 11 Aug 2025 15:57:00 +0200 Subject: [PATCH 0132/1717] fix(signoz): remove hardcoded container names --- templates/compose/signoz.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 3e56b17f8..59527f36c 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -7,7 +7,6 @@ services: init-clickhouse: image: clickhouse/clickhouse-server:24.1.2-alpine - container_name: signoz-init-clickhouse command: - bash - -c @@ -29,7 +28,6 @@ services: zookeeper: image: bitnami/zookeeper:3.7.1 - container_name: signoz-zookeeper user: root healthcheck: test: @@ -54,7 +52,6 @@ services: clickhouse: # addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab image: clickhouse/clickhouse-server:24.1.2-alpine - container_name: signoz-clickhouse tty: true depends_on: init-clickhouse: @@ -388,7 +385,6 @@ services: signoz: image: signoz/signoz:latest - container_name: signoz depends_on: clickhouse: condition: service_healthy @@ -469,7 +465,6 @@ services: otel-collector: image: signoz/signoz-otel-collector:latest - container_name: signoz-otel-collector depends_on: clickhouse: condition: service_healthy @@ -602,7 +597,6 @@ services: schema-migrator-sync: image: signoz/signoz-schema-migrator:latest - container_name: schema-migrator-sync command: - sync - --dsn=tcp://clickhouse:9000 @@ -618,7 +612,6 @@ services: schema-migrator-async: image: signoz/signoz-schema-migrator:latest - container_name: schema-migrator-async depends_on: clickhouse: condition: service_healthy From a506c29b95b1e611a7ff99f5b09177b55c0755d6 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Mon, 11 Aug 2025 15:59:40 +0200 Subject: [PATCH 0133/1717] fix(signoz): remove HTTP collector FQDN in otel-collector --- templates/compose/signoz.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 59527f36c..35911f9f0 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -585,7 +585,6 @@ services: content: | server_endpoint: ws://signoz:4320/v1/opamp environment: - - SERVICE_FQDN_OTELCOLLECTORGRPC_4317 - SERVICE_FQDN_OTELCOLLECTORHTTP_4318 - OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux - LOW_CARDINAL_EXCEPTION_GROUPING=false From c404581b25c8c5319dc9fe1460bdedd9c3c532ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8F=94=EF=B8=8F=20Peak?= <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:22:03 +0200 Subject: [PATCH 0134/1717] fix(database): custom postgres configs with SSL (#6352) --- app/Actions/Database/StartPostgresql.php | 26 +++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index a40eac17b..4314ccd2f 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -185,6 +185,8 @@ public function handle(StandalonePostgresql $database) } } + $command = ['postgres']; + if (filled($this->database->postgres_conf)) { $docker_compose['services'][$container_name]['volumes'] = array_merge( $docker_compose['services'][$container_name]['volumes'], @@ -195,29 +197,25 @@ public function handle(StandalonePostgresql $database) 'read_only' => true, ]] ); - $docker_compose['services'][$container_name]['command'] = [ - 'postgres', - '-c', - 'config_file=/etc/postgresql/postgresql.conf', - ]; + $command = array_merge($command, ['-c', 'config_file=/etc/postgresql/postgresql.conf']); } if ($this->database->enable_ssl) { - $docker_compose['services'][$container_name]['command'] = [ - 'postgres', - '-c', - 'ssl=on', - '-c', - 'ssl_cert_file=/var/lib/postgresql/certs/server.crt', - '-c', - 'ssl_key_file=/var/lib/postgresql/certs/server.key', - ]; + $command = array_merge($command, [ + '-c', 'ssl=on', + '-c', 'ssl_cert_file=/var/lib/postgresql/certs/server.crt', + '-c', 'ssl_key_file=/var/lib/postgresql/certs/server.key', + ]); } // Add custom docker run options $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + if (count($command) > 1) { + $docker_compose['services'][$container_name]['command'] = $command; + } + $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; From 9b0fd2073a06a1f470d5b2b36bae9f43f4ab19c7 Mon Sep 17 00:00:00 2001 From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:31:12 -0400 Subject: [PATCH 0135/1717] fix(api): update service upsert to retain name and description values if not set --- app/Http/Controllers/Api/ServicesController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index f2d6c4dcf..1db2a3663 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -713,8 +713,8 @@ private function upsert_service(Request $request, Service $service, string $team } $connectToDockerNetwork = $request->connect_to_docker_network ?? false; - $service->name = $request->name ?? null; - $service->description = $request->description ?? null; + $service->name = $request->name ?? $service->name; + $service->description = $request->description ?? $service->description; $service->environment_id = $environment->id; $service->server_id = $server->id; $service->destination_id = $destination->id; From bf738b2d2d8cb749a35ce7c74298f6b9961ad21b Mon Sep 17 00:00:00 2001 From: Verity <83372423+CallMeVerity@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:55:37 +0100 Subject: [PATCH 0136/1717] feat(service): add pterodactyl & wings services (#5537) --- templates/compose/pterodactyl-with-wings.yaml | 140 ++++++++++++++++++ templates/compose/pterodactyl.yaml | 93 +++--------- templates/compose/wings.yaml | 44 ++++++ 3 files changed, 208 insertions(+), 69 deletions(-) create mode 100644 templates/compose/pterodactyl-with-wings.yaml create mode 100644 templates/compose/wings.yaml diff --git a/templates/compose/pterodactyl-with-wings.yaml b/templates/compose/pterodactyl-with-wings.yaml new file mode 100644 index 000000000..bd0c07cca --- /dev/null +++ b/templates/compose/pterodactyl-with-wings.yaml @@ -0,0 +1,140 @@ +# documentation: https://pterodactyl.io/ +# slogan: Pterodactyl is a free, open-source game server management panel +# tags: game, game server, management, panel, minecraft +# logo: svgs/pterodactyl.png +# port: 80, 8443 + +services: + mariadb: + image: mariadb:10.5 + healthcheck: + test: + ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized || exit 1"] + start_period: 10s + interval: 10s + timeout: 1s + retries: 3 + environment: + - MYSQL_ROOT_PASSWORD=$SERVICE_PASSWORD_MYSQLROOT + - MYSQL_DATABASE=pterodactyl-db + - MYSQL_USER=$SERVICE_USER_MYSQL + - MYSQL_PASSWORD=$SERVICE_PASSWORD_MYSQL + volumes: + - pterodactyl-db:/var/lib/mysql + + redis: + image: redis:alpine + healthcheck: + test: ["CMD-SHELL", "redis-cli ping || exit 1"] + interval: 10s + timeout: 1s + retries: 3 + + pterodactyl: + image: ghcr.io/pterodactyl/panel:latest + volumes: + - "panel-var:/app/var/" + - "panel-nginx:/etc/nginx/http.d/" + - "panel-certs:/etc/letsencrypt/" + - type: bind + source: ./etc/entrypoint.sh + target: /entrypoint.sh + mode: "0755" + content: | + #!/bin/sh + set -e + + echo "Setting logs permissions..." + chown -R nginx: /app/storage/logs/ + + USER_EXISTS=$(php artisan tinker --no-ansi --execute='echo \Pterodactyl\Models\User::where("email", "'"$ADMIN_EMAIL"'")->exists() ? "1" : "0";') + + if [ "$USER_EXISTS" = "0" ]; then + echo "Admin User does not exist, creating user now." + php artisan p:user:make --no-interaction \ + --admin=1 \ + --email="$ADMIN_EMAIL" \ + --username="$ADMIN_USERNAME" \ + --name-first="$ADMIN_FIRSTNAME" \ + --name-last="$ADMIN_LASTNAME" \ + --password="$ADMIN_PASSWORD" + echo "Admin user created successfully!" + else + echo "Admin User already exists, skipping creation." + fi + + exec supervisord --nodaemon + command: ["/entrypoint.sh"] + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:80 || exit 1"] + interval: 10s + timeout: 1s + retries: 3 + environment: + - SERVICE_FQDN_PTERODACTYL_80 + - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} + - ADMIN_USERNAME=${SERVICE_USER_ADMIN} + - ADMIN_FIRSTNAME=${ADMIN_FIRSTNAME:-Admin} + - ADMIN_LASTNAME=${ADMIN_LASTNAME:-User} + - ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN} + - PTERODACTYL_HTTPS=${PTERODACTYL_HTTPS:-false} + - APP_ENV=production + - APP_ENVIRONMENT_ONLY=false + - APP_URL=$SERVICE_FQDN_PTERODACTYL + - APP_TIMEZONE=${TIMEZONE:-UTC} + - APP_SERVICE_AUTHOR=${APP_SERVICE_AUTHOR:-author@example.com} + - LOG_LEVEL=${LOG_LEVEL:-debug} + - CACHE_DRIVER=redis + - SESSION_DRIVER=redis + - QUEUE_DRIVER=redis + - REDIS_HOST=redis + - DB_DATABASE=pterodactyl-db + - DB_USERNAME=$SERVICE_USER_MYSQL + - DB_HOST=mariadb + - DB_PORT=3306 + - DB_PASSWORD=$SERVICE_PASSWORD_MYSQL + - MAIL_FROM=$MAIL_FROM + - MAIL_DRIVER=$MAIL_DRIVER + - MAIL_HOST=$MAIL_HOST + - MAIL_PORT=$MAIL_PORT + - MAIL_USERNAME=$MAIL_USERNAME + - MAIL_PASSWORD=$MAIL_PASSWORD + - MAIL_ENCRYPTION=$MAIL_ENCRYPTION + + wings: + image: "ghcr.io/pterodactyl/wings:latest" + environment: + - SERVICE_FQDN_WINGS_8443 + - "TZ=${TIMEZONE:-UTC}" + - WINGS_USERNAME=$SERVICE_USER_WINGS + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "/var/lib/docker/containers/:/var/lib/docker/containers/" + - "/var/lib/pterodactyl/volumes:/var/lib/pterodactyl/volumes" + - "/tmp/pterodactyl:/tmp/pterodactyl" + - wings_lib:/var/lib/pterodactyl/ + - wings_logs:/var/log/pterodactyl/ + - type: bind + source: ./etc/config.yml + target: /etc/pterodactyl/config.yml + content: | + debug: false + uuid: ReplaceConfig + token_id: ReplaceConfig + token: ReplaceConfig + api: + host: 0.0.0.0 + port: 8443 # Warning, panel must have 443 as daemon port, while here it should should be 8443, FQDN in Coolify for this service should be https://*:8443 + ssl: + enabled: false + cert: ReplaceConfig + key: ReplaceConfig + upload_limit: 100 + system: + data: /var/lib/pterodactyl/volumes + sftp: + bind_port: 2022 + allowed_mounts: [] + remote: '' + ports: + - '2022:2022' \ No newline at end of file diff --git a/templates/compose/pterodactyl.yaml b/templates/compose/pterodactyl.yaml index ea64de47a..32939ad7e 100644 --- a/templates/compose/pterodactyl.yaml +++ b/templates/compose/pterodactyl.yaml @@ -1,4 +1,3 @@ -# ignore: true # documentation: https://pterodactyl.io/ # slogan: Pterodactyl is a free, open-source game server management panel # tags: game, game server, management, panel, minecraft @@ -8,8 +7,6 @@ services: mariadb: image: mariadb:10.5 - restart: unless-stopped - command: --default-authentication-plugin=mysql_native_password healthcheck: test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized || exit 1"] @@ -18,17 +15,15 @@ services: timeout: 1s retries: 3 environment: - - SERVICE_PASSWORD_MYSQL - MYSQL_ROOT_PASSWORD=$SERVICE_PASSWORD_MYSQLROOT - - MYSQL_DATABASE=panel - - MYSQL_USER=pterodactyl + - MYSQL_DATABASE=pterodactyl-db + - MYSQL_USER=$SERVICE_USER_MYSQL - MYSQL_PASSWORD=$SERVICE_PASSWORD_MYSQL volumes: - pterodactyl-db:/var/lib/mysql redis: image: redis:alpine - restart: unless-stopped healthcheck: test: ["CMD-SHELL", "redis-cli ping || exit 1"] interval: 10s @@ -37,7 +32,6 @@ services: pterodactyl: image: ghcr.io/pterodactyl/panel:latest - restart: unless-stopped volumes: - "panel-var:/app/var/" - "panel-nginx:/etc/nginx/http.d/" @@ -50,28 +44,26 @@ services: #!/bin/sh set -e - echo "Waiting for services to be ready..." - sleep 30 + echo "Setting logs permissions..." + chown -R nginx: /app/storage/logs/ - echo "Setting logs permissions..." - chown -R nginx: /app/storage/logs/ + USER_EXISTS=$(php artisan tinker --no-ansi --execute='echo \Pterodactyl\Models\User::where("email", "'"$ADMIN_EMAIL"'")->exists() ? "1" : "0";') - if ! php artisan p:user:list | grep -q "$ADMIN_EMAIL"; then - echo "Creating admin user..." - php artisan p:user:make --no-interaction \ - --admin=1 \ - --email="$ADMIN_EMAIL" \ - --username="$ADMIN_USERNAME" \ - --name-first="$ADMIN_FIRSTNAME" \ - --name-last="$ADMIN_LASTNAME" \ - --password="$ADMIN_PASSWORD" - echo "Admin user created" - else - echo "Admin user already exists, skipping creation" - fi - - exec supervisord -c --nodaemon + if [ "$USER_EXISTS" = "0" ]; then + echo "Admin User does not exist, creating user now." + php artisan p:user:make --no-interaction \ + --admin=1 \ + --email="$ADMIN_EMAIL" \ + --username="$ADMIN_USERNAME" \ + --name-first="$ADMIN_FIRSTNAME" \ + --name-last="$ADMIN_LASTNAME" \ + --password="$ADMIN_PASSWORD" + echo "Admin user created successfully!" + else + echo "Admin User already exists, skipping creation." + fi + exec supervisord --nodaemon command: ["/entrypoint.sh"] healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:80 || exit 1"] @@ -79,7 +71,7 @@ services: timeout: 1s retries: 3 environment: - - SERVICE_FQDN_PTERODACTYL + - SERVICE_FQDN_PTERODACTYL_80 - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} - ADMIN_USERNAME=${SERVICE_USER_ADMIN} - ADMIN_FIRSTNAME=${ADMIN_FIRSTNAME:-Admin} @@ -88,7 +80,7 @@ services: - PTERODACTYL_HTTPS=${PTERODACTYL_HTTPS:-false} - APP_ENV=production - APP_ENVIRONMENT_ONLY=false - - APP_URL=${PTERODACTYL_PUBLIC_FQDN:-$SERVICE_FQDN_PTERODACTYL} + - APP_URL=$SERVICE_FQDN_PTERODACTYL - APP_TIMEZONE=${TIMEZONE:-UTC} - APP_SERVICE_AUTHOR=${APP_SERVICE_AUTHOR:-author@example.com} - LOG_LEVEL=${LOG_LEVEL:-debug} @@ -96,6 +88,8 @@ services: - SESSION_DRIVER=redis - QUEUE_DRIVER=redis - REDIS_HOST=redis + - DB_DATABASE=pterodactyl-db + - DB_USERNAME=$SERVICE_USER_MYSQL - DB_HOST=mariadb - DB_PORT=3306 - DB_PASSWORD=$SERVICE_PASSWORD_MYSQL @@ -105,43 +99,4 @@ services: - MAIL_PORT=$MAIL_PORT - MAIL_USERNAME=$MAIL_USERNAME - MAIL_PASSWORD=$MAIL_PASSWORD - - MAIL_ENCRYPTION=$MAIL_ENCRYPTION - - wings: - image: ghcr.io/pterodactyl/wings:latest - restart: unless-stopped - environment: - - SERVICE_FQDN_WINGS_8080 - - TZ=${TIMEZONE:-UTC} - - WINGS_USERNAME=pterodactyl - volumes: - - "/var/run/docker.sock:/var/run/docker.sock" - - "/var/lib/docker/containers/:/var/lib/docker/containers/" - - "/var/lib/pterodactyl/:/var/lib/pterodactyl/" # See https://discord.com/channels/122900397965705216/493443725012500490/1272195151309045902 - - "/tmp/pterodactyl/:/tmp/pterodactyl/" # See https://discord.com/channels/122900397965705216/493443725012500490/1272195151309045902 - - "wings-logs:/var/log/pterodactyl/" - - - type: bind - source: ./etc/config.yml - target: /etc/pterodactyl/config.yml - content: | - docker: - network: - interface: 172.28.0.1 - dns: - - 1.1.1.1 - - 1.0.0.1 - name: pterodactyl_nw - ispn: false - driver: "" - network_mode: pterodactyl_nw - is_internal: false - enable_icc: true - network_mtu: 1500 - interfaces: - v4: - subnet: 172.28.0.0/16 - gateway: 172.28.0.1 - v6: - subnet: fdba:17c8:6c94::/64 - gateway: fdba:17c8:6c94::1011 + - MAIL_ENCRYPTION=$MAIL_ENCRYPTION \ No newline at end of file diff --git a/templates/compose/wings.yaml b/templates/compose/wings.yaml new file mode 100644 index 000000000..ce4a073f8 --- /dev/null +++ b/templates/compose/wings.yaml @@ -0,0 +1,44 @@ +# documentation: https://pterodactyl.io/ +# slogan: Wings is Pterodactyl's server control plane +# tags: game, game server, management, panel, minecraft +# logo: svgs/pterodactyl.png +# port: 8443 + +services: + wings: + image: "ghcr.io/pterodactyl/wings:latest" + environment: + - SERVICE_FQDN_WINGS_8443 + - "TZ=${TIMEZONE:-UTC}" + - WINGS_USERNAME=$SERVICE_USER_WINGS + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "/var/lib/docker/containers/:/var/lib/docker/containers/" + - "/var/lib/pterodactyl/volumes:/var/lib/pterodactyl/volumes" + - "/tmp/pterodactyl:/tmp/pterodactyl" + - wings_lib:/var/lib/pterodactyl/ + - wings_logs:/var/log/pterodactyl/ + - type: bind + source: ./etc/config.yml + target: /etc/pterodactyl/config.yml + content: | + debug: false + uuid: ReplaceConfig + token_id: ReplaceConfig + token: ReplaceConfig + api: + host: 0.0.0.0 + port: 8443 # Warning, panel must have 443 as daemon port, while here it should should be 8443, FQDN in Coolify for this service should be https://*:8443 + ssl: + enabled: false + cert: ReplaceConfig + key: ReplaceConfig + upload_limit: 100 + system: + data: /var/lib/pterodactyl/volumes + sftp: + bind_port: 2022 + allowed_mounts: [] + remote: '' + ports: + - '2022:2022' \ No newline at end of file From 9d7e4286b66076f305dbf1876dc192a9d14a4686 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 11 Aug 2025 19:25:59 +0200 Subject: [PATCH 0137/1717] Update service-templates.json --- templates/service-templates.json | 83 +++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/templates/service-templates.json b/templates/service-templates.json index 9f2c0a773..49884462f 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -685,7 +685,7 @@ "documenso": { "documentation": "https://docs.documenso.com/?utm_source=coolify.io", "slogan": "Document signing, finally open source", - "compose": "c2VydmljZXM6CiAgZG9jdW1lbnNvOgogICAgaW1hZ2U6IGRvY3VtZW5zby9kb2N1bWVuc28KICAgIGRlcGVuZHNfb246CiAgICAgIGRhdGFiYXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRE9DVU1FTlNPXzMwMDAKICAgICAgLSAnTkVYVEFVVEhfVVJMPSR7U0VSVklDRV9GUUROX0RPQ1VNRU5TT30nCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X0FVVEhTRUNSRVR9JwogICAgICAtICdORVhUX1BSSVZBVEVfRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX0JBU0U2NF9FTkNSWVBUSU9OS0VZfScKICAgICAgLSAnTkVYVF9QUklWQVRFX0VOQ1JZUFRJT05fU0VDT05EQVJZX0tFWT0ke1NFUlZJQ0VfQkFTRTY0X1NFQ09OREFSWUVOQ1JZUFRJT05LRVl9JwogICAgICAtICdORVhUX1BVQkxJQ19XRUJBUFBfVVJMPSR7U0VSVklDRV9GUUROX0RPQ1VNRU5TT30nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX1RSQU5TUE9SVD0ke05FWFRfUFJJVkFURV9TTVRQX1RSQU5TUE9SVH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0hPU1Q9JHtORVhUX1BSSVZBVEVfU01UUF9IT1NUfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfUE9SVD0ke05FWFRfUFJJVkFURV9TTVRQX1BPUlR9JwogICAgICAtICdORVhUX1BSSVZBVEVfU01UUF9VU0VSTkFNRT0ke05FWFRfUFJJVkFURV9TTVRQX1VTRVJOQU1FfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfUEFTU1dPUkQ9JHtORVhUX1BSSVZBVEVfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0ZST01fTkFNRT0ke05FWFRfUFJJVkFURV9TTVRQX0ZST01fTkFNRX0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0ZST01fQUREUkVTUz0ke05FWFRfUFJJVkFURV9TTVRQX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9EQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRhdGFiYXNlLyR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0/c2NoZW1hPXB1YmxpYycKICAgICAgLSAnTkVYVF9QUklWQVRFX0RJUkVDVF9EQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRhdGFiYXNlLyR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0/c2NoZW1hPXB1YmxpYycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAid2dldCAtcSAtTyAtIGh0dHA6Ly9kb2N1bWVuc286MzAwMC8gfCBncmVwIC1xICdTaWduIGluIHRvIHlvdXIgYWNjb3VudCciCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAKICBkYXRhYmFzZToKICAgIGltYWdlOiAncG9zdGdyZXM6MTcnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdkb2N1bWVuc29fcG9zdGdyZXNxbF9kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgZG9jdW1lbnNvOgogICAgaW1hZ2U6IGRvY3VtZW5zby9kb2N1bWVuc28KICAgIGRlcGVuZHNfb246CiAgICAgIGRhdGFiYXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRE9DVU1FTlNPXzMwMDAKICAgICAgLSAnTkVYVEFVVEhfVVJMPSR7U0VSVklDRV9GUUROX0RPQ1VNRU5TT30nCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X0FVVEhTRUNSRVR9JwogICAgICAtICdORVhUX1BSSVZBVEVfRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX0JBU0U2NF9FTkNSWVBUSU9OS0VZfScKICAgICAgLSAnTkVYVF9QUklWQVRFX0VOQ1JZUFRJT05fU0VDT05EQVJZX0tFWT0ke1NFUlZJQ0VfQkFTRTY0X1NFQ09OREFSWUVOQ1JZUFRJT05LRVl9JwogICAgICAtICdORVhUX1BVQkxJQ19XRUJBUFBfVVJMPSR7U0VSVklDRV9GUUROX0RPQ1VNRU5TT30nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX1RSQU5TUE9SVD0ke05FWFRfUFJJVkFURV9TTVRQX1RSQU5TUE9SVH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0hPU1Q9JHtORVhUX1BSSVZBVEVfU01UUF9IT1NUfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfUE9SVD0ke05FWFRfUFJJVkFURV9TTVRQX1BPUlR9JwogICAgICAtICdORVhUX1BSSVZBVEVfU01UUF9VU0VSTkFNRT0ke05FWFRfUFJJVkFURV9TTVRQX1VTRVJOQU1FfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfUEFTU1dPUkQ9JHtORVhUX1BSSVZBVEVfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0ZST01fTkFNRT0ke05FWFRfUFJJVkFURV9TTVRQX0ZST01fTkFNRX0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0ZST01fQUREUkVTUz0ke05FWFRfUFJJVkFURV9TTVRQX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9EQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRhdGFiYXNlLyR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0/c2NoZW1hPXB1YmxpYycKICAgICAgLSAnTkVYVF9QUklWQVRFX0RJUkVDVF9EQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRhdGFiYXNlLyR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0/c2NoZW1hPXB1YmxpYycKICAgICAgLSBORVhUX1BSSVZBVEVfU0lHTklOR19MT0NBTF9GSUxFX1BBVEg9L2FwcC9hcHBzL3JlbWl4L2NlcnRzL2NlcnRpZmljYXRlLnAxMgogICAgICAtICdORVhUX1BSSVZBVEVfU0lHTklOR19QQVNTUEhSQVNFPSR7U0VSVklDRV9QQVNTV09SRF9ET0NVTUVOU099JwogICAgICAtICdDRVJUX1ZBTElEX0RBWVM9JHtDRVJUX1ZBTElEX0RBWVM6LTM2NX0nCiAgICAgIC0gJ0NFUlRfSU5GT19DT1VOVFJZX05BTUU9JHtDRVJUX0lORk9fQ09VTlRSWV9OQU1FOi1ET30nCiAgICAgIC0gJ0NFUlRfSU5GT19TVEFURV9PUl9QUk9WSURFTkNFPSR7Q0VSVF9JTkZPX1NUQVRFX09SX1BST1ZJREVOQ0U6LVNhbnRpYWdvfScKICAgICAgLSAnQ0VSVF9JTkZPX0xPQ0FMSVRZX05BTUU9JHtDRVJUX0lORk9fTE9DQUxJVFlfTkFNRTotU2FudGlhZ299JwogICAgICAtICdDRVJUX0lORk9fT1JHQU5JWkFUSU9OX05BTUU9JHtDRVJUX0lORk9fT1JHQU5JWkFUSU9OX05BTUU6LUV4YW1wbGUgSU5DfScKICAgICAgLSAnQ0VSVF9JTkZPX09SR0FOSVpBVElPTkFMX1VOSVQ9JHtDRVJUX0lORk9fT1JHQU5JWkFUSU9OQUxfVU5JVDotSVQgRGVwYXJ0bWVudH0nCiAgICAgIC0gJ0NFUlRfSU5GT19FTUFJTD0ke0NFUlRfSU5GT19FTUFJTDotZXhhbXBsZUBnbWFpbC5jb219JwogICAgICAtICdORVhUX1BVQkxJQ19ESVNBQkxFX1NJR05VUD0ke0RJU0FCTEVfTE9HSU46LWZhbHNlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAid2dldCAtcSAtTyAtIGh0dHA6Ly9kb2N1bWVuc286MzAwMC8gfCBncmVwIC1xICdTaWduIGluIHRvIHlvdXIgYWNjb3VudCciCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAKICAgIGVudHJ5cG9pbnQ6CiAgICAgIC0gL2Jpbi9zaAogICAgICAtICctYycKICAgICAgLSAiZWNobyBcIi4vY2VydHNcIiA+IC90bXAvY2VydHNfZGlyX3BhdGhcbmVjaG8gXCIuL21ha2UtY2VydHMuc2hcIiA+IC90bXAvY2VydF9zY3JpcHRfcGF0aFxuZWNobyBcIiR7U0VSVklDRV9QQVNTV09SRF9ET0NVTUVOU099XCIgPiAvdG1wL2NlcnRfcGFzc1xuXG50b3VjaCAvdG1wL2NlcnRfaW5mb19wYXRoXG5jYXQgPDxFT0YgPiAvdG1wL2NlcnRfaW5mb19wYXRoXG5bIHJlcSBdXG5kaXN0aW5ndWlzaGVkX25hbWUgPSByZXFfZGlzdGluZ3Vpc2hlZF9uYW1lXG5wcm9tcHQgPSBub1xuWyByZXFfZGlzdGluZ3Vpc2hlZF9uYW1lIF1cbkMgICAgICAgICAgICA9ICR7Q0VSVF9JTkZPX0NPVU5UUllfTkFNRX1cblNUICAgICAgICAgICA9ICR7Q0VSVF9JTkZPX1NUQVRFX09SX1BST1ZJREVOQ0V9XG5MICAgICAgICAgICAgPSAke0NFUlRfSU5GT19MT0NBTElUWV9OQU1FfVxuTyAgICAgICAgICAgID0gJHtDRVJUX0lORk9fT1JHQU5JWkFUSU9OX05BTUV9XG5PVSAgICAgICAgICAgPSAke0NFUlRfSU5GT19PUkdBTklaQVRJT05BTF9VTklUfVxuQ04gICAgICAgICAgID0gJHtTRVJWSUNFX0ZRRE5fRE9DVU1FTlNPfVxuZW1haWxBZGRyZXNzID0gJHtDRVJUX0lORk9fRU1BSUx9XG5FT0ZcblxuY2F0IDw8RU9GID4gXCIkKGNhdCAvdG1wL2NlcnRfc2NyaXB0X3BhdGgpXCJcbm1rZGlyIC1wIFwiJChjYXQgL3RtcC9jZXJ0c19kaXJfcGF0aClcIiAmJiBjZCBcIiQoY2F0IC90bXAvY2VydHNfZGlyX3BhdGgpXCJcblxub3BlbnNzbCBnZW5yc2EgLW91dCBwcml2YXRlLmtleSAyMDQ4XG5cbm9wZW5zc2wgcmVxIFxcXG4gIC1uZXcgXFxcbiAgLXg1MDkgXFxcbiAgLWtleSBwcml2YXRlLmtleSBcXFxuICAtb3V0IGNlcnRpZmljYXRlLmNydCBcXFxuICAtZGF5cyAke0NFUlRfVkFMSURfREFZU30gXFxcbiAgLWNvbmZpZyAvdG1wL2NlcnRfaW5mb19wYXRoXG5cbm9wZW5zc2wgcGtjczEyIFxcXG4gIC1leHBvcnQgXFxcbiAgLW91dCBjZXJ0aWZpY2F0ZS5wMTIgXFxcbiAgLWlua2V5IHByaXZhdGUua2V5IFxcXG4gIC1pbiBjZXJ0aWZpY2F0ZS5jcnQgXFxcbiAgLWxlZ2FjeSBcXFxuICAtcGFzc3dvcmQgZmlsZTovdG1wL2NlcnRfcGFzc1xuRU9GXG5jaG1vZCAreCBcIiQoY2F0IC90bXAvY2VydF9zY3JpcHRfcGF0aClcIlxuXG5zaCBcIiQoY2F0IC90bXAvY2VydF9zY3JpcHRfcGF0aClcIlxuXG4uL3N0YXJ0LnNoXG4iCiAgZGF0YWJhc2U6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1kb2N1bWVuc28tZGJ9JwogICAgdm9sdW1lczoKICAgICAgLSAnZG9jdW1lbnNvX3Bvc3RncmVzcWxfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "signing", "opensource", @@ -1480,6 +1480,19 @@ "minversion": "0.0.0", "port": "7575" }, + "homebox": { + "documentation": "https://github.com/sysadminsmedia/homebox?utm_source=coolify.io", + "slogan": "Homebox is the inventory and organization system built for the Home User.", + "compose": "c2VydmljZXM6CiAgaG9tZWJveDoKICAgIGltYWdlOiAnZ2hjci5pby9zeXNhZG1pbnNtZWRpYS9ob21lYm94OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9IT01FQk9YXzc3NDUKICAgICAgLSAnSEJPWF9PUFRJT05TX0FMTE9XX1JFR0lTVFJBVElPTj0ke0hCT1hfT1BUSU9OU19BTExPV19SRUdJU1RSQVRJT046LWZhbHNlfScKICAgICAgLSAnSEJPWF9MT0dfTEVWRUw9JHtIQk9YX0xPR19MRVZFTDotaW5mb30nCiAgICAgIC0gJ0hCT1hfTE9HX0ZPUk1BVD0ke0hCT1hfTE9HX0ZPUk1BVDotdGV4dH0nCiAgICAgIC0gJ0hCT1hfV0VCX01BWF9VUExPQURfU0laRT0ke0hCT1hfV0VCX01BWF9VUExPQURfU0laRTotMTB9JwogICAgICAtICdIQk9YX01BSUxFUl9IT1NUPSR7SEJPWF9NQUlMRVJfSE9TVH0nCiAgICAgIC0gJ0hCT1hfTUFJTEVSX1BPUlQ9JHtIQk9YX01BSUxFUl9QT1JUOi01ODd9JwogICAgICAtICdIQk9YX01BSUxFUl9VU0VSTkFNRT0ke0hCT1hfTUFJTEVSX1VTRVJOQU1FfScKICAgICAgLSAnSEJPWF9NQUlMRVJfUEFTU1dPUkQ9JHtIQk9YX01BSUxFUl9QQVNTV09SRH0nCiAgICAgIC0gJ0hCT1hfTUFJTEVSX0ZST009JHtIQk9YX01BSUxFUl9GUk9NfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2hvbWVib3gtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBzaAogICAgICAgIC0gJy1jJwogICAgICAgIC0gJ3dnZXQgLS1tZXRob2Q9R0VUIC1xTy0gaHR0cDovL2xvY2FsaG9zdDo3NzQ1L2FwaS92MS9zdGF0dXMgPiAvZGV2L251bGwgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "inventory", + "home", + "organize" + ], + "logo": "svgs/homebox.svg", + "minversion": "0.0.0", + "port": "7745" + }, "homepage": { "documentation": "https://gethomepage.dev/latest/?utm_source=coolify.io", "slogan": "A modern, fully static, fast, secure fully proxied, highly customizable application dashboard", @@ -1806,6 +1819,27 @@ "minversion": "0.0.0", "port": "8080" }, + "librechat": { + "documentation": "https://docs.librechat.ai/install/configuration/dotenv.html?utm_source=coolify.io", + "slogan": "Self-hosted, powerful, and privacy-focused chat UI for multiple AI models", + "compose": "c2VydmljZXM6CiAgbGlicmVjaGF0OgogICAgaW1hZ2U6ICdnaGNyLmlvL2Rhbm55LWF2aWxhL2xpYnJlY2hhdC1kZXYtYXBpOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9MSUJSRUNIQVRfMzA4MAogICAgICAtICdET01BSU5fQ0xJRU5UPSR7U0VSVklDRV9GUUROX0xJQlJFQ0hBVH0nCiAgICAgIC0gJ0RPTUFJTl9TRVJWRVI9JHtTRVJWSUNFX0ZRRE5fTElCUkVDSEFUfScKICAgICAgLSBIT1NUPTAuMC4wLjAKICAgICAgLSBQT1JUPTMwODAKICAgICAgLSAnTU9OR09fVVJJPW1vbmdvZGI6Ly8ke1NFUlZJQ0VfVVNFUl9NT05HT306JHtTRVJWSUNFX1BBU1NXT1JEX01PTkdPfUBtb25nb2RiOjI3MDE3L2xpYnJlY2hhdD9hdXRoU291cmNlPWFkbWluJwogICAgICAtICdNRUlMSV9IT1NUPWh0dHA6Ly9tZWlsaXNlYXJjaDo3NzAwJwogICAgICAtICdNRUlMSV9NQVNURVJfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9NRUlMSX0nCiAgICAgIC0gUkFHX1BPUlQ9ODAwMAogICAgICAtICdSQUdfQVBJX1VSTD1odHRwOi8vcmFnLWFwaTo4MDAwJwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdKV1RfUkVGUkVTSF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0pXVH0nCiAgICAgIC0gJ0FQUF9USVRMRT0ke0FQUF9USVRMRTotTGlicmVDaGF0fScKICAgICAgLSAnQUxMT1dfRU1BSUxfTE9HSU49JHtBTExPV19FTUFJTF9MT0dJTjotdHJ1ZX0nCiAgICAgIC0gJ0FMTE9XX1JFR0lTVFJBVElPTj0ke0FMTE9XX1JFR0lTVFJBVElPTjotdHJ1ZX0nCiAgICAgIC0gJ0FMTE9XX1NPQ0lBTF9MT0dJTj0ke0FMTE9XX1NPQ0lBTF9MT0dJTjotZmFsc2V9JwogICAgICAtICdBTExPV19TT0NJQUxfUkVHSVNUUkFUSU9OPSR7QUxMT1dfU09DSUFMX1JFR0lTVFJBVElPTjotZmFsc2V9JwogICAgICAtICdBTExPV19QQVNTV09SRF9SRVNFVD0ke0FMTE9XX1BBU1NXT1JEX1JFU0VUOi1mYWxzZX0nCiAgICAgIC0gJ0FMTE9XX1VOVkVSSUZJRURfRU1BSUxfTE9HSU49JHtBTExPV19VTlZFUklGSUVEX0VNQUlMX0xPR0lOOi10cnVlfScKICAgICAgLSAnQ1JFRFNfS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9DUkVEU30nCiAgICAgIC0gJ0NSRURTX0lWPSR7U0VSVklDRV9QQVNTV09SRF9DUkVEU30nCiAgICAgIC0gJ0FOVEhST1BJQ19BUElfS0VZPSR7U0VSVklDRV9BTlRIUk9QSUNfQVBJX0tFWTotdXNlcl9wcm92aWRlZH0nCiAgICAgIC0gJ0dPT0dMRV9LRVk9JHtTRVJWSUNFX0dPT0dMRV9BUElfS0VZOi11c2VyX3Byb3ZpZGVkfScKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtTRVJWSUNFX09QRU5BSV9BUElfS0VZOi11c2VyX3Byb3ZpZGVkfScKICAgICAgLSAnQVNTSVNUQU5UU19BUElfS0VZPSR7U0VSVklDRV9BU1NJU1RBTlRTX0FQSV9LRVk6LXVzZXJfcHJvdmlkZWR9JwogICAgICAtICdERUJVR19MT0dHSU5HPSR7REVCVUdfTE9HR0lORzotZmFsc2V9JwogICAgICAtICdERUJVR19PUEVOQUk9JHtERUJVR19PUEVOQUk6LWZhbHNlfScKICAgICAgLSAnREVCVUdfUExVR0lOUz0ke0RFQlVHX09QRU5BSTotZmFsc2V9JwogICAgICAtICdOT19JTkRFWD0ke05PX0lOREVYOi10cnVlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xpYnJlY2hhdC1pbWFnZXM6L2FwcC9jbGllbnQvcHVibGljL2ltYWdlcycKICAgICAgLSAnbGlicmVjaGF0LWxvZ3M6L2FwcC9hcGkvbG9ncycKICAgICAgLSAnbGlicmVjaGF0LXVwbG9hZHM6L2FwcC91cGxvYWRzJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9saWJyZWNoYXQueWFtbAogICAgICAgIHRhcmdldDogL2FwcC9saWJyZWNoYXQueWFtbAogICAgICAgIGNvbnRlbnQ6ICJ2ZXJzaW9uOiAxLjIuOFxuIgogICAgZGVwZW5kc19vbjoKICAgICAgbW9uZ29kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBtZWlsaXNlYXJjaDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICB2ZWN0b3JkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByYWctYXBpOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDgwL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQogIG1vbmdvZGI6CiAgICBpbWFnZTogJ21vbmdvOjgnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTU9OR09fSU5JVERCX1JPT1RfVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfTU9OR099JwogICAgICAtICdNT05HT19JTklUREJfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTU9OR099JwogICAgdm9sdW1lczoKICAgICAgLSAnbW9uZ29kYi1kYXRhOi9kYXRhL2RiJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG1vbmdvc2gKICAgICAgICAtICctLWV2YWwnCiAgICAgICAgLSAiZGIucnVuQ29tbWFuZCgncGluZycpLm9rIgogICAgICAgIC0gJzEyNy4wLjAuMToyNzAxNy90ZXN0JwogICAgICAgIC0gJy0tcXVpZXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIG1laWxpc2VhcmNoOgogICAgaW1hZ2U6ICdnZXRtZWlsaS9tZWlsaXNlYXJjaDp2MS4xMi4zJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01FSUxJX01BU1RFUl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX01FSUxJfScKICAgICAgLSAnTUVJTElfTk9fQU5BTFlUSUNTPSR7TUVJTElfTk9fQU5BTFlUSUNTOi1mYWxzZX0nCiAgICAgIC0gTUVJTElfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnTUVJTElfSE9TVD1odHRwOi8vbWVpbGlzZWFyY2g6NzcwMCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21laWxpc2VhcmNoLWRhdGE6L21laWxpX2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NzcwMC9oZWFsdGgnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICB2ZWN0b3JkYjoKICAgIGltYWdlOiAnYW5rYW5lL3BndmVjdG9yOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0RCPXJhZwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSBQT1NUR1JFU19IT1NUX0FVVEhfTUVUSE9EPXRydXN0CiAgICB2b2x1bWVzOgogICAgICAtICd2ZWN0b3JkYi1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBwZ19pc3JlYWR5CiAgICAgICAgLSAnLS11c2VybmFtZT0kU0VSVklDRV9VU0VSX1BPU1RHUkVTJwogICAgICAgIC0gJy0taG9zdD0xMjcuMC4wLjEnCiAgICAgICAgLSAnLS1wb3J0PTU0MzInCiAgICAgICAgLSAnLS1kYm5hbWU9cmFnJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMW0KICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogIHJhZy1hcGk6CiAgICBpbWFnZTogJ2doY3IuaW8vZGFubnktYXZpbGEvbGlicmVjaGF0LXJhZy1hcGktZGV2LWxpdGU6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfREI9cmFnCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtIERCX0hPU1Q9dmVjdG9yZGIKICAgICAgLSAnREJfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gREJfTkFNRT1yYWcKICAgICAgLSBSQUdfUE9SVD04MDAwCiAgICAgIC0gJ1JBR19PUEVOQUlfQVBJX0tFWT0ke1NFUlZJQ0VfT1BFTkFJX0FQSV9LRVk6LXVzZXJfcHJvdmlkZWR9JwogICAgZGVwZW5kc19vbjoKICAgICAgdmVjdG9yZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBweXRob24KICAgICAgICAtICctYycKICAgICAgICAtICJpbXBvcnQgdXJsbGliLnJlcXVlc3Q7IHVybGxpYi5yZXF1ZXN0LnVybG9wZW4oJ2h0dHA6Ly8xMjcuMC4wLjE6ODAwMC9oZWFsdGgnKSIKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "ai", + "chat", + "gpt", + "claude", + "palm", + "openai", + "azure", + "huggingface", + "anthropic", + "ollama", + "llm" + ], + "logo": "svgs/librechat.svg", + "minversion": "0.0.0", + "port": "3080" + }, "libreoffice": { "documentation": "https://docs.linuxserver.io/images/docker-libreoffice/?utm_source=coolify.io", "slogan": "LibreOffice is a free and powerful office suite.", @@ -2715,7 +2749,7 @@ "penpot": { "documentation": "https://help.penpot.app/technical-guide/getting-started/#install-with-docker?utm_source=coolify.io", "slogan": "Penpot is the first Open Source design and prototyping platform for product teams.", - "compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9mcm9udGVuZDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtYXNzZXRzOi9vcHQvZGF0YS9hc3NldHMnCiAgICBkZXBlbmRzX29uOgogICAgICBwZW5wb3QtYmFja2VuZDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwZW5wb3QtZXhwb3J0ZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwCiAgICAgIC0gJ1BFTlBPVF9GTEFHUz0ke1BFTlBPVF9GUk9OVEVORF9GTEFHUzotZW5hYmxlLWxvZ2luLXdpdGgtcGFzc3dvcmR9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwZW5wb3QtYmFja2VuZDoKICAgIGltYWdlOiAncGVucG90YXBwL2JhY2tlbmQ6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LWFzc2V0czovb3B0L2RhdGEvYXNzZXRzJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtICdQRU5QT1RfRkxBR1M9JHtQRU5QT1RfQkFDS0VORF9GTEFHUzotZW5hYmxlLWxvZ2luLXdpdGgtcGFzc3dvcmQgZW5hYmxlLXNtdHAgZW5hYmxlLXByZXBsLXNlcnZlcn0nCiAgICAgIC0gUEVOUE9UX0hUVFBfU0VSVkVSX1BPUlQ9NjA2MAogICAgICAtIFBFTlBPVF9TRUNSRVRfS0VZPSRTRVJWSUNFX1JFQUxCQVNFNjRfNjRfUEVOUE9UCiAgICAgIC0gUEVOUE9UX1BVQkxJQ19VUkk9JFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwCiAgICAgIC0gJ1BFTlBPVF9CQUNLRU5EX1VSST1odHRwOi8vcGVucG90LWJhY2tlbmQnCiAgICAgIC0gJ1BFTlBPVF9FWFBPUlRFUl9VUkk9aHR0cDovL3BlbnBvdC1leHBvcnRlcicKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1VSST1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXMvJHtQT1NUR1JFU19EQjotcGVucG90fScKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BFTlBPVF9SRURJU19VUkk9cmVkaXM6Ly9yZWRpcy8wJwogICAgICAtIFBFTlBPVF9BU1NFVFNfU1RPUkFHRV9CQUNLRU5EPWFzc2V0cy1mcwogICAgICAtIFBFTlBPVF9TVE9SQUdFX0FTU0VUU19GU19ESVJFQ1RPUlk9L29wdC9kYXRhL2Fzc2V0cwogICAgICAtICdQRU5QT1RfVEVMRU1FVFJZX0VOQUJMRUQ9JHtQRU5QT1RfVEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnUEVOUE9UX1NNVFBfREVGQVVMVF9GUk9NPSR7UEVOUE9UX1NNVFBfREVGQVVMVF9GUk9NOi1uby1yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0RFRkFVTFRfUkVQTFlfVE89JHtQRU5QT1RfU01UUF9ERUZBVUxUX1JFUExZX1RPOi1uby1yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0hPU1Q9JHtQRU5QT1RfU01UUF9IT1NUOi1tYWlscGl0fScKICAgICAgLSAnUEVOUE9UX1NNVFBfUE9SVD0ke1BFTlBPVF9TTVRQX1BPUlQ6LTEwMjV9JwogICAgICAtICdQRU5QT1RfU01UUF9VU0VSTkFNRT0ke1BFTlBPVF9TTVRQX1VTRVJOQU1FOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfU01UUF9QQVNTV09SRD0ke1BFTlBPVF9TTVRQX1BBU1NXT1JEOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfU01UUF9UTFM9JHtQRU5QT1RfU01UUF9UTFM6LWZhbHNlfScKICAgICAgLSAnUEVOUE9UX1NNVFBfU1NMPSR7UEVOUE9UX1NNVFBfU1NMOi1mYWxzZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NjA2MC9yZWFkeXonCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMzBzCiAgICAgIHJldHJpZXM6IDE1CiAgcGVucG90LWV4cG9ydGVyOgogICAgaW1hZ2U6ICdwZW5wb3RhcHAvZXhwb3J0ZXI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUEVOUE9UX1BVQkxJQ19VUkk9JFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwCiAgICAgIC0gJ1BFTlBPVF9SRURJU19VUkk9cmVkaXM6Ly9yZWRpcy8wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjYwNjEvcmVhZHl6JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbWFpbHBpdDoKICAgIGltYWdlOiAnYXhsbGVudC9tYWlscGl0OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NQUlMUElUXzgwMjUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvbWFpbHBpdAogICAgICAgIC0gcmVhZHl6CiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0lOSVREQl9BUkdTPS0tZGF0YS1jaGVja3N1bXMKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wZW5wb3R9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LWFscGluZScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1hcHBlbmRvbmx5IHllcycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BlbnBvdC1yZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9mcm9udGVuZDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtYXNzZXRzOi9vcHQvZGF0YS9hc3NldHMnCiAgICBkZXBlbmRzX29uOgogICAgICBwZW5wb3QtYmFja2VuZDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwZW5wb3QtZXhwb3J0ZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwCiAgICAgIC0gJ1BFTlBPVF9GTEFHUz0ke1BFTlBPVF9GUk9OVEVORF9GTEFHUzotZW5hYmxlLWxvZ2luLXdpdGgtcGFzc3dvcmR9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwZW5wb3QtYmFja2VuZDoKICAgIGltYWdlOiAncGVucG90YXBwL2JhY2tlbmQ6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LWFzc2V0czovb3B0L2RhdGEvYXNzZXRzJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtICdQRU5QT1RfRkxBR1M9JHtQRU5QT1RfQkFDS0VORF9GTEFHUzotZW5hYmxlLWxvZ2luLXdpdGgtcGFzc3dvcmQgZW5hYmxlLXNtdHAgZW5hYmxlLXByZXBsLXNlcnZlcn0nCiAgICAgIC0gUEVOUE9UX0hUVFBfU0VSVkVSX1BPUlQ9NjA2MAogICAgICAtIFBFTlBPVF9TRUNSRVRfS0VZPSRTRVJWSUNFX1JFQUxCQVNFNjRfNjRfUEVOUE9UCiAgICAgIC0gUEVOUE9UX1BVQkxJQ19VUkk9JFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwCiAgICAgIC0gJ1BFTlBPVF9CQUNLRU5EX1VSST1odHRwOi8vcGVucG90LWJhY2tlbmQnCiAgICAgIC0gJ1BFTlBPVF9FWFBPUlRFUl9VUkk9aHR0cDovL3BlbnBvdC1leHBvcnRlcicKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1VSST1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXMvJHtQT1NUR1JFU19EQjotcGVucG90fScKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BFTlBPVF9SRURJU19VUkk9cmVkaXM6Ly9yZWRpcy8wJwogICAgICAtIFBFTlBPVF9BU1NFVFNfU1RPUkFHRV9CQUNLRU5EPWFzc2V0cy1mcwogICAgICAtIFBFTlBPVF9TVE9SQUdFX0FTU0VUU19GU19ESVJFQ1RPUlk9L29wdC9kYXRhL2Fzc2V0cwogICAgICAtICdQRU5QT1RfVEVMRU1FVFJZX0VOQUJMRUQ9JHtQRU5QT1RfVEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnUEVOUE9UX1NNVFBfREVGQVVMVF9GUk9NPSR7UEVOUE9UX1NNVFBfREVGQVVMVF9GUk9NOi1uby1yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0RFRkFVTFRfUkVQTFlfVE89JHtQRU5QT1RfU01UUF9ERUZBVUxUX1JFUExZX1RPOi1uby1yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0hPU1Q9JHtQRU5QT1RfU01UUF9IT1NUOi1tYWlscGl0fScKICAgICAgLSAnUEVOUE9UX1NNVFBfUE9SVD0ke1BFTlBPVF9TTVRQX1BPUlQ6LTEwMjV9JwogICAgICAtICdQRU5QT1RfU01UUF9VU0VSTkFNRT0ke1BFTlBPVF9TTVRQX1VTRVJOQU1FOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfU01UUF9QQVNTV09SRD0ke1BFTlBPVF9TTVRQX1BBU1NXT1JEOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfU01UUF9UTFM9JHtQRU5QT1RfU01UUF9UTFM6LWZhbHNlfScKICAgICAgLSAnUEVOUE9UX1NNVFBfU1NMPSR7UEVOUE9UX1NNVFBfU1NMOi1mYWxzZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gInJlcXVpcmUoJ2h0dHAnKS5nZXQoe2hvc3Q6JzEyNy4wLjAuMScsIHBvcnQ6NjA2MCwgcGF0aDonL3JlYWR5eid9LCByZXMgPT4gcHJvY2Vzcy5leGl0KHJlcy5zdGF0dXNDb2RlPT09MjAwID8gMCA6IDEpKS5vbignZXJyb3InLCAoKSA9PiBwcm9jZXNzLmV4aXQoMSkpOyIKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogMTUKICBwZW5wb3QtZXhwb3J0ZXI6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9leHBvcnRlcjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQRU5QT1RfUFVCTElDX1VSST0kU0VSVklDRV9GUUROX0ZST05URU5EXzgwODAKICAgICAgLSAnUEVOUE9UX1JFRElTX1VSST1yZWRpczovL3JlZGlzLzAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NjA2MS9yZWFkeXonCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBtYWlscGl0OgogICAgaW1hZ2U6ICdheGxsZW50L21haWxwaXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01BSUxQSVRfODAyNQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9tYWlscGl0CiAgICAgICAgLSByZWFkeXoKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BlbnBvdC1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfSU5JVERCX0FSR1M9LS1kYXRhLWNoZWNrc3VtcwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXBlbnBvdH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjctYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzJwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", "tags": [ "penpot", "design", @@ -2949,6 +2983,36 @@ "minversion": "0.0.0", "port": "9696" }, + "pterodactyl-with-wings": { + "documentation": "https://pterodactyl.io/?utm_source=coolify.io", + "slogan": "Pterodactyl is a free, open-source game server management panel", + "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMC41JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdwYW5lbC12YXI6L2FwcC92YXIvJwogICAgICAtICdwYW5lbC1uZ2lueDovZXRjL25naW54L2h0dHAuZC8nCiAgICAgIC0gJ3BhbmVsLWNlcnRzOi9ldGMvbGV0c2VuY3J5cHQvJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9ldGMvZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBtb2RlOiAnMDc1NScKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG5zZXQgLWVcblxuIGVjaG8gXCJTZXR0aW5nIGxvZ3MgcGVybWlzc2lvbnMuLi5cIlxuIGNob3duIC1SIG5naW54OiAvYXBwL3N0b3JhZ2UvbG9ncy9cblxuIFVTRVJfRVhJU1RTPSQocGhwIGFydGlzYW4gdGlua2VyIC0tbm8tYW5zaSAtLWV4ZWN1dGU9J2VjaG8gXFxQdGVyb2RhY3R5bFxcTW9kZWxzXFxVc2VyOjp3aGVyZShcImVtYWlsXCIsIFwiJ1wiJEFETUlOX0VNQUlMXCInXCIpLT5leGlzdHMoKSA/IFwiMVwiIDogXCIwXCI7JylcblxuIGlmIFsgXCIkVVNFUl9FWElTVFNcIiA9IFwiMFwiIF07IHRoZW5cbiAgIGVjaG8gXCJBZG1pbiBVc2VyIGRvZXMgbm90IGV4aXN0LCBjcmVhdGluZyB1c2VyIG5vdy5cIlxuICAgcGhwIGFydGlzYW4gcDp1c2VyOm1ha2UgLS1uby1pbnRlcmFjdGlvbiBcXFxuICAgICAtLWFkbWluPTEgXFxcbiAgICAgLS1lbWFpbD1cIiRBRE1JTl9FTUFJTFwiIFxcXG4gICAgIC0tdXNlcm5hbWU9XCIkQURNSU5fVVNFUk5BTUVcIiBcXFxuICAgICAtLW5hbWUtZmlyc3Q9XCIkQURNSU5fRklSU1ROQU1FXCIgXFxcbiAgICAgLS1uYW1lLWxhc3Q9XCIkQURNSU5fTEFTVE5BTUVcIiBcXFxuICAgICAtLXBhc3N3b3JkPVwiJEFETUlOX1BBU1NXT1JEXCJcbiAgIGVjaG8gXCJBZG1pbiB1c2VyIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5IVwiXG4gZWxzZVxuICAgZWNobyBcIkFkbWluIFVzZXIgYWxyZWFkeSBleGlzdHMsIHNraXBwaW5nIGNyZWF0aW9uLlwiXG4gZmlcblxuIGV4ZWMgc3VwZXJ2aXNvcmQgLS1ub2RhZW1vblxuIgogICAgY29tbWFuZDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdjdXJsIC1zZiBodHRwOi8vbG9jYWxob3N0OjgwIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAxcwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUFRFUk9EQUNUWUxfODAKICAgICAgLSAnQURNSU5fRU1BSUw9JHtBRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdBRE1JTl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0FETUlOX0ZJUlNUTkFNRT0ke0FETUlOX0ZJUlNUTkFNRTotQWRtaW59JwogICAgICAtICdBRE1JTl9MQVNUTkFNRT0ke0FETUlOX0xBU1ROQU1FOi1Vc2VyfScKICAgICAgLSAnQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSAnUFRFUk9EQUNUWUxfSFRUUFM9JHtQVEVST0RBQ1RZTF9IVFRQUzotZmFsc2V9JwogICAgICAtIEFQUF9FTlY9cHJvZHVjdGlvbgogICAgICAtIEFQUF9FTlZJUk9OTUVOVF9PTkxZPWZhbHNlCiAgICAgIC0gQVBQX1VSTD0kU0VSVklDRV9GUUROX1BURVJPREFDVFlMCiAgICAgIC0gJ0FQUF9USU1FWk9ORT0ke1RJTUVaT05FOi1VVEN9JwogICAgICAtICdBUFBfU0VSVklDRV9BVVRIT1I9JHtBUFBfU0VSVklDRV9BVVRIT1I6LWF1dGhvckBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0xPR19MRVZFTD0ke0xPR19MRVZFTDotZGVidWd9JwogICAgICAtIENBQ0hFX0RSSVZFUj1yZWRpcwogICAgICAtIFNFU1NJT05fRFJJVkVSPXJlZGlzCiAgICAgIC0gUVVFVUVfRFJJVkVSPXJlZGlzCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIERCX0RBVEFCQVNFPXB0ZXJvZGFjdHlsLWRiCiAgICAgIC0gREJfVVNFUk5BTUU9JFNFUlZJQ0VfVVNFUl9NWVNRTAogICAgICAtIERCX0hPU1Q9bWFyaWFkYgogICAgICAtIERCX1BPUlQ9MzMwNgogICAgICAtIERCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIC0gTUFJTF9GUk9NPSRNQUlMX0ZST00KICAgICAgLSBNQUlMX0RSSVZFUj0kTUFJTF9EUklWRVIKICAgICAgLSBNQUlMX0hPU1Q9JE1BSUxfSE9TVAogICAgICAtIE1BSUxfUE9SVD0kTUFJTF9QT1JUCiAgICAgIC0gTUFJTF9VU0VSTkFNRT0kTUFJTF9VU0VSTkFNRQogICAgICAtIE1BSUxfUEFTU1dPUkQ9JE1BSUxfUEFTU1dPUkQKICAgICAgLSBNQUlMX0VOQ1JZUFRJT049JE1BSUxfRU5DUllQVElPTgogIHdpbmdzOgogICAgaW1hZ2U6ICdnaGNyLmlvL3B0ZXJvZGFjdHlsL3dpbmdzOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XSU5HU184NDQzCiAgICAgIC0gJ1RaPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gV0lOR1NfVVNFUk5BTUU9JFNFUlZJQ0VfVVNFUl9XSU5HUwogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJy92YXIvbGliL2RvY2tlci9jb250YWluZXJzLzovdmFyL2xpYi9kb2NrZXIvY29udGFpbmVycy8nCiAgICAgIC0gJy92YXIvbGliL3B0ZXJvZGFjdHlsL3ZvbHVtZXM6L3Zhci9saWIvcHRlcm9kYWN0eWwvdm9sdW1lcycKICAgICAgLSAnL3RtcC9wdGVyb2RhY3R5bDovdG1wL3B0ZXJvZGFjdHlsJwogICAgICAtICd3aW5nc19saWI6L3Zhci9saWIvcHRlcm9kYWN0eWwvJwogICAgICAtICd3aW5nc19sb2dzOi92YXIvbG9nL3B0ZXJvZGFjdHlsLycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2NvbmZpZy55bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvcHRlcm9kYWN0eWwvY29uZmlnLnltbAogICAgICAgIGNvbnRlbnQ6ICJkZWJ1ZzogZmFsc2VcbnV1aWQ6IFJlcGxhY2VDb25maWdcbnRva2VuX2lkOiBSZXBsYWNlQ29uZmlnXG50b2tlbjogUmVwbGFjZUNvbmZpZ1xuYXBpOlxuICBob3N0OiAwLjAuMC4wXG4gIHBvcnQ6IDg0NDMgIyBXYXJuaW5nLCBwYW5lbCBtdXN0IGhhdmUgNDQzIGFzIGRhZW1vbiBwb3J0LCB3aGlsZSBoZXJlIGl0IHNob3VsZCBzaG91bGQgYmUgODQ0MywgRlFETiBpbiBDb29saWZ5IGZvciB0aGlzIHNlcnZpY2Ugc2hvdWxkIGJlIGh0dHBzOi8vKjo4NDQzXG4gIHNzbDpcbiAgICBlbmFibGVkOiBmYWxzZVxuICAgIGNlcnQ6IFJlcGxhY2VDb25maWdcbiAgICBrZXk6IFJlcGxhY2VDb25maWdcbiAgdXBsb2FkX2xpbWl0OiAxMDBcbnN5c3RlbTpcbiAgZGF0YTogL3Zhci9saWIvcHRlcm9kYWN0eWwvdm9sdW1lc1xuICBzZnRwOlxuICAgIGJpbmRfcG9ydDogMjAyMlxuYWxsb3dlZF9tb3VudHM6IFtdXG5yZW1vdGU6ICcnIgogICAgcG9ydHM6CiAgICAgIC0gJzIwMjI6MjAyMicK", + "tags": [ + "game", + "game server", + "management", + "panel", + "minecraft" + ], + "logo": "svgs/pterodactyl.png", + "minversion": "0.0.0", + "port": "80, 8443" + }, + "pterodactyl": { + "documentation": "https://pterodactyl.io/?utm_source=coolify.io", + "slogan": "Pterodactyl is a free, open-source game server management panel", + "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMC41JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdwYW5lbC12YXI6L2FwcC92YXIvJwogICAgICAtICdwYW5lbC1uZ2lueDovZXRjL25naW54L2h0dHAuZC8nCiAgICAgIC0gJ3BhbmVsLWNlcnRzOi9ldGMvbGV0c2VuY3J5cHQvJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9ldGMvZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBtb2RlOiAnMDc1NScKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG5zZXQgLWVcblxuIGVjaG8gXCJTZXR0aW5nIGxvZ3MgcGVybWlzc2lvbnMuLi5cIlxuIGNob3duIC1SIG5naW54OiAvYXBwL3N0b3JhZ2UvbG9ncy9cblxuIFVTRVJfRVhJU1RTPSQocGhwIGFydGlzYW4gdGlua2VyIC0tbm8tYW5zaSAtLWV4ZWN1dGU9J2VjaG8gXFxQdGVyb2RhY3R5bFxcTW9kZWxzXFxVc2VyOjp3aGVyZShcImVtYWlsXCIsIFwiJ1wiJEFETUlOX0VNQUlMXCInXCIpLT5leGlzdHMoKSA/IFwiMVwiIDogXCIwXCI7JylcblxuIGlmIFsgXCIkVVNFUl9FWElTVFNcIiA9IFwiMFwiIF07IHRoZW5cbiAgIGVjaG8gXCJBZG1pbiBVc2VyIGRvZXMgbm90IGV4aXN0LCBjcmVhdGluZyB1c2VyIG5vdy5cIlxuICAgcGhwIGFydGlzYW4gcDp1c2VyOm1ha2UgLS1uby1pbnRlcmFjdGlvbiBcXFxuICAgICAtLWFkbWluPTEgXFxcbiAgICAgLS1lbWFpbD1cIiRBRE1JTl9FTUFJTFwiIFxcXG4gICAgIC0tdXNlcm5hbWU9XCIkQURNSU5fVVNFUk5BTUVcIiBcXFxuICAgICAtLW5hbWUtZmlyc3Q9XCIkQURNSU5fRklSU1ROQU1FXCIgXFxcbiAgICAgLS1uYW1lLWxhc3Q9XCIkQURNSU5fTEFTVE5BTUVcIiBcXFxuICAgICAtLXBhc3N3b3JkPVwiJEFETUlOX1BBU1NXT1JEXCJcbiAgIGVjaG8gXCJBZG1pbiB1c2VyIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5IVwiXG4gZWxzZVxuICAgZWNobyBcIkFkbWluIFVzZXIgYWxyZWFkeSBleGlzdHMsIHNraXBwaW5nIGNyZWF0aW9uLlwiXG4gZmlcblxuIGV4ZWMgc3VwZXJ2aXNvcmQgLS1ub2RhZW1vblxuIgogICAgY29tbWFuZDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdjdXJsIC1zZiBodHRwOi8vbG9jYWxob3N0OjgwIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAxcwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUFRFUk9EQUNUWUxfODAKICAgICAgLSAnQURNSU5fRU1BSUw9JHtBRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdBRE1JTl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0FETUlOX0ZJUlNUTkFNRT0ke0FETUlOX0ZJUlNUTkFNRTotQWRtaW59JwogICAgICAtICdBRE1JTl9MQVNUTkFNRT0ke0FETUlOX0xBU1ROQU1FOi1Vc2VyfScKICAgICAgLSAnQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSAnUFRFUk9EQUNUWUxfSFRUUFM9JHtQVEVST0RBQ1RZTF9IVFRQUzotZmFsc2V9JwogICAgICAtIEFQUF9FTlY9cHJvZHVjdGlvbgogICAgICAtIEFQUF9FTlZJUk9OTUVOVF9PTkxZPWZhbHNlCiAgICAgIC0gQVBQX1VSTD0kU0VSVklDRV9GUUROX1BURVJPREFDVFlMCiAgICAgIC0gJ0FQUF9USU1FWk9ORT0ke1RJTUVaT05FOi1VVEN9JwogICAgICAtICdBUFBfU0VSVklDRV9BVVRIT1I9JHtBUFBfU0VSVklDRV9BVVRIT1I6LWF1dGhvckBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0xPR19MRVZFTD0ke0xPR19MRVZFTDotZGVidWd9JwogICAgICAtIENBQ0hFX0RSSVZFUj1yZWRpcwogICAgICAtIFNFU1NJT05fRFJJVkVSPXJlZGlzCiAgICAgIC0gUVVFVUVfRFJJVkVSPXJlZGlzCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIERCX0RBVEFCQVNFPXB0ZXJvZGFjdHlsLWRiCiAgICAgIC0gREJfVVNFUk5BTUU9JFNFUlZJQ0VfVVNFUl9NWVNRTAogICAgICAtIERCX0hPU1Q9bWFyaWFkYgogICAgICAtIERCX1BPUlQ9MzMwNgogICAgICAtIERCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIC0gTUFJTF9GUk9NPSRNQUlMX0ZST00KICAgICAgLSBNQUlMX0RSSVZFUj0kTUFJTF9EUklWRVIKICAgICAgLSBNQUlMX0hPU1Q9JE1BSUxfSE9TVAogICAgICAtIE1BSUxfUE9SVD0kTUFJTF9QT1JUCiAgICAgIC0gTUFJTF9VU0VSTkFNRT0kTUFJTF9VU0VSTkFNRQogICAgICAtIE1BSUxfUEFTU1dPUkQ9JE1BSUxfUEFTU1dPUkQKICAgICAgLSBNQUlMX0VOQ1JZUFRJT049JE1BSUxfRU5DUllQVElPTgo=", + "tags": [ + "game", + "game server", + "management", + "panel", + "minecraft" + ], + "logo": "svgs/pterodactyl.png", + "minversion": "0.0.0", + "port": "80" + }, "qbittorrent": { "documentation": "https://docs.linuxserver.io/images/docker-qbittorrent/?utm_source=coolify.io", "slogan": "The qBittorrent project aims to provide an open-source software alternative to \u03bcTorrent.", @@ -3793,6 +3857,21 @@ "minversion": "0.0.0", "port": "8000" }, + "wings": { + "documentation": "https://pterodactyl.io/?utm_source=coolify.io", + "slogan": "Wings is Pterodactyl's server control plane", + "compose": "c2VydmljZXM6CiAgd2luZ3M6CiAgICBpbWFnZTogJ2doY3IuaW8vcHRlcm9kYWN0eWwvd2luZ3M6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1dJTkdTXzg0NDMKICAgICAgLSAnVFo9JHtUSU1FWk9ORTotVVRDfScKICAgICAgLSBXSU5HU19VU0VSTkFNRT0kU0VSVklDRV9VU0VSX1dJTkdTCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnL3Zhci9saWIvZG9ja2VyL2NvbnRhaW5lcnMvOi92YXIvbGliL2RvY2tlci9jb250YWluZXJzLycKICAgICAgLSAnL3Zhci9saWIvcHRlcm9kYWN0eWwvdm9sdW1lczovdmFyL2xpYi9wdGVyb2RhY3R5bC92b2x1bWVzJwogICAgICAtICcvdG1wL3B0ZXJvZGFjdHlsOi90bXAvcHRlcm9kYWN0eWwnCiAgICAgIC0gJ3dpbmdzX2xpYjovdmFyL2xpYi9wdGVyb2RhY3R5bC8nCiAgICAgIC0gJ3dpbmdzX2xvZ3M6L3Zhci9sb2cvcHRlcm9kYWN0eWwvJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9ldGMvY29uZmlnLnltbAogICAgICAgIHRhcmdldDogL2V0Yy9wdGVyb2RhY3R5bC9jb25maWcueW1sCiAgICAgICAgY29udGVudDogImRlYnVnOiBmYWxzZVxudXVpZDogUmVwbGFjZUNvbmZpZ1xudG9rZW5faWQ6IFJlcGxhY2VDb25maWdcbnRva2VuOiBSZXBsYWNlQ29uZmlnXG5hcGk6XG4gIGhvc3Q6IDAuMC4wLjBcbiAgcG9ydDogODQ0MyAjIFdhcm5pbmcsIHBhbmVsIG11c3QgaGF2ZSA0NDMgYXMgZGFlbW9uIHBvcnQsIHdoaWxlIGhlcmUgaXQgc2hvdWxkIHNob3VsZCBiZSA4NDQzLCBGUUROIGluIENvb2xpZnkgZm9yIHRoaXMgc2VydmljZSBzaG91bGQgYmUgaHR0cHM6Ly8qOjg0NDNcbiAgc3NsOlxuICAgIGVuYWJsZWQ6IGZhbHNlXG4gICAgY2VydDogUmVwbGFjZUNvbmZpZ1xuICAgIGtleTogUmVwbGFjZUNvbmZpZ1xuICB1cGxvYWRfbGltaXQ6IDEwMFxuc3lzdGVtOlxuICBkYXRhOiAvdmFyL2xpYi9wdGVyb2RhY3R5bC92b2x1bWVzXG4gIHNmdHA6XG4gICAgYmluZF9wb3J0OiAyMDIyXG5hbGxvd2VkX21vdW50czogW11cbnJlbW90ZTogJyciCiAgICBwb3J0czoKICAgICAgLSAnMjAyMjoyMDIyJwo=", + "tags": [ + "game", + "game server", + "management", + "panel", + "minecraft" + ], + "logo": "svgs/pterodactyl.png", + "minversion": "0.0.0", + "port": "8443" + }, "wireguard-easy": { "documentation": "https://github.com/wg-easy/wg-easy?utm_source=coolify.io", "slogan": "The easiest way to run WireGuard VPN + Web-based Admin UI.", From 8eb3f94644908c6b80ce0d07fdb34333668ad769 Mon Sep 17 00:00:00 2001 From: Scan <103391616+scanash00@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:55:35 -0800 Subject: [PATCH 0138/1717] feat(service): add Bluesky PDS template (#6302) --- public/svgs/bluesky.svg | 3 ++ templates/compose/bluesky-pds.yaml | 72 +++++++++++++++++++++--------- 2 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 public/svgs/bluesky.svg diff --git a/public/svgs/bluesky.svg b/public/svgs/bluesky.svg new file mode 100644 index 000000000..77ebea072 --- /dev/null +++ b/public/svgs/bluesky.svg @@ -0,0 +1,3 @@ + + + diff --git a/templates/compose/bluesky-pds.yaml b/templates/compose/bluesky-pds.yaml index 679eb8e79..7d2804421 100644 --- a/templates/compose/bluesky-pds.yaml +++ b/templates/compose/bluesky-pds.yaml @@ -1,34 +1,64 @@ -# ignore: true # documentation: https://github.com/bluesky-social/pds -# slogan: A social network for the decentralized web -# tags: pds, bluesky, social, network, decentralized -# logo: +# slogan: Bluesky PDS (Personal Data Server) +# tags: bluesky, pds, platform +# logo: svgs/bluesky.svg # port: 3000 services: pds: - image: ghcr.io/bluesky-social/pds:0.4 + image: 'ghcr.io/bluesky-social/pds:latest' volumes: - - pds-data:/pds + - ./pds-data:/pds environment: - SERVICE_FQDN_PDS_3000 - - PDS_JWT_SECRET=${SERVICE_BASE64_PDS} - - PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_PDS} - - PDS_ADMIN_EMAIL=${PDS_ADMIN_EMAIL:-admin@example.com} - - PDS_DATADIR=${PDS_DATADIR:-/pds} - - PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATADIR:-/pds}/blocks - - PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-52428800} - PDS_HOSTNAME=${SERVICE_URL_PDS} - - PDS_DID_PLC_URL=https://plc.directory - - PDS_BSKY_APP_VIEW_URL=https://api.bsky.app - - PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app - - PDS_REPORT_SERVICE_URL=https://mod.bsky.app - - PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac - - PDS_CRAWLERS=https://bsky.network + - PDS_JWT_SECRET=${SERVICE_PASSWORD_JWT_SECRET} + - PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN} + - PDS_ADMIN_EMAIL=${SERVICE_EMAIL_ADMIN} + - PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX} + - PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds} + - PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATA_DIRECTORY:-/pds}/blocks + - PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-52428800} + - PDS_DID_PLC_URL=${PDS_DID_PLC_URL:-https://plc.directory} + - PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL:-https://api.bsky.app} + - PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID:-did:web:api.bsky.app} + - PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL:-https://mod.bsky.app/xrpc/com.atproto.moderation.createReport} + - PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID:-did:plc:ar7c4by46qjdydhdevvrndac} + - PDS_CRAWLERS=${PDS_CRAWLERS:-https://bsky.network} - LOG_ENABLED=${LOG_ENABLED:-true} - - PDS_EMAIL_SMTP_URL=${PDS_EMAIL_SMTP_URL:-smtp://localhost:8025} - - PDS_EMAIL_FROM_ADDRESS=${PDS_EMAIL_FROM_ADDRESS:-admin@example.com} - - PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${SERVICE_HEX_32_ROTATIONKEY} + + command: > + sh -c ' + echo "Installing curl, bash, and pdsadmin..." + apk add --no-cache curl bash && \ + curl -o /usr/local/bin/pdsadmin.sh https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin.sh && \ + chmod +x /usr/local/bin/pdsadmin.sh && \ + ln -sf /usr/local/bin/pdsadmin.sh /usr/local/bin/pdsadmin + + echo "Generating /pds/pds.env..." + printf "%s\n" \ + "SERVICE_FQDN_PDS_3000=$${SERVICE_FQDN_PDS_3000}" \ + "PDS_HOSTNAME=$${PDS_HOSTNAME}" \ + "PDS_JWT_SECRET=$${PDS_JWT_SECRET}" \ + "PDS_ADMIN_PASSWORD=$${PDS_ADMIN_PASSWORD}" \ + "PDS_ADMIN_EMAIL=$${PDS_ADMIN_EMAIL}" \ + "PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX}" \ + "PDS_DATA_DIRECTORY=$${PDS_DATA_DIRECTORY}" \ + "PDS_BLOBSTORE_DISK_LOCATION=$${PDS_DATA_DIRECTORY}/blocks" \ + "PDS_BLOB_UPLOAD_LIMIT=$${PDS_BLOB_UPLOAD_LIMIT}" \ + "PDS_DID_PLC_URL=$${PDS_DID_PLC_URL}" \ + "PDS_BSKY_APP_VIEW_URL=$${PDS_BSKY_APP_VIEW_URL}" \ + "PDS_BSKY_APP_VIEW_DID=$${PDS_BSKY_APP_VIEW_DID}" \ + "PDS_REPORT_SERVICE_URL=$${PDS_REPORT_SERVICE_URL}" \ + "PDS_REPORT_SERVICE_DID=$${PDS_REPORT_SERVICE_DID}" \ + "PDS_CRAWLERS=$${PDS_CRAWLERS}" \ + "LOG_ENABLED=$${LOG_ENABLED}" \ + > /pds/pds.env + + echo "Launching PDS..." + exec node --enable-source-maps index.js + ' + healthcheck: test: ["CMD", "wget", "--spider", "http://127.0.0.1:3000/xrpc/_health"] interval: 2s From 158711a1658e9ca000711f33852d73f6a76b838c Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:03:52 +0200 Subject: [PATCH 0139/1717] Update service-templates.json --- templates/service-templates.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/templates/service-templates.json b/templates/service-templates.json index 49884462f..1dbfac94b 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -203,6 +203,19 @@ "logo": "svgs/bitcoin.svg", "minversion": "0.0.0" }, + "bluesky-pds": { + "documentation": "https://github.com/bluesky-social/pds?utm_source=coolify.io", + "slogan": "Bluesky PDS (Personal Data Server)", + "compose": "c2VydmljZXM6CiAgcGRzOgogICAgaW1hZ2U6ICdnaGNyLmlvL2JsdWVza3ktc29jaWFsL3BkczpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICcuL3Bkcy1kYXRhOi9wZHMnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUERTXzMwMDAKICAgICAgLSAnUERTX0hPU1ROQU1FPSR7U0VSVklDRV9VUkxfUERTfScKICAgICAgLSAnUERTX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVF9TRUNSRVR9JwogICAgICAtICdQRFNfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSAnUERTX0FETUlOX0VNQUlMPSR7U0VSVklDRV9FTUFJTF9BRE1JTn0nCiAgICAgIC0gJ1BEU19QTENfUk9UQVRJT05fS0VZX0syNTZfUFJJVkFURV9LRVlfSEVYPSR7UERTX1BMQ19ST1RBVElPTl9LRVlfSzI1Nl9QUklWQVRFX0tFWV9IRVh9JwogICAgICAtICdQRFNfREFUQV9ESVJFQ1RPUlk9JHtQRFNfREFUQV9ESVJFQ1RPUlk6LS9wZHN9JwogICAgICAtICdQRFNfQkxPQlNUT1JFX0RJU0tfTE9DQVRJT049JHtQRFNfREFUQV9ESVJFQ1RPUlk6LS9wZHN9L2Jsb2NrcycKICAgICAgLSAnUERTX0JMT0JfVVBMT0FEX0xJTUlUPSR7UERTX0JMT0JfVVBMT0FEX0xJTUlUOi01MjQyODgwMH0nCiAgICAgIC0gJ1BEU19ESURfUExDX1VSTD0ke1BEU19ESURfUExDX1VSTDotaHR0cHM6Ly9wbGMuZGlyZWN0b3J5fScKICAgICAgLSAnUERTX0JTS1lfQVBQX1ZJRVdfVVJMPSR7UERTX0JTS1lfQVBQX1ZJRVdfVVJMOi1odHRwczovL2FwaS5ic2t5LmFwcH0nCiAgICAgIC0gJ1BEU19CU0tZX0FQUF9WSUVXX0RJRD0ke1BEU19CU0tZX0FQUF9WSUVXX0RJRDotZGlkOndlYjphcGkuYnNreS5hcHB9JwogICAgICAtICdQRFNfUkVQT1JUX1NFUlZJQ0VfVVJMPSR7UERTX1JFUE9SVF9TRVJWSUNFX1VSTDotaHR0cHM6Ly9tb2QuYnNreS5hcHAveHJwYy9jb20uYXRwcm90by5tb2RlcmF0aW9uLmNyZWF0ZVJlcG9ydH0nCiAgICAgIC0gJ1BEU19SRVBPUlRfU0VSVklDRV9ESUQ9JHtQRFNfUkVQT1JUX1NFUlZJQ0VfRElEOi1kaWQ6cGxjOmFyN2M0Ynk0NnFqZHlkaGRldnZybmRhY30nCiAgICAgIC0gJ1BEU19DUkFXTEVSUz0ke1BEU19DUkFXTEVSUzotaHR0cHM6Ly9ic2t5Lm5ldHdvcmt9JwogICAgICAtICdMT0dfRU5BQkxFRD0ke0xPR19FTkFCTEVEOi10cnVlfScKICAgIGNvbW1hbmQ6ICJzaCAtYyAnXG4gIGVjaG8gXCJJbnN0YWxsaW5nIGN1cmwsIGJhc2gsIGFuZCBwZHNhZG1pbi4uLlwiXG4gIGFwayBhZGQgLS1uby1jYWNoZSBjdXJsIGJhc2ggJiYgXFxcbiAgY3VybCAtbyAvdXNyL2xvY2FsL2Jpbi9wZHNhZG1pbi5zaCBodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYmx1ZXNreS1zb2NpYWwvcGRzL21haW4vcGRzYWRtaW4uc2ggJiYgXFxcbiAgY2htb2QgK3ggL3Vzci9sb2NhbC9iaW4vcGRzYWRtaW4uc2ggJiYgXFxcbiAgbG4gLXNmIC91c3IvbG9jYWwvYmluL3Bkc2FkbWluLnNoIC91c3IvbG9jYWwvYmluL3Bkc2FkbWluXG5cbiAgZWNobyBcIkdlbmVyYXRpbmcgL3Bkcy9wZHMuZW52Li4uXCJcbiAgcHJpbnRmIFwiJXNcXG5cIiBcXFxuICBcIlNFUlZJQ0VfRlFETl9QRFNfMzAwMD0kJHtTRVJWSUNFX0ZRRE5fUERTXzMwMDB9XCIgXFxcbiAgXCJQRFNfSE9TVE5BTUU9JCR7UERTX0hPU1ROQU1FfVwiIFxcXG4gIFwiUERTX0pXVF9TRUNSRVQ9JCR7UERTX0pXVF9TRUNSRVR9XCIgXFxcbiAgXCJQRFNfQURNSU5fUEFTU1dPUkQ9JCR7UERTX0FETUlOX1BBU1NXT1JEfVwiIFxcXG4gIFwiUERTX0FETUlOX0VNQUlMPSQke1BEU19BRE1JTl9FTUFJTH1cIiBcXFxuICBcIlBEU19QTENfUk9UQVRJT05fS0VZX0syNTZfUFJJVkFURV9LRVlfSEVYPSQke1BEU19QTENfUk9UQVRJT05fS0VZX0syNTZfUFJJVkFURV9LRVlfSEVYfVwiIFxcXG4gIFwiUERTX0RBVEFfRElSRUNUT1JZPSQke1BEU19EQVRBX0RJUkVDVE9SWX1cIiBcXFxuICBcIlBEU19CTE9CU1RPUkVfRElTS19MT0NBVElPTj0kJHtQRFNfREFUQV9ESVJFQ1RPUll9L2Jsb2Nrc1wiIFxcXG4gIFwiUERTX0JMT0JfVVBMT0FEX0xJTUlUPSQke1BEU19CTE9CX1VQTE9BRF9MSU1JVH1cIiBcXFxuICBcIlBEU19ESURfUExDX1VSTD0kJHtQRFNfRElEX1BMQ19VUkx9XCIgXFxcbiAgXCJQRFNfQlNLWV9BUFBfVklFV19VUkw9JCR7UERTX0JTS1lfQVBQX1ZJRVdfVVJMfVwiIFxcXG4gIFwiUERTX0JTS1lfQVBQX1ZJRVdfRElEPSQke1BEU19CU0tZX0FQUF9WSUVXX0RJRH1cIiBcXFxuICBcIlBEU19SRVBPUlRfU0VSVklDRV9VUkw9JCR7UERTX1JFUE9SVF9TRVJWSUNFX1VSTH1cIiBcXFxuICBcIlBEU19SRVBPUlRfU0VSVklDRV9ESUQ9JCR7UERTX1JFUE9SVF9TRVJWSUNFX0RJRH1cIiBcXFxuICBcIlBEU19DUkFXTEVSUz0kJHtQRFNfQ1JBV0xFUlN9XCIgXFxcbiAgXCJMT0dfRU5BQkxFRD0kJHtMT0dfRU5BQkxFRH1cIiBcXFxuICA+IC9wZHMvcGRzLmVudlxuXG4gIGVjaG8gXCJMYXVuY2hpbmcgUERTLi4uXCJcbiAgZXhlYyBub2RlIC0tZW5hYmxlLXNvdXJjZS1tYXBzIGluZGV4LmpzXG4nXG4iCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC94cnBjL19oZWFsdGgnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "bluesky", + "pds", + "platform" + ], + "logo": "svgs/bluesky.svg", + "minversion": "0.0.0", + "port": "3000" + }, "bookstack": { "documentation": "https://www.bookstackapp.com/docs/?utm_source=coolify.io", "slogan": "BookStack is a simple, self-hosted, easy-to-use platform for organising and storing information", From 103a9c2df29c0c47983ea5faec8d75fe373884c0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 23 Jul 2025 17:57:05 +0200 Subject: [PATCH 0140/1717] fix(policy): update delete method to check for admin status in S3StoragePolicy --- app/Policies/S3StoragePolicy.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Policies/S3StoragePolicy.php b/app/Policies/S3StoragePolicy.php index 28f5f8426..4f837a3dd 100644 --- a/app/Policies/S3StoragePolicy.php +++ b/app/Policies/S3StoragePolicy.php @@ -21,7 +21,7 @@ public function viewAny(User $user): bool */ public function view(User $user, S3Storage $storage): bool { - return $user->teams()->where('id', $storage->team_id)->exists(); + return $user->teams()->get()->firstWhere('id', $storage->team_id)->exists(); } /** @@ -37,7 +37,7 @@ public function create(User $user): bool */ public function update(User $user, Server $server): bool { - return $user->teams()->get()->firstWhere('id', $server->team_id) !== null; + return $user->teams()->get()->firstWhere('id', $server->team_id)->exists() && $user->isAdmin(); } /** @@ -45,7 +45,7 @@ public function update(User $user, Server $server): bool */ public function delete(User $user, S3Storage $storage): bool { - return $user->teams()->where('id', $storage->team_id)->exists(); + return $user->teams()->get()->firstWhere('id', $storage->team_id)->exists() && $user->isAdmin(); } /** From cc5abc093d35be0b25bb6a69fefa34575df1eba3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:49:12 +0200 Subject: [PATCH 0141/1717] fix(container): sort containers alphabetically by name in ExecuteContainerCommand and update filtering in Terminal Index --- app/Livewire/Project/Shared/ExecuteContainerCommand.php | 6 ++++++ app/Livewire/Terminal/Index.php | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 2d55807c7..6833492a6 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -132,6 +132,12 @@ public function loadContainers() }); } } + + // Sort containers alphabetically by name + $this->containers = $this->containers->sortBy(function ($container) { + return data_get($container, 'container.Names'); + }); + if ($this->containers->count() === 1) { $this->selected_container = data_get($this->containers->first(), 'container.Names'); } diff --git a/app/Livewire/Terminal/Index.php b/app/Livewire/Terminal/Index.php index 10084a991..f4008cb1c 100644 --- a/app/Livewire/Terminal/Index.php +++ b/app/Livewire/Terminal/Index.php @@ -59,7 +59,7 @@ private function getAllActiveContainers() return null; })->filter(); - }); + })->sortBy('name'); } public function updatedSelectedUuid() From a0bc4dac55e5f9255376dc017874035eb8642fa7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:39:27 +0200 Subject: [PATCH 0142/1717] fix(application): streamline environment variable updates for Docker Compose services and enhance FQDN generation logic --- app/Livewire/Project/Application/General.php | 63 +- app/Livewire/Project/Resource/Create.php | 2 +- app/Models/Application.php | 2 +- app/Models/Service.php | 4 +- bootstrap/helpers/parsers.php | 1657 ++++++++++++++++++ bootstrap/helpers/services.php | 201 +-- bootstrap/helpers/shared.php | 113 +- 7 files changed, 1794 insertions(+), 248 deletions(-) create mode 100644 bootstrap/helpers/parsers.php diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 58a35caa0..2eadbaa0e 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -4,7 +4,6 @@ use App\Actions\Application\GenerateConfig; use App\Models\Application; -use App\Models\EnvironmentVariable; use Illuminate\Support\Collection; use Livewire\Component; use Spatie\Url\Url; @@ -270,7 +269,6 @@ public function generateDomain(string $serviceName) $this->application->save(); $this->dispatch('success', 'Domain generated.'); if ($this->application->build_pack === 'dockercompose') { - $this->updateServiceEnvironmentVariables(); $this->loadComposeFile(showToast: false); } @@ -344,7 +342,7 @@ public function resetDefaultLabels($manualReset = false) $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); if ($this->application->build_pack === 'dockercompose') { - $this->loadComposeFile(); + $this->loadComposeFile(showToast: false); } $this->dispatch('configurationChanged'); } catch (\Throwable $e) { @@ -421,7 +419,7 @@ public function submit($showToaster = true) } if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) { - $compose_return = $this->loadComposeFile(); + $compose_return = $this->loadComposeFile(showToast: false); if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) { return; } @@ -470,7 +468,6 @@ public function submit($showToaster = true) } $this->application->docker_compose_domains = json_encode($originalDomains); - foreach ($originalDomains as $serviceName => $service) { $domain = data_get($service, 'domain'); if ($domain) { @@ -486,12 +483,6 @@ public function submit($showToaster = true) } $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); - - // Update SERVICE_FQDN_ and SERVICE_URL_ environment variables for Docker Compose applications - if ($this->application->build_pack === 'dockercompose') { - $this->updateServiceEnvironmentVariables(); - } - $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!'); } catch (\Throwable $e) { $originalFqdn = $this->application->getOriginal('fqdn'); @@ -525,25 +516,33 @@ private function updateServiceEnvironmentVariables() foreach ($domains as $serviceName => $service) { $serviceNameFormatted = str($serviceName)->upper()->replace('-', '_'); $domain = data_get($service, 'domain'); + // Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed + $this->application->environment_variables()->where('resourceable_type', Application::class) + ->where('resourceable_id', $this->application->id) + ->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%") + ->delete(); + + $this->application->environment_variables()->where('resourceable_type', Application::class) + ->where('resourceable_id', $this->application->id) + ->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%") + ->delete(); if ($domain) { // Create or update SERVICE_FQDN_ and SERVICE_URL_ variables $fqdn = Url::fromString($domain); $port = $fqdn->getPort(); $path = $fqdn->getPath(); - $fqdnValue = $fqdn->getScheme().'://'.$fqdn->getHost(); - if ($path !== '/') { - $fqdnValue = $fqdnValue.$path; - } - $urlValue = str($domain)->after('://'); + $urlValue = $fqdn->getScheme().'://'.$fqdn->getHost(); if ($path !== '/') { $urlValue = $urlValue.$path; } + $fqdnValue = str($domain)->after('://'); + if ($path !== '/') { + $fqdnValue = $fqdnValue.$path; + } // Create/update SERVICE_FQDN_ - EnvironmentVariable::updateOrCreate([ - 'resourceable_type' => Application::class, - 'resourceable_id' => $this->application->id, + $this->application->environment_variables()->updateOrCreate([ 'key' => "SERVICE_FQDN_{$serviceNameFormatted}", ], [ 'value' => $fqdnValue, @@ -552,21 +551,16 @@ private function updateServiceEnvironmentVariables() ]); // Create/update SERVICE_URL_ - EnvironmentVariable::updateOrCreate([ - 'resourceable_type' => Application::class, - 'resourceable_id' => $this->application->id, + $this->application->environment_variables()->updateOrCreate([ 'key' => "SERVICE_URL_{$serviceNameFormatted}", ], [ 'value' => $urlValue, 'is_build_time' => false, 'is_preview' => false, ]); - // Create/update port-specific variables if port exists - if ($port) { - EnvironmentVariable::updateOrCreate([ - 'resourceable_type' => Application::class, - 'resourceable_id' => $this->application->id, + if (filled($port)) { + $this->application->environment_variables()->updateOrCreate([ 'key' => "SERVICE_FQDN_{$serviceNameFormatted}_{$port}", ], [ 'value' => $fqdnValue, @@ -574,9 +568,7 @@ private function updateServiceEnvironmentVariables() 'is_preview' => false, ]); - EnvironmentVariable::updateOrCreate([ - 'resourceable_type' => Application::class, - 'resourceable_id' => $this->application->id, + $this->application->environment_variables()->updateOrCreate([ 'key' => "SERVICE_URL_{$serviceNameFormatted}_{$port}", ], [ 'value' => $urlValue, @@ -584,17 +576,6 @@ private function updateServiceEnvironmentVariables() 'is_preview' => false, ]); } - } else { - // Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed - EnvironmentVariable::where('resourceable_type', Application::class) - ->where('resourceable_id', $this->application->id) - ->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%") - ->delete(); - - EnvironmentVariable::where('resourceable_type', Application::class) - ->where('resourceable_id', $this->application->id) - ->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%") - ->delete(); } } } diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index e7cff4f29..d8e397e59 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -102,7 +102,7 @@ public function mount() } }); } - $service->parse(isNew: true); + $service->parse(isNew: true, isOneClick: true); return redirect()->route('project.service.configuration', [ 'service_uuid' => $service->uuid, diff --git a/app/Models/Application.php b/app/Models/Application.php index 86eea1de8..ed97454d2 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1353,7 +1353,7 @@ public function oldRawParser() public function parse(int $pull_request_id = 0, ?int $preview_id = null) { if ((int) $this->compose_parsing_version >= 3) { - return newParser($this, $pull_request_id, $preview_id); + return applicationParser($this, $pull_request_id, $preview_id); } elseif ($this->docker_compose_raw) { return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id); } else { diff --git a/app/Models/Service.php b/app/Models/Service.php index da6c34fbb..4bef46642 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -1274,10 +1274,10 @@ public function saveComposeConfigs() instant_remote_process($commands, $this->server); } - public function parse(bool $isNew = false): Collection + public function parse(bool $isNew = false, bool $isOneClick = false): Collection { if ((int) $this->compose_parsing_version >= 3) { - return newParser($this); + return serviceParser($this, isOneClick: $isOneClick); } elseif ($this->docker_compose_raw) { return parseDockerComposeFile($this, $isNew); } else { diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php new file mode 100644 index 000000000..649de731d --- /dev/null +++ b/bootstrap/helpers/parsers.php @@ -0,0 +1,1657 @@ +fileStorages(); + + try { + $yaml = Yaml::parse($compose); + } catch (\Exception) { + return collect([]); + } + $services = data_get($yaml, 'services', collect([])); + $topLevel = collect([ + 'volumes' => collect(data_get($yaml, 'volumes', [])), + 'networks' => collect(data_get($yaml, 'networks', [])), + 'configs' => collect(data_get($yaml, 'configs', [])), + 'secrets' => collect(data_get($yaml, 'secrets', [])), + ]); + // If there are predefined volumes, make sure they are not null + if ($topLevel->get('volumes')->count() > 0) { + $temp = collect([]); + foreach ($topLevel['volumes'] as $volumeName => $volume) { + if (is_null($volume)) { + continue; + } + $temp->put($volumeName, $volume); + } + $topLevel['volumes'] = $temp; + } + // Get the base docker network + $baseNetwork = collect([$uuid]); + if ($isPullRequest) { + $baseNetwork = collect(["{$uuid}-{$pullRequestId}"]); + } + + $parsedServices = collect([]); + + $allMagicEnvironments = collect([]); + foreach ($services as $serviceName => $service) { + $magicEnvironments = collect([]); + $image = data_get_str($service, 'image'); + $environment = collect(data_get($service, 'environment', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + + $environment = collect(data_get($service, 'environment', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + + // convert environment variables to one format + $environment = convertToKeyValueCollection($environment); + + // Add Coolify defined environments + $allEnvironments = $resource->environment_variables()->get(['key', 'value']); + + $allEnvironments = $allEnvironments->mapWithKeys(function ($item) { + return [$item['key'] => $item['value']]; + }); + // filter and add magic environments + foreach ($environment as $key => $value) { + // Get all SERVICE_ variables from keys and values + $key = str($key); + $value = str($value); + $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; + preg_match_all($regex, $value, $valueMatches); + if (count($valueMatches[1]) > 0) { + foreach ($valueMatches[1] as $match) { + $match = replaceVariables($match); + if ($match->startsWith('SERVICE_')) { + if ($magicEnvironments->has($match->value())) { + continue; + } + $magicEnvironments->put($match->value(), ''); + } + } + } + // Get magic environments where we need to preset the FQDN + if ($key->startsWith('SERVICE_FQDN_')) { + // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 + if (substr_count(str($key)->value(), '_') === 3) { + $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); + $port = $key->afterLast('_')->value(); + } else { + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + $port = null; + } + $fqdn = $resource->fqdn; + if (blank($resource->fqdn)) { + $fqdn = generateFqdn($server, "$uuid"); + } + + if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { + $path = $value->value(); + if ($path !== '/') { + $fqdn = "$fqdn$path"; + } + } + $fqdnWithPort = $fqdn; + if ($port) { + $fqdnWithPort = "$fqdn:$port"; + } + if (is_null($resource->fqdn)) { + data_forget($resource, 'environment_variables'); + data_forget($resource, 'environment_variables_preview'); + $resource->fqdn = $fqdnWithPort; + $resource->save(); + } + + if (substr_count(str($key)->value(), '_') === 2) { + $resource->environment_variables()->updateOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + if (substr_count(str($key)->value(), '_') === 3) { + + $newKey = str($key)->beforeLast('_'); + $resource->environment_variables()->updateOrCreate([ + 'key' => $newKey->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } + } + + $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); + if ($magicEnvironments->count() > 0) { + foreach ($magicEnvironments as $key => $value) { + $key = str($key); + $value = replaceVariables($value); + $command = parseCommandFromMagicEnvVariable($key); + if ($command->value() === 'FQDN') { + if ($isOneClick) { + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); + } + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } elseif ($command->value() === 'URL') { + if ($isOneClick) { + $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); + } + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } else { + $value = generateEnvValue($command, $resource); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } + } + } + + // Parse the rest of the services + foreach ($services as $serviceName => $service) { + $image = data_get_str($service, 'image'); + $restart = data_get_str($service, 'restart', RESTART_MODE); + $logging = data_get($service, 'logging'); + + if ($server->isLogDrainEnabled()) { + if ($resource->isLogDrainEnabled()) { + $logging = generate_fluentd_configuration(); + } + } + $volumes = collect(data_get($service, 'volumes', [])); + $networks = collect(data_get($service, 'networks', [])); + $use_network_mode = data_get($service, 'network_mode') !== null; + $depends_on = collect(data_get($service, 'depends_on', [])); + $labels = collect(data_get($service, 'labels', [])); + if ($labels->count() > 0) { + if (isAssociativeArray($labels)) { + $newLabels = collect([]); + $labels->each(function ($value, $key) use ($newLabels) { + $newLabels->push("$key=$value"); + }); + $labels = $newLabels; + } + } + $environment = collect(data_get($service, 'environment', [])); + $ports = collect(data_get($service, 'ports', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + + $environment = convertToKeyValueCollection($environment); + $coolifyEnvironments = collect([]); + + $isDatabase = isDatabaseImage($image, $service); + $volumesParsed = collect([]); + + $baseName = generateApplicationContainerName( + application: $resource, + pull_request_id: $pullRequestId + ); + $containerName = "$serviceName-$baseName"; + $predefinedPort = null; + + $originalResource = $resource; + + if ($volumes->count() > 0) { + foreach ($volumes as $index => $volume) { + $type = null; + $source = null; + $target = null; + $content = null; + $isDirectory = false; + if (is_string($volume)) { + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); + $foundConfig = $fileStorages->whereMountPath($target)->first(); + if (sourceIsLocal($source)) { + $type = str('bind'); + if ($foundConfig) { + $contentNotNull_temp = data_get($foundConfig, 'content'); + if ($contentNotNull_temp) { + $content = $contentNotNull_temp; + } + $isDirectory = data_get($foundConfig, 'is_directory'); + } else { + // By default, we cannot determine if the bind is a directory or not, so we set it to directory + $isDirectory = true; + } + } else { + $type = str('volume'); + } + } elseif (is_array($volume)) { + $type = data_get_str($volume, 'type'); + $source = data_get_str($volume, 'source'); + $target = data_get_str($volume, 'target'); + $content = data_get($volume, 'content'); + $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); + + $foundConfig = $fileStorages->whereMountPath($target)->first(); + if ($foundConfig) { + $contentNotNull_temp = data_get($foundConfig, 'content'); + if ($contentNotNull_temp) { + $content = $contentNotNull_temp; + } + $isDirectory = data_get($foundConfig, 'is_directory'); + } else { + // if isDirectory is not set (or false) & content is also not set, we assume it is a directory + if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { + $isDirectory = true; + } + } + } + if ($type->value() === 'bind') { + if ($source->value() === '/var/run/docker.sock') { + $volume = $source->value().':'.$target->value(); + } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { + $volume = $source->value().':'.$target->value(); + } else { + if ((int) $resource->compose_parsing_version >= 4) { + $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + } else { + $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + } + $source = replaceLocalSource($source, $mainDirectory); + if ($isPullRequest) { + $source = $source."-pr-$pullRequestId"; + } + LocalFileVolume::updateOrCreate( + [ + 'mount_path' => $target, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ], + [ + 'fs_path' => $source, + 'mount_path' => $target, + 'content' => $content, + 'is_directory' => $isDirectory, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ] + ); + if (isDev()) { + if ((int) $resource->compose_parsing_version >= 4) { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + } else { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + } + } + $volume = "$source:$target"; + } + } elseif ($type->value() === 'volume') { + if ($topLevel->get('volumes')->has($source->value())) { + $temp = $topLevel->get('volumes')->get($source->value()); + if (data_get($temp, 'driver_opts.type') === 'cifs') { + continue; + } + if (data_get($temp, 'driver_opts.type') === 'nfs') { + continue; + } + } + $slugWithoutUuid = Str::slug($source, '-'); + $name = "{$uuid}_{$slugWithoutUuid}"; + + if ($isPullRequest) { + $name = "{$name}-pr-$pullRequestId"; + } + if (is_string($volume)) { + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); + $source = $name; + $volume = "$source:$target"; + } elseif (is_array($volume)) { + data_set($volume, 'source', $name); + } + $topLevel->get('volumes')->put($name, [ + 'name' => $name, + ]); + LocalPersistentVolume::updateOrCreate( + [ + 'name' => $name, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ], + [ + 'name' => $name, + 'mount_path' => $target, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ] + ); + } + dispatch(new ServerFilesFromServerJob($originalResource)); + $volumesParsed->put($index, $volume); + } + } + + if ($depends_on?->count() > 0) { + if ($isPullRequest) { + $newDependsOn = collect([]); + $depends_on->each(function ($dependency, $condition) use ($pullRequestId, $newDependsOn) { + if (is_numeric($condition)) { + $dependency = "$dependency-pr-$pullRequestId"; + + $newDependsOn->put($condition, $dependency); + } else { + $condition = "$condition-pr-$pullRequestId"; + $newDependsOn->put($condition, $dependency); + } + }); + $depends_on = $newDependsOn; + } + } + if (! $use_network_mode) { + if ($topLevel->get('networks')?->count() > 0) { + foreach ($topLevel->get('networks') as $networkName => $network) { + if ($networkName === 'default') { + continue; + } + // ignore aliases + if ($network['aliases'] ?? false) { + continue; + } + $networkExists = $networks->contains(function ($value, $key) use ($networkName) { + return $value == $networkName || $key == $networkName; + }); + if (! $networkExists) { + $networks->put($networkName, null); + } + } + } + $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { + return $value == $baseNetwork; + }); + if (! $baseNetworkExists) { + foreach ($baseNetwork as $network) { + $topLevel->get('networks')->put($network, [ + 'name' => $network, + 'external' => true, + ]); + } + } + } + + // Collect/create/update ports + $collectedPorts = collect([]); + if ($ports->count() > 0) { + foreach ($ports as $sport) { + if (is_string($sport) || is_numeric($sport)) { + $collectedPorts->push($sport); + } + if (is_array($sport)) { + $target = data_get($sport, 'target'); + $published = data_get($sport, 'published'); + $protocol = data_get($sport, 'protocol'); + $collectedPorts->push("$target:$published/$protocol"); + } + } + } + + $networks_temp = collect(); + + if (! $use_network_mode) { + foreach ($networks as $key => $network) { + if (gettype($network) === 'string') { + // networks: + // - appwrite + $networks_temp->put($network, null); + } elseif (gettype($network) === 'array') { + // networks: + // default: + // ipv4_address: 192.168.203.254 + $networks_temp->put($key, $network); + } + } + foreach ($baseNetwork as $key => $network) { + $networks_temp->put($network, null); + } + + if (data_get($resource, 'settings.connect_to_docker_network')) { + $network = $resource->destination->network; + $networks_temp->put($network, null); + $topLevel->get('networks')->put($network, [ + 'name' => $network, + 'external' => true, + ]); + } + } + + $normalEnvironments = $environment->diffKeys($allMagicEnvironments); + $normalEnvironments = $normalEnvironments->filter(function ($value, $key) { + return ! str($value)->startsWith('SERVICE_'); + }); + foreach ($normalEnvironments as $key => $value) { + $key = str($key); + $value = str($value); + $originalValue = $value; + $parsedValue = replaceVariables($value); + if ($value->startsWith('$SERVICE_')) { + $resource->environment_variables()->firstOrCreate([ + 'key' => $key, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + + continue; + } + if (! $value->startsWith('$')) { + continue; + } + if ($key->value() === $parsedValue->value()) { + $value = null; + $resource->environment_variables()->firstOrCreate([ + 'key' => $key, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } else { + if ($value->startsWith('$')) { + $isRequired = false; + if ($value->contains(':-')) { + $value = replaceVariables($value); + $key = $value->before(':'); + $value = $value->after(':-'); + } elseif ($value->contains('-')) { + $value = replaceVariables($value); + + $key = $value->before('-'); + $value = $value->after('-'); + } elseif ($value->contains(':?')) { + $value = replaceVariables($value); + + $key = $value->before(':'); + $value = $value->after(':?'); + $isRequired = true; + } elseif ($value->contains('?')) { + $value = replaceVariables($value); + + $key = $value->before('?'); + $value = $value->after('?'); + $isRequired = true; + } + if ($originalValue->value() === $value->value()) { + // This means the variable does not have a default value, so it needs to be created in Coolify + $parsedKeyValue = replaceVariables($value); + $resource->environment_variables()->firstOrCreate([ + 'key' => $parsedKeyValue, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'is_build_time' => false, + 'is_preview' => false, + 'is_required' => $isRequired, + ]); + // Add the variable to the environment so it will be shown in the deployable compose file + $environment[$parsedKeyValue->value()] = $value; + + continue; + } + $resource->environment_variables()->firstOrCreate([ + 'key' => $key, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + 'is_required' => $isRequired, + ]); + } + } + } + $branch = $originalResource->git_branch; + if ($pullRequestId !== 0) { + $branch = "pull/{$pullRequestId}/head"; + } + if ($originalResource->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $coolifyEnvironments->put('COOLIFY_BRANCH', "\"{$branch}\""); + } + + // Add COOLIFY_RESOURCE_UUID to environment + if ($resource->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { + $coolifyEnvironments->put('COOLIFY_RESOURCE_UUID', "{$resource->uuid}"); + } + + // Add COOLIFY_CONTAINER_NAME to environment + if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', "{$containerName}"); + } + + if ($isPullRequest) { + $preview = $resource->previews()->find($preview_id); + $domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))) ?? collect([]); + } else { + $domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]); + } + ray($domains); + $fqdns = data_get($domains, "$serviceName.domain"); + // Generate SERVICE_FQDN & SERVICE_URL for dockercompose + if ($resource->build_pack === 'dockercompose') { + foreach ($domains as $forServiceName => $domain) { + $parsedDomain = data_get($domain, 'domain'); + $serviceNameFormatted = str($serviceName)->upper()->replace('-', '_'); + + if (filled($parsedDomain)) { + $parsedDomain = str($parsedDomain)->explode(',')->first(); + $coolifyUrl = Url::fromString($parsedDomain); + $coolifyScheme = $coolifyUrl->getScheme(); + $coolifyFqdn = $coolifyUrl->getHost(); + $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); + $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyUrl->__toString()); + $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyFqdn); + $resource->environment_variables()->updateOrCreate([ + 'resourceable_type' => Application::class, + 'resourceable_id' => $resource->id, + 'key' => 'SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_'), + ], [ + 'value' => $coolifyUrl->__toString(), + 'is_build_time' => false, + 'is_preview' => false, + ]); + $resource->environment_variables()->updateOrCreate([ + 'resourceable_type' => Application::class, + 'resourceable_id' => $resource->id, + 'key' => 'SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_'), + ], [ + 'value' => $coolifyFqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } else { + $resource->environment_variables()->where('resourceable_type', Application::class) + ->where('resourceable_id', $resource->id) + ->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%") + ->delete(); + $resource->environment_variables()->where('resourceable_type', Application::class) + ->where('resourceable_id', $resource->id) + ->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%") + ->delete(); + } + } + } + // If the domain is set, we need to generate the FQDNs for the preview + if (filled($fqdns)) { + $fqdns = str($fqdns)->explode(','); + if ($isPullRequest) { + $preview = $resource->previews()->find($preview_id); + $docker_compose_domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))); + if ($docker_compose_domains->count() > 0) { + $found_fqdn = data_get($docker_compose_domains, "$serviceName.domain"); + if ($found_fqdn) { + $fqdns = collect($found_fqdn); + } else { + $fqdns = collect([]); + } + } else { + $fqdns = $fqdns->map(function ($fqdn) use ($pullRequestId, $resource) { + $preview = ApplicationPreview::findPreviewByApplicationAndPullId($resource->id, $pullRequestId); + $url = Url::fromString($fqdn); + $template = $resource->preview_url_template; + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2; + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $preview->fqdn = $preview_fqdn; + $preview->save(); + + return $preview_fqdn; + }); + } + } + } + $defaultLabels = defaultLabels( + id: $resource->id, + name: $containerName, + projectName: $resource->project()->name, + resourceName: $resource->name, + pull_request_id: $pullRequestId, + type: 'application', + environment: $resource->environment->name, + ); + + $isDatabase = isDatabaseImage($image, $service); + // Add COOLIFY_FQDN & COOLIFY_URL to environment + if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { + $fqdnsWithoutPort = $fqdns->map(function ($fqdn) { + return str($fqdn)->after('://')->before(':')->prepend(str($fqdn)->before('://')->append('://')); + }); + $coolifyEnvironments->put('COOLIFY_URL', $fqdnsWithoutPort->implode(',')); + + $urls = $fqdns->map(function ($fqdn) { + return str($fqdn)->replace('http://', '')->replace('https://', '')->before(':'); + }); + $coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(',')); + } + add_coolify_default_environment_variables($resource, $coolifyEnvironments, $resource->environment_variables); + if ($environment->count() > 0) { + $environment = $environment->filter(function ($value, $key) { + return ! str($key)->startsWith('SERVICE_FQDN_'); + })->map(function ($value, $key) use ($resource) { + // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used + if (str($value)->isEmpty()) { + if ($resource->environment_variables()->where('key', $key)->exists()) { + $value = $resource->environment_variables()->where('key', $key)->first()->value; + } else { + $value = null; + } + } + + return $value; + }); + } + $serviceLabels = $labels->merge($defaultLabels); + if ($serviceLabels->count() > 0) { + $isContainerLabelEscapeEnabled = data_get($resource, 'settings.is_container_label_escape_enabled'); + if ($isContainerLabelEscapeEnabled) { + $serviceLabels = $serviceLabels->map(function ($value, $key) { + return escapeDollarSign($value); + }); + } + } + if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { + $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; + $uuid = $resource->uuid; + $network = data_get($resource, 'destination.network'); + if ($isPullRequest) { + $uuid = "{$resource->uuid}-{$pullRequestId}"; + } + if ($isPullRequest) { + $network = "{$resource->destination->network}-{$pullRequestId}"; + } + if ($shouldGenerateLabelsExactly) { + switch ($server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image + )); + break; + case ProxyTypes::CADDY->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $network, + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image, + predefinedPort: $predefinedPort + )); + break; + } + } else { + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image + )); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $network, + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image, + predefinedPort: $predefinedPort + )); + } + } + data_forget($service, 'volumes.*.content'); + data_forget($service, 'volumes.*.isDirectory'); + data_forget($service, 'volumes.*.is_directory'); + data_forget($service, 'exclude_from_hc'); + + $volumesParsed = $volumesParsed->map(function ($volume) { + data_forget($volume, 'content'); + data_forget($volume, 'is_directory'); + data_forget($volume, 'isDirectory'); + + return $volume; + }); + + $payload = collect($service)->merge([ + 'container_name' => $containerName, + 'restart' => $restart->value(), + 'labels' => $serviceLabels, + ]); + if (! $use_network_mode) { + $payload['networks'] = $networks_temp; + } + if ($ports->count() > 0) { + $payload['ports'] = $ports; + } + if ($volumesParsed->count() > 0) { + $payload['volumes'] = $volumesParsed; + } + if ($environment->count() > 0 || $coolifyEnvironments->count() > 0) { + $payload['environment'] = $environment->merge($coolifyEnvironments); + } + if ($logging) { + $payload['logging'] = $logging; + } + if ($depends_on->count() > 0) { + $payload['depends_on'] = $depends_on; + } + if ($isPullRequest) { + $serviceName = "{$serviceName}-pr-{$pullRequestId}"; + } + + $parsedServices->put($serviceName, $payload); + } + $topLevel->put('services', $parsedServices); + + $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets']; + + $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) { + return array_search($key, $customOrder); + }); + + $resource->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2); + data_forget($resource, 'environment_variables'); + data_forget($resource, 'environment_variables_preview'); + $resource->save(); + + return $topLevel; +} + +function serviceParser(Service $resource, bool $isOneClick = false): Collection +{ + $uuid = data_get($resource, 'uuid'); + $compose = data_get($resource, 'docker_compose_raw'); + if (! $compose) { + return collect([]); + } + + $server = data_get($resource, 'server'); + $allServices = get_service_templates(); + + try { + $yaml = Yaml::parse($compose); + } catch (\Exception) { + return collect([]); + } + $services = data_get($yaml, 'services', collect([])); + $topLevel = collect([ + 'volumes' => collect(data_get($yaml, 'volumes', [])), + 'networks' => collect(data_get($yaml, 'networks', [])), + 'configs' => collect(data_get($yaml, 'configs', [])), + 'secrets' => collect(data_get($yaml, 'secrets', [])), + ]); + // If there are predefined volumes, make sure they are not null + if ($topLevel->get('volumes')->count() > 0) { + $temp = collect([]); + foreach ($topLevel['volumes'] as $volumeName => $volume) { + if (is_null($volume)) { + continue; + } + $temp->put($volumeName, $volume); + } + $topLevel['volumes'] = $temp; + } + // Get the base docker network + $baseNetwork = collect([$uuid]); + + $parsedServices = collect([]); + + $allMagicEnvironments = collect([]); + foreach ($services as $serviceName => $service) { + $predefinedPort = null; + $magicEnvironments = collect([]); + $image = data_get_str($service, 'image'); + $environment = collect(data_get($service, 'environment', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + $isDatabase = isDatabaseImage($image, $service); + + $containerName = "$serviceName-{$resource->uuid}"; + + if ($serviceName === 'registry') { + $tempServiceName = 'docker-registry'; + } else { + $tempServiceName = $serviceName; + } + if (str(data_get($service, 'image'))->contains('glitchtip')) { + $tempServiceName = 'glitchtip'; + } + if ($serviceName === 'supabase-kong') { + $tempServiceName = 'supabase'; + } + $serviceDefinition = data_get($allServices, $tempServiceName); + $predefinedPort = data_get($serviceDefinition, 'port'); + if ($serviceName === 'plausible') { + $predefinedPort = '8000'; + } + if ($isDatabase) { + $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); + if ($applicationFound) { + $savedService = $applicationFound; + } else { + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $serviceName, + 'service_id' => $resource->id, + ]); + } + } else { + $savedService = ServiceApplication::firstOrCreate([ + 'name' => $serviceName, + 'service_id' => $resource->id, + ], [ + 'is_gzip_enabled' => true, + ]); + } + // Check if image changed + if ($savedService->image !== $image) { + $savedService->image = $image; + $savedService->save(); + } + // Pocketbase does not need gzip for SSE. + if (str($savedService->image)->contains('pocketbase') && $savedService->is_gzip_enabled) { + $savedService->is_gzip_enabled = false; + $savedService->save(); + } + + $environment = collect(data_get($service, 'environment', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + + // convert environment variables to one format + $environment = convertToKeyValueCollection($environment); + + // Add Coolify defined environments + $allEnvironments = $resource->environment_variables()->get(['key', 'value']); + + $allEnvironments = $allEnvironments->mapWithKeys(function ($item) { + return [$item['key'] => $item['value']]; + }); + // filter and add magic environments + foreach ($environment as $key => $value) { + // Get all SERVICE_ variables from keys and values + $key = str($key); + $value = str($value); + $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; + preg_match_all($regex, $value, $valueMatches); + if (count($valueMatches[1]) > 0) { + foreach ($valueMatches[1] as $match) { + $match = replaceVariables($match); + if ($match->startsWith('SERVICE_')) { + if ($magicEnvironments->has($match->value())) { + continue; + } + $magicEnvironments->put($match->value(), ''); + } + } + } + // Get magic environments where we need to preset the FQDN + if ($key->startsWith('SERVICE_FQDN_')) { + // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 + if (substr_count(str($key)->value(), '_') === 3) { + $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); + $port = $key->afterLast('_')->value(); + } else { + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + $port = null; + } + if ($isOneClick) { + if (blank($savedService->fqdn)) { + if ($fqdnFor) { + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + } else { + $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); + } + } else { + $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); + } + + } else { + // For services which are not one-click, if no explicit FQDN is set, leave SERVICE_FQDN_ variables empty + if (blank($savedService->fqdn)) { + $fqdn = ''; + } else { + $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); + } + } + if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { + $path = $value->value(); + if ($path !== '/') { + $fqdn = "$fqdn$path"; + } + } + $fqdnWithPort = $fqdn; + if ($port) { + $fqdnWithPort = "$fqdn:$port"; + } + if (is_null($savedService->fqdn)) { + $savedService->fqdn = $fqdnWithPort; + $savedService->save(); + } + + if (substr_count(str($key)->value(), '_') === 2) { + $resource->environment_variables()->updateOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + if (substr_count(str($key)->value(), '_') === 3) { + $newKey = str($key)->beforeLast('_'); + $resource->environment_variables()->updateOrCreate([ + 'key' => $newKey->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } + } + + $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); + if ($magicEnvironments->count() > 0) { + foreach ($magicEnvironments as $key => $value) { + $key = str($key); + $value = replaceVariables($value); + $command = parseCommandFromMagicEnvVariable($key); + if ($command->value() === 'FQDN') { + if ($isOneClick) { + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); + } + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } elseif ($command->value() === 'URL') { + if ($isOneClick) { + $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); + } + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } else { + $value = generateEnvValue($command, $resource); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } + } + } + + $serviceAppsLogDrainEnabledMap = $resource->applications()->get()->keyBy('name')->map(function ($app) { + return $app->isLogDrainEnabled(); + }); + + // Parse the rest of the services + foreach ($services as $serviceName => $service) { + $image = data_get_str($service, 'image'); + $restart = data_get_str($service, 'restart', RESTART_MODE); + $logging = data_get($service, 'logging'); + + if ($server->isLogDrainEnabled()) { + if ($serviceAppsLogDrainEnabledMap->get($serviceName)) { + $logging = generate_fluentd_configuration(); + } + } + $volumes = collect(data_get($service, 'volumes', [])); + $networks = collect(data_get($service, 'networks', [])); + $use_network_mode = data_get($service, 'network_mode') !== null; + $depends_on = collect(data_get($service, 'depends_on', [])); + $labels = collect(data_get($service, 'labels', [])); + if ($labels->count() > 0) { + if (isAssociativeArray($labels)) { + $newLabels = collect([]); + $labels->each(function ($value, $key) use ($newLabels) { + $newLabels->push("$key=$value"); + }); + $labels = $newLabels; + } + } + $environment = collect(data_get($service, 'environment', [])); + $ports = collect(data_get($service, 'ports', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + + $environment = convertToKeyValueCollection($environment); + $coolifyEnvironments = collect([]); + + $isDatabase = isDatabaseImage($image, $service); + $volumesParsed = collect([]); + + $containerName = "$serviceName-{$resource->uuid}"; + + if ($serviceName === 'registry') { + $tempServiceName = 'docker-registry'; + } else { + $tempServiceName = $serviceName; + } + if (str(data_get($service, 'image'))->contains('glitchtip')) { + $tempServiceName = 'glitchtip'; + } + if ($serviceName === 'supabase-kong') { + $tempServiceName = 'supabase'; + } + $serviceDefinition = data_get($allServices, $tempServiceName); + $predefinedPort = data_get($serviceDefinition, 'port'); + if ($serviceName === 'plausible') { + $predefinedPort = '8000'; + } + + if ($isDatabase) { + $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + if ($applicationFound) { + $savedService = $applicationFound; + } else { + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } + } else { + $savedService = ServiceApplication::firstOrCreate([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } + $fileStorages = $savedService->fileStorages(); + if ($savedService->image !== $image) { + $savedService->image = $image; + $savedService->save(); + } + + $originalResource = $savedService; + + if ($volumes->count() > 0) { + foreach ($volumes as $index => $volume) { + $type = null; + $source = null; + $target = null; + $content = null; + $isDirectory = false; + if (is_string($volume)) { + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); + $foundConfig = $fileStorages->whereMountPath($target)->first(); + if (sourceIsLocal($source)) { + $type = str('bind'); + if ($foundConfig) { + $contentNotNull_temp = data_get($foundConfig, 'content'); + if ($contentNotNull_temp) { + $content = $contentNotNull_temp; + } + $isDirectory = data_get($foundConfig, 'is_directory'); + } else { + // By default, we cannot determine if the bind is a directory or not, so we set it to directory + $isDirectory = true; + } + } else { + $type = str('volume'); + } + } elseif (is_array($volume)) { + $type = data_get_str($volume, 'type'); + $source = data_get_str($volume, 'source'); + $target = data_get_str($volume, 'target'); + $content = data_get($volume, 'content'); + $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); + + $foundConfig = $fileStorages->whereMountPath($target)->first(); + if ($foundConfig) { + $contentNotNull_temp = data_get($foundConfig, 'content'); + if ($contentNotNull_temp) { + $content = $contentNotNull_temp; + } + $isDirectory = data_get($foundConfig, 'is_directory'); + } else { + // if isDirectory is not set (or false) & content is also not set, we assume it is a directory + if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { + $isDirectory = true; + } + } + } + if ($type->value() === 'bind') { + if ($source->value() === '/var/run/docker.sock') { + $volume = $source->value().':'.$target->value(); + } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { + $volume = $source->value().':'.$target->value(); + } else { + if ((int) $resource->compose_parsing_version >= 4) { + $mainDirectory = str(base_configuration_dir().'/services/'.$uuid); + } else { + $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + } + $source = replaceLocalSource($source, $mainDirectory); + LocalFileVolume::updateOrCreate( + [ + 'mount_path' => $target, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ], + [ + 'fs_path' => $source, + 'mount_path' => $target, + 'content' => $content, + 'is_directory' => $isDirectory, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ] + ); + if (isDev()) { + if ((int) $resource->compose_parsing_version >= 4) { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid); + } else { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + } + } + $volume = "$source:$target"; + } + } elseif ($type->value() === 'volume') { + if ($topLevel->get('volumes')->has($source->value())) { + $temp = $topLevel->get('volumes')->get($source->value()); + if (data_get($temp, 'driver_opts.type') === 'cifs') { + continue; + } + if (data_get($temp, 'driver_opts.type') === 'nfs') { + continue; + } + } + $slugWithoutUuid = Str::slug($source, '-'); + $name = "{$uuid}_{$slugWithoutUuid}"; + + if (is_string($volume)) { + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); + $source = $name; + $volume = "$source:$target"; + } elseif (is_array($volume)) { + data_set($volume, 'source', $name); + } + $topLevel->get('volumes')->put($name, [ + 'name' => $name, + ]); + LocalPersistentVolume::updateOrCreate( + [ + 'name' => $name, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ], + [ + 'name' => $name, + 'mount_path' => $target, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ] + ); + } + dispatch(new ServerFilesFromServerJob($originalResource)); + $volumesParsed->put($index, $volume); + } + } + + if (! $use_network_mode) { + if ($topLevel->get('networks')?->count() > 0) { + foreach ($topLevel->get('networks') as $networkName => $network) { + if ($networkName === 'default') { + continue; + } + // ignore aliases + if ($network['aliases'] ?? false) { + continue; + } + $networkExists = $networks->contains(function ($value, $key) use ($networkName) { + return $value == $networkName || $key == $networkName; + }); + if (! $networkExists) { + $networks->put($networkName, null); + } + } + } + $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { + return $value == $baseNetwork; + }); + if (! $baseNetworkExists) { + foreach ($baseNetwork as $network) { + $topLevel->get('networks')->put($network, [ + 'name' => $network, + 'external' => true, + ]); + } + } + } + + // Collect/create/update ports + $collectedPorts = collect([]); + if ($ports->count() > 0) { + foreach ($ports as $sport) { + if (is_string($sport) || is_numeric($sport)) { + $collectedPorts->push($sport); + } + if (is_array($sport)) { + $target = data_get($sport, 'target'); + $published = data_get($sport, 'published'); + $protocol = data_get($sport, 'protocol'); + $collectedPorts->push("$target:$published/$protocol"); + } + } + } + $originalResource->ports = $collectedPorts->implode(','); + $originalResource->save(); + + $networks_temp = collect(); + + if (! $use_network_mode) { + foreach ($networks as $key => $network) { + if (gettype($network) === 'string') { + // networks: + // - appwrite + $networks_temp->put($network, null); + } elseif (gettype($network) === 'array') { + // networks: + // default: + // ipv4_address: 192.168.203.254 + $networks_temp->put($key, $network); + } + } + foreach ($baseNetwork as $key => $network) { + $networks_temp->put($network, null); + } + } + + $normalEnvironments = $environment->diffKeys($allMagicEnvironments); + $normalEnvironments = $normalEnvironments->filter(function ($value, $key) { + return ! str($value)->startsWith('SERVICE_'); + }); + foreach ($normalEnvironments as $key => $value) { + $key = str($key); + $value = str($value); + $originalValue = $value; + $parsedValue = replaceVariables($value); + if ($value->startsWith('$SERVICE_')) { + $resource->environment_variables()->firstOrCreate([ + 'key' => $key, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + + continue; + } + if (! $value->startsWith('$')) { + continue; + } + if ($key->value() === $parsedValue->value()) { + $value = null; + $resource->environment_variables()->firstOrCreate([ + 'key' => $key, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } else { + if ($value->startsWith('$')) { + $isRequired = false; + if ($value->contains(':-')) { + $value = replaceVariables($value); + $key = $value->before(':'); + $value = $value->after(':-'); + } elseif ($value->contains('-')) { + $value = replaceVariables($value); + + $key = $value->before('-'); + $value = $value->after('-'); + } elseif ($value->contains(':?')) { + $value = replaceVariables($value); + + $key = $value->before(':'); + $value = $value->after(':?'); + $isRequired = true; + } elseif ($value->contains('?')) { + $value = replaceVariables($value); + + $key = $value->before('?'); + $value = $value->after('?'); + $isRequired = true; + } + if ($originalValue->value() === $value->value()) { + // This means the variable does not have a default value, so it needs to be created in Coolify + $parsedKeyValue = replaceVariables($value); + $resource->environment_variables()->firstOrCreate([ + 'key' => $parsedKeyValue, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'is_build_time' => false, + 'is_preview' => false, + 'is_required' => $isRequired, + ]); + // Add the variable to the environment so it will be shown in the deployable compose file + $environment[$parsedKeyValue->value()] = $value; + + continue; + } + $resource->environment_variables()->firstOrCreate([ + 'key' => $key, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + 'is_required' => $isRequired, + ]); + } + } + } + + // Add COOLIFY_RESOURCE_UUID to environment + if ($resource->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { + $coolifyEnvironments->put('COOLIFY_RESOURCE_UUID', "{$resource->uuid}"); + } + + // Add COOLIFY_CONTAINER_NAME to environment + if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', "{$containerName}"); + } + + if ($savedService->serviceType()) { + $fqdns = generateServiceSpecificFqdns($savedService); + } else { + $fqdns = collect(data_get($savedService, 'fqdns'))->filter(); + } + + $defaultLabels = defaultLabels( + id: $resource->id, + name: $containerName, + projectName: $resource->project()->name, + resourceName: $resource->name, + type: 'service', + subType: $isDatabase ? 'database' : 'application', + subId: $savedService->id, + subName: $savedService->human_name ?? $savedService->name, + environment: $resource->environment->name, + ); + + // Add COOLIFY_FQDN & COOLIFY_URL to environment + if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { + $fqdnsWithoutPort = $fqdns->map(function ($fqdn) { + return str($fqdn)->replace('http://', '')->replace('https://', '')->before(':'); + }); + $coolifyEnvironments->put('COOLIFY_FQDN', $fqdnsWithoutPort->implode(',')); + $urls = $fqdns->map(function ($fqdn) { + return str($fqdn)->after('://')->before(':')->prepend(str($fqdn)->before('://')->append('://')); + }); + $coolifyEnvironments->put('COOLIFY_URL', $urls->implode(',')); + } + add_coolify_default_environment_variables($resource, $coolifyEnvironments, $resource->environment_variables); + if ($environment->count() > 0) { + $environment = $environment->filter(function ($value, $key) { + return ! str($key)->startsWith('SERVICE_FQDN_'); + })->map(function ($value, $key) use ($resource) { + // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used + if (str($value)->isEmpty()) { + if ($resource->environment_variables()->where('key', $key)->exists()) { + $value = $resource->environment_variables()->where('key', $key)->first()->value; + } else { + $value = null; + } + } + + return $value; + }); + } + $serviceLabels = $labels->merge($defaultLabels); + if ($serviceLabels->count() > 0) { + $isContainerLabelEscapeEnabled = data_get($resource, 'is_container_label_escape_enabled'); + if ($isContainerLabelEscapeEnabled) { + $serviceLabels = $serviceLabels->map(function ($value, $key) { + return escapeDollarSign($value); + }); + } + } + if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { + $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; + $uuid = $resource->uuid; + $network = data_get($resource, 'destination.network'); + if ($shouldGenerateLabelsExactly) { + switch ($server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image + )); + break; + case ProxyTypes::CADDY->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $network, + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image, + predefinedPort: $predefinedPort + )); + break; + } + } else { + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image + )); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $network, + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image, + predefinedPort: $predefinedPort + )); + } + } + if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) { + $savedService->update(['exclude_from_status' => true]); + } + data_forget($service, 'volumes.*.content'); + data_forget($service, 'volumes.*.isDirectory'); + data_forget($service, 'volumes.*.is_directory'); + data_forget($service, 'exclude_from_hc'); + + $volumesParsed = $volumesParsed->map(function ($volume) { + data_forget($volume, 'content'); + data_forget($volume, 'is_directory'); + data_forget($volume, 'isDirectory'); + + return $volume; + }); + + $payload = collect($service)->merge([ + 'container_name' => $containerName, + 'restart' => $restart->value(), + 'labels' => $serviceLabels, + ]); + if (! $use_network_mode) { + $payload['networks'] = $networks_temp; + } + if ($ports->count() > 0) { + $payload['ports'] = $ports; + } + if ($volumesParsed->count() > 0) { + $payload['volumes'] = $volumesParsed; + } + if ($environment->count() > 0 || $coolifyEnvironments->count() > 0) { + $payload['environment'] = $environment->merge($coolifyEnvironments); + } + if ($logging) { + $payload['logging'] = $logging; + } + if ($depends_on->count() > 0) { + $payload['depends_on'] = $depends_on; + } + + $parsedServices->put($serviceName, $payload); + } + $topLevel->put('services', $parsedServices); + + $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets']; + + $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) { + return array_search($key, $customOrder); + }); + + $resource->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2); + data_forget($resource, 'environment_variables'); + data_forget($resource, 'environment_variables_preview'); + $resource->save(); + + return $topLevel; +} diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 1e1d2a073..cf12a28a5 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -1,7 +1,6 @@ image = $updatedImage; $resource->save(); } + + $serviceName = str($resource->name)->upper()->replace('-', '_'); + $resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%")->delete(); + $resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%")->delete(); + if ($resource->fqdn) { $resourceFqdns = str($resource->fqdn)->explode(','); - if ($resourceFqdns->count() === 1) { - $resourceFqdns = $resourceFqdns->first(); - $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_'); - $fqdn = Url::fromString($resourceFqdns); - $port = $fqdn->getPort(); - $path = $fqdn->getPath(); - $fqdn = $fqdn->getScheme().'://'.$fqdn->getHost(); - $fqdnValue = ($path === '/') ? $fqdn : $fqdn.$path; - EnvironmentVariable::updateOrCreate([ - 'resourceable_type' => Service::class, - 'resourceable_id' => $resource->service_id, - 'key' => $variableName, - ], [ - 'value' => $fqdnValue, - 'is_build_time' => false, - 'is_preview' => false, - ]); - if ($port) { - $variableName = $variableName."_$port"; - EnvironmentVariable::updateOrCreate([ - 'resourceable_type' => Service::class, - 'resourceable_id' => $resource->service_id, - 'key' => $variableName, - ], [ - 'value' => $fqdnValue, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } - $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_'); - $url = Url::fromString($fqdn); - $port = $url->getPort(); - $path = $url->getPath(); - $url = $url->getHost(); - $urlValue = str($fqdn)->after('://'); - if ($path !== '/') { - $urlValue = $urlValue.$path; - } - EnvironmentVariable::updateOrCreate([ + $resourceFqdns = $resourceFqdns->first(); + $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_'); + $url = Url::fromString($resourceFqdns); + $port = $url->getPort(); + $path = $url->getPath(); + $urlValue = $url->getScheme().'://'.$url->getHost(); + $urlValue = ($path === '/') ? $urlValue : $urlValue.$path; + $resource->service->environment_variables()->updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $urlValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + if ($port) { + $variableName = $variableName."_$port"; + $resource->service->environment_variables()->updateOrCreate([ 'resourceable_type' => Service::class, 'resourceable_id' => $resource->service_id, 'key' => $variableName, @@ -164,114 +148,37 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) 'is_build_time' => false, 'is_preview' => false, ]); - if ($port) { - $variableName = $variableName."_$port"; - EnvironmentVariable::updateOrCreate([ - 'resourceable_type' => Service::class, - 'resourceable_id' => $resource->service_id, - 'key' => $variableName, - ], [ - 'value' => $urlValue, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } - } elseif ($resourceFqdns->count() > 1) { - foreach ($resourceFqdns as $fqdn) { - $host = Url::fromString($fqdn); - $port = $host->getPort(); - $url = $host->getHost(); - $path = $host->getPath(); - $host = $host->getScheme().'://'.$host->getHost(); - if ($port) { - $port_envs = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', 'like', "SERVICE_FQDN_%_$port") - ->get(); - foreach ($port_envs as $port_env) { - $service_fqdn = str($port_env->key)->beforeLast('_')->after('SERVICE_FQDN_'); - $env = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', 'SERVICE_FQDN_'.$service_fqdn) - ->first(); - if ($env) { - if ($path === '/') { - $env->value = $host; - } else { - $env->value = $host.$path; - } - $env->save(); - } - if ($path === '/') { - $port_env->value = $host; - } else { - $port_env->value = $host.$path; - } - $port_env->save(); - } - $port_envs_url = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', 'like', "SERVICE_URL_%_$port") - ->get(); - foreach ($port_envs_url as $port_env_url) { - $service_url = str($port_env_url->key)->beforeLast('_')->after('SERVICE_URL_'); - $env = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', 'SERVICE_URL_'.$service_url) - ->first(); - if ($env) { - if ($path === '/') { - $env->value = $url; - } else { - $env->value = $url.$path; - } - $env->save(); - } - if ($path === '/') { - $port_env_url->value = $url; - } else { - $port_env_url->value = $url.$path; - } - $port_env_url->save(); - } - } else { - $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_'); - $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', $variableName) - ->first(); - $fqdn = Url::fromString($fqdn); - $fqdn = $fqdn->getScheme().'://'.$fqdn->getHost().$fqdn->getPath(); - if ($generatedEnv) { - $generatedEnv->value = $fqdn; - $generatedEnv->save(); - } - $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_'); - $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', $variableName) - ->first(); - $url = Url::fromString($fqdn); - $url = $url->getHost().$url->getPath(); - if ($generatedEnv) { - $url = str($fqdn)->after('://'); - $generatedEnv->value = $url; - $generatedEnv->save(); - } - } - } } - } else { - // If FQDN is removed, delete the corresponding environment variables - $serviceName = str($resource->name)->upper()->replace('-', '_'); - EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%") - ->delete(); - EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%") - ->delete(); + $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_'); + $fqdn = Url::fromString($resourceFqdns); + $port = $fqdn->getPort(); + $path = $fqdn->getPath(); + $fqdn = $fqdn->getHost(); + $fqdnValue = str($fqdn)->after('://'); + if ($path !== '/') { + $fqdnValue = $fqdnValue.$path; + } + $resource->service->environment_variables()->updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $fqdnValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + if ($port) { + $variableName = $variableName."_$port"; + $resource->service->environment_variables()->updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $fqdnValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } } } catch (\Throwable $e) { return handleError($e); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7ce511f2c..1e9b95288 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2918,10 +2918,18 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } -function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection +function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $preview_id = null, bool $isOneClick = false): Collection { $isApplication = $resource instanceof Application; $isService = $resource instanceof Service; + if ($isApplication) { + return applicationParser($resource, $pull_request_id, $preview_id); + } + if ($isService) { + return serviceParser($resource, $isOneClick); + } + + return collect([]); $uuid = data_get($resource, 'uuid'); $compose = data_get($resource, 'docker_compose_raw'); @@ -3078,14 +3086,25 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $fqdn = generateFqdn($server, "$uuid"); } } elseif ($isService) { - if (blank($savedService->fqdn)) { - if ($fqdnFor) { - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + if ($isOneClick) { + if (blank($savedService->fqdn)) { + if ($fqdnFor) { + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + } else { + $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); + } } else { - $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); + $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); } + } else { - $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); + // For services which are not one-click, if no explicit FQDN is set, leave SERVICE_FQDN_ variables empty + if (blank($savedService->fqdn)) { + $fqdn = ''; + } else { + $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); + } + ray($fqdn); } } @@ -3141,51 +3160,41 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $key = str($key); $value = replaceVariables($value); $command = parseCommandFromMagicEnvVariable($key); - $found = $resource->environment_variables()->where('key', $key->value())->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first(); - if ($found) { - continue; - } if ($command->value() === 'FQDN') { - if ($isApplication && $resource->build_pack === 'dockercompose') { - continue; + if ($isOneClick) { + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); + } + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); } - $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); } elseif ($command->value() === 'URL') { - if ($isApplication && $resource->build_pack === 'dockercompose') { - continue; + if ($isOneClick) { + $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); + } + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); } - // For services, only generate URL if explicit FQDN is set - if ($isService && blank($savedService->fqdn)) { - continue; - } - $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); } else { $value = generateEnvValue($command, $resource); $resource->environment_variables()->firstOrCreate([ @@ -3279,12 +3288,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); if ($applicationFound) { $savedService = $applicationFound; - // $savedService = ServiceDatabase::firstOrCreate([ - // 'name' => $applicationFound->name, - // 'image' => $applicationFound->image, - // 'service_id' => $applicationFound->service_id, - // ]); - // $applicationFound->delete(); } else { $savedService = ServiceDatabase::firstOrCreate([ 'name' => $serviceName, @@ -3550,7 +3553,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $normalEnvironments = $normalEnvironments->filter(function ($value, $key) { return ! str($value)->startsWith('SERVICE_'); }); - foreach ($normalEnvironments as $key => $value) { $key = str($key); $value = str($value); @@ -3669,6 +3671,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $fqdns = data_get($domains, "$serviceName.domain"); // Generate SERVICE_FQDN & SERVICE_URL for dockercompose if ($resource->build_pack === 'dockercompose') { + foreach ($domains as $forServiceName => $domain) { $parsedDomain = data_get($domain, 'domain'); if (filled($parsedDomain)) { @@ -3731,7 +3734,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } else { $fqdns = collect(data_get($savedService, 'fqdns'))->filter(); } - $defaultLabels = defaultLabels( id: $resource->id, name: $containerName, @@ -3757,7 +3759,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(',')); } add_coolify_default_environment_variables($resource, $coolifyEnvironments, $resource->environment_variables); - if ($environment->count() > 0) { $environment = $environment->filter(function ($value, $key) { return ! str($key)->startsWith('SERVICE_FQDN_'); From 1ddec358a55793bd12a2d4f99d6b866c756dbb37 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 2 Aug 2025 15:45:21 +0200 Subject: [PATCH 0143/1717] feat(input): add autofocus attribute to input component for improved accessibility --- app/View/Components/Forms/Input.php | 1 + resources/views/components/forms/input.blade.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index 7283ef20f..a7bd87949 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -25,6 +25,7 @@ public function __construct( public string $autocomplete = 'off', public ?int $minlength = null, public ?int $maxlength = null, + public bool $autofocus = false, ) {} public function render(): View|Closure|string diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index 20e6485bf..799a5f110 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -32,7 +32,7 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" - aria-placeholder="{{ $attributes->get('placeholder') }}"> + aria-placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif>
@else @@ -45,7 +45,7 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}" maxlength="{{ $attributes->get('maxlength') }}" @if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}" - placeholder="{{ $attributes->get('placeholder') }}"> + placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif> @endif @if (!$label && $helper) From a2c5f4b9d1f9afa68db054ad9f392533a9c29977 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 2 Aug 2025 15:50:09 +0200 Subject: [PATCH 0144/1717] refactor(public-git-repository): enhance form structure and add autofocus to repository URL input --- .../new/public-git-repository.blade.php | 162 ++++++++++-------- 1 file changed, 86 insertions(+), 76 deletions(-) diff --git a/resources/views/livewire/project/new/public-git-repository.blade.php b/resources/views/livewire/project/new/public-git-repository.blade.php index ad48b6d4d..e07cfaaae 100644 --- a/resources/views/livewire/project/new/public-git-repository.blade.php +++ b/resources/views/livewire/project/new/public-git-repository.blade.php @@ -1,84 +1,94 @@ -
+

Create a new Application

Deploy any public Git repositories.
+ +
-
-
- - - Check repository - -
-
- For example application deployments, checkout Coolify - Examples. -
- @if ($branchFound) - @if ($rate_limit_remaining && $rate_limit_reset) -
-
Rate Limit
- -
- @endif -
-
- @if ($git_source === 'other') - - @else - - @endif - - - - - - - @if ($isStatic) - - @endif -
- @if ($build_pack === 'dockercompose') - - - Compose file location in your repository:{{ Str::start($base_directory . $docker_compose_location, '/') }} - @else - - @endif - @if ($show_is_static) - -
- -
- @endif - {{--
- -
--}} - {{-- @if ($build_pack === 'dockercompose' && isDev()) -
If you choose Docker Compose based deployments, you cannot - change it afterwards.
- - @endif --}} -
- - Continue - - @endif +
+ + + Check repository + +
+
+ For example application deployments, checkout Coolify + Examples.
+ + @if ($branchFound) + @if ($rate_limit_remaining && $rate_limit_reset) +
+
Rate Limit
+ +
+ @endif + + +
+
+
+ @if ($git_source === 'other') + + @else + + @endif + + + + + + + @if ($isStatic) + + @endif +
+ @if ($build_pack === 'dockercompose') +
+ + +
+ + Compose file location in your repository: +
+
+ @else + + @endif + @if ($show_is_static) + +
+ +
+ @endif + {{--
+ +
--}} + {{-- @if ($build_pack === 'dockercompose' && isDev()) +
If you choose Docker Compose based deployments, you cannot + change it afterwards.
+ + @endif --}} +
+ + Continue + +
+ @endif
From e2518e53d9d5842265b0dd95b9102af1680e41df Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:21:15 +0200 Subject: [PATCH 0145/1717] refactor(public-git-repository): remove commented-out code for cleaner template --- .../livewire/project/new/public-git-repository.blade.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/resources/views/livewire/project/new/public-git-repository.blade.php b/resources/views/livewire/project/new/public-git-repository.blade.php index e07cfaaae..ba9e37784 100644 --- a/resources/views/livewire/project/new/public-git-repository.blade.php +++ b/resources/views/livewire/project/new/public-git-repository.blade.php @@ -76,15 +76,6 @@ helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
@endif - {{--
- -
--}} - {{-- @if ($build_pack === 'dockercompose' && isDev()) -
If you choose Docker Compose based deployments, you cannot - change it afterwards.
- - @endif --}}
Continue From e8892b3d29290b69b796a6f2bddeab84d323b392 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:46:59 +0200 Subject: [PATCH 0146/1717] feat(core): finally fqdn is fqdn and url is url. haha --- app/Livewire/Project/Application/General.php | 18 +- .../Project/Application/PreviewsCompose.php | 2 +- app/Livewire/Project/CloneMe.php | 2 +- app/Livewire/Project/Resource/Create.php | 2 +- .../Project/Shared/ResourceOperations.php | 2 +- app/Models/Application.php | 18 +- app/Models/Service.php | 6 +- bootstrap/helpers/docker.php | 8 +- bootstrap/helpers/parsers.php | 235 +- bootstrap/helpers/shared.php | 1914 +++++++++-------- 10 files changed, 1159 insertions(+), 1048 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 2eadbaa0e..e7515b23f 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -227,7 +227,19 @@ public function loadComposeFile($isInit = false, $showToast = true) return; } - $this->application->parse(); + + // Refresh parsedServiceDomains to reflect any changes in docker_compose_domains + $this->application->refresh(); + $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; + ray($this->parsedServiceDomains); + // Convert service names with dots to use underscores for HTML form binding + $sanitizedDomains = []; + foreach ($this->parsedServiceDomains as $serviceName => $domain) { + $sanitizedKey = str($serviceName)->slug('_')->toString(); + $sanitizedDomains[$sanitizedKey] = $domain; + } + $this->parsedServiceDomains = $sanitizedDomains; + $showToast && $this->dispatch('success', 'Docker compose file loaded.'); $this->dispatch('compose_loaded'); $this->dispatch('refreshStorages'); @@ -245,7 +257,7 @@ public function loadComposeFile($isInit = false, $showToast = true) public function generateDomain(string $serviceName) { $uuid = new Cuid2; - $domain = generateFqdn($this->application->destination->server, $uuid); + $domain = generateUrl(server: $this->application->destination->server, random: $uuid); $sanitizedKey = str($serviceName)->slug('_')->toString(); $this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain; @@ -315,7 +327,7 @@ public function getWildcardDomain() { $server = data_get($this->application, 'destination.server'); if ($server) { - $fqdn = generateFqdn($server, $this->application->uuid); + $fqdn = generateFqdn(server: $server, random: $this->application->uuid, parserVersion: $this->application->compose_parsing_version); $this->application->fqdn = $fqdn; $this->application->save(); $this->resetDefaultLabels(); diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index 334d96cad..5938b2944 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -48,7 +48,7 @@ public function generate() $random = new Cuid2; // Generate a unique domain like main app services do - $generated_fqdn = generateFqdn($server, $random); + $generated_fqdn = generateFqdn(server: $server, random: $random, parserVersion: $this->preview->application->compose_parsing_version); $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', str($generated_fqdn)->after('://'), $preview_fqdn); diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index a5d80a11a..57f4a0c68 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -129,7 +129,7 @@ public function clone(string $type) $uuid = (string) new Cuid2; $url = $application->fqdn; if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { - $url = generateFqdn($this->server, $uuid); + $url = generateFqdn(server: $this->server, random: $uuid, parserVersion: $application->compose_parsing_version); } $newApplication = $application->replicate([ diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index d8e397e59..e7cff4f29 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -102,7 +102,7 @@ public function mount() } }); } - $service->parse(isNew: true, isOneClick: true); + $service->parse(isNew: true); return redirect()->route('project.service.configuration', [ 'service_uuid' => $service->uuid, diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index c8916bf19..853dbe57a 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -61,7 +61,7 @@ public function cloneTo($destination_id) $url = $this->resource->fqdn; if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { - $url = generateFqdn($server, $uuid); + $url = generateFqdn(server: $server, random: $uuid, parserVersion: $this->resource->compose_parsing_version); } $new_resource = $this->resource->replicate([ diff --git a/app/Models/Application.php b/app/Models/Application.php index ed97454d2..f74ed89d1 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -111,7 +111,7 @@ class Application extends BaseModel { use HasConfiguration, HasFactory, SoftDeletes; - private static $parserVersion = '4'; + private static $parserVersion = '5'; protected $guarded = []; @@ -1442,7 +1442,21 @@ public function loadComposeFile($isInit = false) $parsedServices = $this->parse(); if ($this->docker_compose_domains) { $json = collect(json_decode($this->docker_compose_domains)); - $names = collect(data_get($parsedServices, 'services'))->keys()->toArray(); + foreach ($json as $key => $value) { + if (str($key)->contains('-')) { + $key = str($key)->replace('-', '_'); + } + $json->put((string) $key, $value); + } + $services = collect(data_get($parsedServices, 'services', [])); + foreach ($services as $name => $service) { + if (str($name)->contains('-')) { + $replacedName = str($name)->replace('-', '_'); + $services->put((string) $replacedName, $service); + $services->forget((string) $name); + } + } + $names = collect($services)->keys()->toArray(); $jsonNames = $json->keys()->toArray(); $diff = array_diff($jsonNames, $names); $json = $json->filter(function ($value, $key) use ($diff) { diff --git a/app/Models/Service.php b/app/Models/Service.php index 4bef46642..ed3241f46 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -42,7 +42,7 @@ class Service extends BaseModel { use HasFactory, SoftDeletes; - private static $parserVersion = '4'; + private static $parserVersion = '5'; protected $guarded = []; @@ -1274,10 +1274,10 @@ public function saveComposeConfigs() instant_remote_process($commands, $this->server); } - public function parse(bool $isNew = false, bool $isOneClick = false): Collection + public function parse(bool $isNew = false): Collection { if ((int) $this->compose_parsing_version >= 3) { - return serviceParser($this, isOneClick: $isOneClick); + return serviceParser($this); } elseif ($this->docker_compose_raw) { return parseDockerComposeFile($this, $isNew); } else { diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 739f98f22..1737ca714 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -256,12 +256,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) if (str($MINIO_BROWSER_REDIRECT_URL->value ?? '')->isEmpty()) { $MINIO_BROWSER_REDIRECT_URL->update([ - 'value' => generateFqdn($server, 'console-'.$uuid, true), + 'value' => generateFqdn(server: $server, random: 'console-'.$uuid, parserVersion: $resource->compose_parsing_version, forceHttps: true), ]); } if (str($MINIO_SERVER_URL->value ?? '')->isEmpty()) { $MINIO_SERVER_URL->update([ - 'value' => generateFqdn($server, 'minio-'.$uuid, true), + 'value' => generateFqdn(server: $server, random: 'minio-'.$uuid, parserVersion: $resource->compose_parsing_version, forceHttps: true), ]); } $payload = collect([ @@ -279,12 +279,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) if (str($LOGTO_ENDPOINT->value ?? '')->isEmpty()) { $LOGTO_ENDPOINT->update([ - 'value' => generateFqdn($server, 'logto-'.$uuid), + 'value' => generateFqdn(server: $server, random: 'logto-'.$uuid, parserVersion: $resource->compose_parsing_version), ]); } if (str($LOGTO_ADMIN_ENDPOINT->value ?? '')->isEmpty()) { $LOGTO_ADMIN_ENDPOINT->update([ - 'value' => generateFqdn($server, 'logto-admin-'.$uuid), + 'value' => generateFqdn(server: $server, random: 'logto-admin-'.$uuid, parserVersion: $resource->compose_parsing_version), ]); } $payload = collect([ diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 649de731d..e5e0cbe86 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -16,7 +16,7 @@ use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; -function applicationParser(Application $resource, int $pull_request_id = 0, ?int $preview_id = null, bool $isOneClick = false): Collection +function applicationParser(Application $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection { $uuid = data_get($resource, 'uuid'); $compose = data_get($resource, 'docker_compose_raw'); @@ -100,6 +100,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int } } // Get magic environments where we need to preset the FQDN + // for example SERVICE_FQDN_APP_3000 (without a value) if ($key->startsWith('SERVICE_FQDN_')) { // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 if (substr_count(str($key)->value(), '_') === 3) { @@ -111,7 +112,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int } $fqdn = $resource->fqdn; if (blank($resource->fqdn)) { - $fqdn = generateFqdn($server, "$uuid"); + $fqdn = generateFqdn(server: $server, random: "$uuid", parserVersion: $resource->compose_parsing_version); } if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { @@ -160,44 +161,58 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); if ($magicEnvironments->count() > 0) { + // Generate Coolify environment variables foreach ($magicEnvironments as $key => $value) { $key = str($key); $value = replaceVariables($value); $command = parseCommandFromMagicEnvVariable($key); if ($command->value() === 'FQDN') { - if ($isOneClick) { - $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + if (str($fqdnFor)->contains('-')) { + $fqdnFor = str($fqdnFor)->replace('-', '_'); + } + $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version); + $url = generateUrl(server: $server, random: "$fqdnFor-$uuid"); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + if ($resource->build_pack === 'dockercompose') { + $domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]); + // Put URL in the domains array instead of FQDN + $domains->put((string) $fqdnFor, [ + 'domain' => $url, ]); + $resource->docker_compose_domains = $domains->toJson(); + $resource->save(); } } elseif ($command->value() === 'URL') { - if ($isOneClick) { - $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, + $urlFor = $key->after('SERVICE_URL_')->lower()->value(); + if (str($urlFor)->contains('-')) { + $urlFor = str($urlFor)->replace('-', '_'); + } + $url = generateUrl(server: $server, random: "$urlFor-$uuid"); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $url, + 'is_build_time' => false, + 'is_preview' => false, + ]); + if ($resource->build_pack === 'dockercompose') { + $domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]); + $domains->put((string) $urlFor, [ + 'domain' => $url, ]); + $resource->docker_compose_domains = $domains->toJson(); + $resource->save(); } } else { $value = generateEnvValue($command, $resource); @@ -599,7 +614,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int } else { $domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]); } - ray($domains); $fqdns = data_get($domains, "$serviceName.domain"); // Generate SERVICE_FQDN & SERVICE_URL for dockercompose if ($resource->build_pack === 'dockercompose') { @@ -849,7 +863,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int return $topLevel; } -function serviceParser(Service $resource, bool $isOneClick = false): Collection +function serviceParser(Service $resource): Collection { $uuid = data_get($resource, 'uuid'); $compose = data_get($resource, 'docker_compose_raw'); @@ -977,49 +991,86 @@ function serviceParser(Service $resource, bool $isOneClick = false): Collection } } // Get magic environments where we need to preset the FQDN - if ($key->startsWith('SERVICE_FQDN_')) { + if ($key->startsWith('SERVICE_FQDN_') || $key->startsWith('SERVICE_URL_')) { // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 if (substr_count(str($key)->value(), '_') === 3) { - $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); + if ($key->startsWith('SERVICE_FQDN_')) { + $urlFor = null; + $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); + } + if ($key->startsWith('SERVICE_URL_')) { + $fqdnFor = null; + $urlFor = $key->after('SERVICE_URL_')->beforeLast('_')->lower()->value(); + } $port = $key->afterLast('_')->value(); } else { - $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + if ($key->startsWith('SERVICE_FQDN_')) { + $urlFor = null; + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + } + if ($key->startsWith('SERVICE_URL_')) { + $fqdnFor = null; + $urlFor = $key->after('SERVICE_URL_')->lower()->value(); + } $port = null; } - if ($isOneClick) { - if (blank($savedService->fqdn)) { - if ($fqdnFor) { - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - } else { - $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); - } + // if ($isOneClick) { + if (blank($savedService->fqdn)) { + if ($fqdnFor) { + $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version); } else { - $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); + $fqdn = generateFqdn(server: $server, random: "{$savedService->name}-$uuid", parserVersion: $resource->compose_parsing_version); + } + if ($urlFor) { + $url = generateUrl($server, "$urlFor-$uuid"); + } else { + $url = generateUrl($server, "{$savedService->name}-$uuid"); } - } else { - // For services which are not one-click, if no explicit FQDN is set, leave SERVICE_FQDN_ variables empty - if (blank($savedService->fqdn)) { - $fqdn = ''; - } else { - $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); - } + $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); + $url = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); } + + // } else { + // // For services which are not one-click, if no explicit FQDN is set, leave SERVICE_FQDN_ variables empty + // if (blank($savedService->fqdn)) { + // $fqdn = ''; + // } else { + // $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); + // } + // } if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { $path = $value->value(); if ($path !== '/') { $fqdn = "$fqdn$path"; + $url = "$url$path"; } } $fqdnWithPort = $fqdn; - if ($port) { + $urlWithPort = $url; + if ($fqdn && $port) { $fqdnWithPort = "$fqdn:$port"; } + if ($url && $port) { + $urlWithPort = "$url:$port"; + } + ray("urlWithPort: $urlWithPort, fqdnWithPort: $fqdnWithPort", "parserVersion: $resource->compose_parsing_version", 'isVersionGreaterThan4207: '.version_compare('4.0.0-beta.420.7', config('constants.coolify.version'), '>=')); if (is_null($savedService->fqdn)) { - $savedService->fqdn = $fqdnWithPort; + if ((int) $resource->compose_parsing_version >= 5 && version_compare(config('constants.coolify.version'), '4.0.0-beta.420.7', '>=')) { + if ($fqdnFor) { + ray("setting fqdn(fqdnWithPort) to $fqdnWithPort"); + $savedService->fqdn = $fqdnWithPort; + } + if ($urlFor) { + ray("setting fqdn(urlWithPort) to $urlWithPort"); + $savedService->fqdn = $urlWithPort; + } + } else { + ray("setting fqdn(fqdnWithPort) old parser version to $fqdnWithPort"); + $savedService->fqdn = $fqdnWithPort; + } $savedService->save(); } - if (substr_count(str($key)->value(), '_') === 2) { $resource->environment_variables()->updateOrCreate([ 'key' => $key->value(), @@ -1030,6 +1081,15 @@ function serviceParser(Service $resource, bool $isOneClick = false): Collection 'is_build_time' => false, 'is_preview' => false, ]); + $resource->environment_variables()->updateOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $url, + 'is_build_time' => false, + 'is_preview' => false, + ]); } if (substr_count(str($key)->value(), '_') === 3) { $newKey = str($key)->beforeLast('_'); @@ -1042,6 +1102,15 @@ function serviceParser(Service $resource, bool $isOneClick = false): Collection 'is_build_time' => false, 'is_preview' => false, ]); + $resource->environment_variables()->updateOrCreate([ + 'key' => $newKey->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $url, + 'is_build_time' => false, + 'is_preview' => false, + ]); } } } @@ -1053,40 +1122,36 @@ function serviceParser(Service $resource, bool $isOneClick = false): Collection $value = replaceVariables($value); $command = parseCommandFromMagicEnvVariable($key); if ($command->value() === 'FQDN') { - if ($isOneClick) { - $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); } + $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); } elseif ($command->value() === 'URL') { - if ($isOneClick) { - $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); + $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); } + $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version); + $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); } else { $value = generateEnvValue($command, $resource); $resource->environment_variables()->firstOrCreate([ diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1e9b95288..624e0b0d1 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -402,7 +402,7 @@ function data_get_str($data, $key, $default = null): Stringable return str($str); } -function generateFqdn(Server $server, string $random, bool $forceHttps = false): string +function generateUrl(Server $server, string $random, bool $forceHttps = false): string { $wildcard = data_get($server, 'settings.wildcard_domain'); if (is_null($wildcard) || $wildcard === '') { @@ -418,6 +418,26 @@ function generateFqdn(Server $server, string $random, bool $forceHttps = false): return "$scheme://{$random}.$host$path"; } +function generateFqdn(Server $server, string $random, bool $forceHttps = false, int $parserVersion = 4): string +{ + $wildcard = data_get($server, 'settings.wildcard_domain'); + if (is_null($wildcard) || $wildcard === '') { + $wildcard = sslip($server); + } + $url = Url::fromString($wildcard); + $host = $url->getHost(); + $path = $url->getPath() === '/' ? '' : $url->getPath(); + $scheme = $url->getScheme(); + if ($forceHttps) { + $scheme = 'https'; + } + + if ($parserVersion >= 5 && version_compare(config('constants.coolify.version'), '4.0.0-beta.420.7', '>=')) { + return "{$random}.$host$path"; + } + + return "$scheme://{$random}.$host$path"; +} function sslip(Server $server) { if (isDev() && $server->id === 0) { @@ -2918,1008 +2938,1008 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } -function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $preview_id = null, bool $isOneClick = false): Collection -{ - $isApplication = $resource instanceof Application; - $isService = $resource instanceof Service; - if ($isApplication) { - return applicationParser($resource, $pull_request_id, $preview_id); - } - if ($isService) { - return serviceParser($resource, $isOneClick); - } +// function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection +// { +// $isApplication = $resource instanceof Application; +// $isService = $resource instanceof Service; +// if ($isApplication) { +// return applicationParser($resource, $pull_request_id, $preview_id); +// } +// if ($isService) { +// return serviceParser($resource); +// } - return collect([]); +// return collect([]); - $uuid = data_get($resource, 'uuid'); - $compose = data_get($resource, 'docker_compose_raw'); - if (! $compose) { - return collect([]); - } +// $uuid = data_get($resource, 'uuid'); +// $compose = data_get($resource, 'docker_compose_raw'); +// if (! $compose) { +// return collect([]); +// } - if ($isApplication) { - $pullRequestId = $pull_request_id; - $isPullRequest = $pullRequestId == 0 ? false : true; - $server = data_get($resource, 'destination.server'); - $fileStorages = $resource->fileStorages(); - } elseif ($isService) { - $server = data_get($resource, 'server'); - $allServices = get_service_templates(); - } else { - return collect([]); - } +// if ($isApplication) { +// $pullRequestId = $pull_request_id; +// $isPullRequest = $pullRequestId == 0 ? false : true; +// $server = data_get($resource, 'destination.server'); +// $fileStorages = $resource->fileStorages(); +// } elseif ($isService) { +// $server = data_get($resource, 'server'); +// $allServices = get_service_templates(); +// } else { +// return collect([]); +// } - try { - $yaml = Yaml::parse($compose); - } catch (\Exception) { - return collect([]); - } - $services = data_get($yaml, 'services', collect([])); - $topLevel = collect([ - 'volumes' => collect(data_get($yaml, 'volumes', [])), - 'networks' => collect(data_get($yaml, 'networks', [])), - 'configs' => collect(data_get($yaml, 'configs', [])), - 'secrets' => collect(data_get($yaml, 'secrets', [])), - ]); - // If there are predefined volumes, make sure they are not null - if ($topLevel->get('volumes')->count() > 0) { - $temp = collect([]); - foreach ($topLevel['volumes'] as $volumeName => $volume) { - if (is_null($volume)) { - continue; - } - $temp->put($volumeName, $volume); - } - $topLevel['volumes'] = $temp; - } - // Get the base docker network - $baseNetwork = collect([$uuid]); - if ($isApplication && $isPullRequest) { - $baseNetwork = collect(["{$uuid}-{$pullRequestId}"]); - } +// try { +// $yaml = Yaml::parse($compose); +// } catch (\Exception) { +// return collect([]); +// } +// $services = data_get($yaml, 'services', collect([])); +// $topLevel = collect([ +// 'volumes' => collect(data_get($yaml, 'volumes', [])), +// 'networks' => collect(data_get($yaml, 'networks', [])), +// 'configs' => collect(data_get($yaml, 'configs', [])), +// 'secrets' => collect(data_get($yaml, 'secrets', [])), +// ]); +// // If there are predefined volumes, make sure they are not null +// if ($topLevel->get('volumes')->count() > 0) { +// $temp = collect([]); +// foreach ($topLevel['volumes'] as $volumeName => $volume) { +// if (is_null($volume)) { +// continue; +// } +// $temp->put($volumeName, $volume); +// } +// $topLevel['volumes'] = $temp; +// } +// // Get the base docker network +// $baseNetwork = collect([$uuid]); +// if ($isApplication && $isPullRequest) { +// $baseNetwork = collect(["{$uuid}-{$pullRequestId}"]); +// } - $parsedServices = collect([]); +// $parsedServices = collect([]); - $allMagicEnvironments = collect([]); - foreach ($services as $serviceName => $service) { - $predefinedPort = null; - $magicEnvironments = collect([]); - $image = data_get_str($service, 'image'); - $environment = collect(data_get($service, 'environment', [])); - $buildArgs = collect(data_get($service, 'build.args', [])); - $environment = $environment->merge($buildArgs); - $isDatabase = isDatabaseImage($image, $service); +// $allMagicEnvironments = collect([]); +// foreach ($services as $serviceName => $service) { +// $predefinedPort = null; +// $magicEnvironments = collect([]); +// $image = data_get_str($service, 'image'); +// $environment = collect(data_get($service, 'environment', [])); +// $buildArgs = collect(data_get($service, 'build.args', [])); +// $environment = $environment->merge($buildArgs); +// $isDatabase = isDatabaseImage($image, $service); - if ($isService) { - $containerName = "$serviceName-{$resource->uuid}"; +// if ($isService) { +// $containerName = "$serviceName-{$resource->uuid}"; - if ($serviceName === 'registry') { - $tempServiceName = 'docker-registry'; - } else { - $tempServiceName = $serviceName; - } - if (str(data_get($service, 'image'))->contains('glitchtip')) { - $tempServiceName = 'glitchtip'; - } - if ($serviceName === 'supabase-kong') { - $tempServiceName = 'supabase'; - } - $serviceDefinition = data_get($allServices, $tempServiceName); - $predefinedPort = data_get($serviceDefinition, 'port'); - if ($serviceName === 'plausible') { - $predefinedPort = '8000'; - } - if ($isDatabase) { - $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); - if ($applicationFound) { - $savedService = $applicationFound; - } else { - $savedService = ServiceDatabase::firstOrCreate([ - 'name' => $serviceName, - 'service_id' => $resource->id, - ]); - } - } else { - $savedService = ServiceApplication::firstOrCreate([ - 'name' => $serviceName, - 'service_id' => $resource->id, - ], [ - 'is_gzip_enabled' => true, - ]); - } - // Check if image changed - if ($savedService->image !== $image) { - $savedService->image = $image; - $savedService->save(); - } - // Pocketbase does not need gzip for SSE. - if (str($savedService->image)->contains('pocketbase') && $savedService->is_gzip_enabled) { - $savedService->is_gzip_enabled = false; - $savedService->save(); - } - } +// if ($serviceName === 'registry') { +// $tempServiceName = 'docker-registry'; +// } else { +// $tempServiceName = $serviceName; +// } +// if (str(data_get($service, 'image'))->contains('glitchtip')) { +// $tempServiceName = 'glitchtip'; +// } +// if ($serviceName === 'supabase-kong') { +// $tempServiceName = 'supabase'; +// } +// $serviceDefinition = data_get($allServices, $tempServiceName); +// $predefinedPort = data_get($serviceDefinition, 'port'); +// if ($serviceName === 'plausible') { +// $predefinedPort = '8000'; +// } +// if ($isDatabase) { +// $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); +// if ($applicationFound) { +// $savedService = $applicationFound; +// } else { +// $savedService = ServiceDatabase::firstOrCreate([ +// 'name' => $serviceName, +// 'service_id' => $resource->id, +// ]); +// } +// } else { +// $savedService = ServiceApplication::firstOrCreate([ +// 'name' => $serviceName, +// 'service_id' => $resource->id, +// ], [ +// 'is_gzip_enabled' => true, +// ]); +// } +// // Check if image changed +// if ($savedService->image !== $image) { +// $savedService->image = $image; +// $savedService->save(); +// } +// // Pocketbase does not need gzip for SSE. +// if (str($savedService->image)->contains('pocketbase') && $savedService->is_gzip_enabled) { +// $savedService->is_gzip_enabled = false; +// $savedService->save(); +// } +// } - $environment = collect(data_get($service, 'environment', [])); - $buildArgs = collect(data_get($service, 'build.args', [])); - $environment = $environment->merge($buildArgs); +// $environment = collect(data_get($service, 'environment', [])); +// $buildArgs = collect(data_get($service, 'build.args', [])); +// $environment = $environment->merge($buildArgs); - // convert environment variables to one format - $environment = convertToKeyValueCollection($environment); +// // convert environment variables to one format +// $environment = convertToKeyValueCollection($environment); - // Add Coolify defined environments - $allEnvironments = $resource->environment_variables()->get(['key', 'value']); +// // Add Coolify defined environments +// $allEnvironments = $resource->environment_variables()->get(['key', 'value']); - $allEnvironments = $allEnvironments->mapWithKeys(function ($item) { - return [$item['key'] => $item['value']]; - }); - // filter and add magic environments - foreach ($environment as $key => $value) { - // Get all SERVICE_ variables from keys and values - $key = str($key); - $value = str($value); - $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; - preg_match_all($regex, $value, $valueMatches); - if (count($valueMatches[1]) > 0) { - foreach ($valueMatches[1] as $match) { - $match = replaceVariables($match); - if ($match->startsWith('SERVICE_')) { - if ($magicEnvironments->has($match->value())) { - continue; - } - $magicEnvironments->put($match->value(), ''); - } - } - } - // Get magic environments where we need to preset the FQDN - if ($key->startsWith('SERVICE_FQDN_')) { - // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 - if (substr_count(str($key)->value(), '_') === 3) { - $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); - $port = $key->afterLast('_')->value(); - } else { - $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); - $port = null; - } - if ($isApplication) { - $fqdn = $resource->fqdn; - if (blank($resource->fqdn)) { - $fqdn = generateFqdn($server, "$uuid"); - } - } elseif ($isService) { - if ($isOneClick) { - if (blank($savedService->fqdn)) { - if ($fqdnFor) { - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - } else { - $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); - } - } else { - $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); - } +// $allEnvironments = $allEnvironments->mapWithKeys(function ($item) { +// return [$item['key'] => $item['value']]; +// }); +// // filter and add magic environments +// foreach ($environment as $key => $value) { +// // Get all SERVICE_ variables from keys and values +// $key = str($key); +// $value = str($value); +// $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; +// preg_match_all($regex, $value, $valueMatches); +// if (count($valueMatches[1]) > 0) { +// foreach ($valueMatches[1] as $match) { +// $match = replaceVariables($match); +// if ($match->startsWith('SERVICE_')) { +// if ($magicEnvironments->has($match->value())) { +// continue; +// } +// $magicEnvironments->put($match->value(), ''); +// } +// } +// } +// // Get magic environments where we need to preset the FQDN +// if ($key->startsWith('SERVICE_FQDN_')) { +// // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 +// if (substr_count(str($key)->value(), '_') === 3) { +// $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); +// $port = $key->afterLast('_')->value(); +// } else { +// $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); +// $port = null; +// } +// if ($isApplication) { +// $fqdn = $resource->fqdn; +// if (blank($resource->fqdn)) { +// $fqdn = generateFqdn($server, "$uuid"); +// } +// } elseif ($isService) { +// if ($isOneClick) { +// if (blank($savedService->fqdn)) { +// if ($fqdnFor) { +// $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); +// } else { +// $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); +// } +// } else { +// $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); +// } - } else { - // For services which are not one-click, if no explicit FQDN is set, leave SERVICE_FQDN_ variables empty - if (blank($savedService->fqdn)) { - $fqdn = ''; - } else { - $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); - } - ray($fqdn); - } - } +// } else { +// // For services which are not one-click, if no explicit FQDN is set, leave SERVICE_FQDN_ variables empty +// if (blank($savedService->fqdn)) { +// $fqdn = ''; +// } else { +// $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); +// } +// ray($fqdn); +// } +// } - if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { - $path = $value->value(); - if ($path !== '/') { - $fqdn = "$fqdn$path"; - } - } - $fqdnWithPort = $fqdn; - if ($port) { - $fqdnWithPort = "$fqdn:$port"; - } - if ($isApplication && is_null($resource->fqdn)) { - data_forget($resource, 'environment_variables'); - data_forget($resource, 'environment_variables_preview'); - $resource->fqdn = $fqdnWithPort; - $resource->save(); - } elseif ($isService && is_null($savedService->fqdn)) { - $savedService->fqdn = $fqdnWithPort; - $savedService->save(); - } +// if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { +// $path = $value->value(); +// if ($path !== '/') { +// $fqdn = "$fqdn$path"; +// } +// } +// $fqdnWithPort = $fqdn; +// if ($port) { +// $fqdnWithPort = "$fqdn:$port"; +// } +// if ($isApplication && is_null($resource->fqdn)) { +// data_forget($resource, 'environment_variables'); +// data_forget($resource, 'environment_variables_preview'); +// $resource->fqdn = $fqdnWithPort; +// $resource->save(); +// } elseif ($isService && is_null($savedService->fqdn)) { +// $savedService->fqdn = $fqdnWithPort; +// $savedService->save(); +// } - if (substr_count(str($key)->value(), '_') === 2) { - $resource->environment_variables()->updateOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } - if (substr_count(str($key)->value(), '_') === 3) { - $newKey = str($key)->beforeLast('_'); - $resource->environment_variables()->updateOrCreate([ - 'key' => $newKey->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } - } - } +// if (substr_count(str($key)->value(), '_') === 2) { +// $resource->environment_variables()->updateOrCreate([ +// 'key' => $key->value(), +// 'resourceable_type' => get_class($resource), +// 'resourceable_id' => $resource->id, +// ], [ +// 'value' => $fqdn, +// 'is_build_time' => false, +// 'is_preview' => false, +// ]); +// } +// if (substr_count(str($key)->value(), '_') === 3) { +// $newKey = str($key)->beforeLast('_'); +// $resource->environment_variables()->updateOrCreate([ +// 'key' => $newKey->value(), +// 'resourceable_type' => get_class($resource), +// 'resourceable_id' => $resource->id, +// ], [ +// 'value' => $fqdn, +// 'is_build_time' => false, +// 'is_preview' => false, +// ]); +// } +// } +// } - $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); - if ($magicEnvironments->count() > 0) { - foreach ($magicEnvironments as $key => $value) { - $key = str($key); - $value = replaceVariables($value); - $command = parseCommandFromMagicEnvVariable($key); - if ($command->value() === 'FQDN') { - if ($isOneClick) { - $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } - } elseif ($command->value() === 'URL') { - if ($isOneClick) { - $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } - } else { - $value = generateEnvValue($command, $resource); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $value, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } - } - } - } +// $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); +// if ($magicEnvironments->count() > 0) { +// foreach ($magicEnvironments as $key => $value) { +// $key = str($key); +// $value = replaceVariables($value); +// $command = parseCommandFromMagicEnvVariable($key); +// if ($command->value() === 'FQDN') { +// if ($isOneClick) { +// $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); +// if (str($fqdnFor)->contains('_')) { +// $fqdnFor = str($fqdnFor)->before('_'); +// } +// $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); +// $resource->environment_variables()->firstOrCreate([ +// 'key' => $key->value(), +// 'resourceable_type' => get_class($resource), +// 'resourceable_id' => $resource->id, +// ], [ +// 'value' => $fqdn, +// 'is_build_time' => false, +// 'is_preview' => false, +// ]); +// } +// } elseif ($command->value() === 'URL') { +// if ($isOneClick) { +// $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); +// if (str($fqdnFor)->contains('_')) { +// $fqdnFor = str($fqdnFor)->before('_'); +// } +// $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); +// $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); +// $resource->environment_variables()->firstOrCreate([ +// 'key' => $key->value(), +// 'resourceable_type' => get_class($resource), +// 'resourceable_id' => $resource->id, +// ], [ +// 'value' => $fqdn, +// 'is_build_time' => false, +// 'is_preview' => false, +// ]); +// } +// } else { +// $value = generateEnvValue($command, $resource); +// $resource->environment_variables()->firstOrCreate([ +// 'key' => $key->value(), +// 'resourceable_type' => get_class($resource), +// 'resourceable_id' => $resource->id, +// ], [ +// 'value' => $value, +// 'is_build_time' => false, +// 'is_preview' => false, +// ]); +// } +// } +// } +// } - $serviceAppsLogDrainEnabledMap = collect([]); - if ($resource instanceof Service) { - $serviceAppsLogDrainEnabledMap = $resource->applications()->get()->keyBy('name')->map(function ($app) { - return $app->isLogDrainEnabled(); - }); - } +// $serviceAppsLogDrainEnabledMap = collect([]); +// if ($resource instanceof Service) { +// $serviceAppsLogDrainEnabledMap = $resource->applications()->get()->keyBy('name')->map(function ($app) { +// return $app->isLogDrainEnabled(); +// }); +// } - // Parse the rest of the services - foreach ($services as $serviceName => $service) { - $image = data_get_str($service, 'image'); - $restart = data_get_str($service, 'restart', RESTART_MODE); - $logging = data_get($service, 'logging'); +// // Parse the rest of the services +// foreach ($services as $serviceName => $service) { +// $image = data_get_str($service, 'image'); +// $restart = data_get_str($service, 'restart', RESTART_MODE); +// $logging = data_get($service, 'logging'); - if ($server->isLogDrainEnabled()) { - if ($resource instanceof Application && $resource->isLogDrainEnabled()) { - $logging = generate_fluentd_configuration(); - } - if ($resource instanceof Service && $serviceAppsLogDrainEnabledMap->get($serviceName)) { - $logging = generate_fluentd_configuration(); - } - } - $volumes = collect(data_get($service, 'volumes', [])); - $networks = collect(data_get($service, 'networks', [])); - $use_network_mode = data_get($service, 'network_mode') !== null; - $depends_on = collect(data_get($service, 'depends_on', [])); - $labels = collect(data_get($service, 'labels', [])); - if ($labels->count() > 0) { - if (isAssociativeArray($labels)) { - $newLabels = collect([]); - $labels->each(function ($value, $key) use ($newLabels) { - $newLabels->push("$key=$value"); - }); - $labels = $newLabels; - } - } - $environment = collect(data_get($service, 'environment', [])); - $ports = collect(data_get($service, 'ports', [])); - $buildArgs = collect(data_get($service, 'build.args', [])); - $environment = $environment->merge($buildArgs); +// if ($server->isLogDrainEnabled()) { +// if ($resource instanceof Application && $resource->isLogDrainEnabled()) { +// $logging = generate_fluentd_configuration(); +// } +// if ($resource instanceof Service && $serviceAppsLogDrainEnabledMap->get($serviceName)) { +// $logging = generate_fluentd_configuration(); +// } +// } +// $volumes = collect(data_get($service, 'volumes', [])); +// $networks = collect(data_get($service, 'networks', [])); +// $use_network_mode = data_get($service, 'network_mode') !== null; +// $depends_on = collect(data_get($service, 'depends_on', [])); +// $labels = collect(data_get($service, 'labels', [])); +// if ($labels->count() > 0) { +// if (isAssociativeArray($labels)) { +// $newLabels = collect([]); +// $labels->each(function ($value, $key) use ($newLabels) { +// $newLabels->push("$key=$value"); +// }); +// $labels = $newLabels; +// } +// } +// $environment = collect(data_get($service, 'environment', [])); +// $ports = collect(data_get($service, 'ports', [])); +// $buildArgs = collect(data_get($service, 'build.args', [])); +// $environment = $environment->merge($buildArgs); - $environment = convertToKeyValueCollection($environment); - $coolifyEnvironments = collect([]); +// $environment = convertToKeyValueCollection($environment); +// $coolifyEnvironments = collect([]); - $isDatabase = isDatabaseImage($image, $service); - $volumesParsed = collect([]); +// $isDatabase = isDatabaseImage($image, $service); +// $volumesParsed = collect([]); - if ($isApplication) { - $baseName = generateApplicationContainerName( - application: $resource, - pull_request_id: $pullRequestId - ); - $containerName = "$serviceName-$baseName"; - $predefinedPort = null; - } elseif ($isService) { - $containerName = "$serviceName-{$resource->uuid}"; +// if ($isApplication) { +// $baseName = generateApplicationContainerName( +// application: $resource, +// pull_request_id: $pullRequestId +// ); +// $containerName = "$serviceName-$baseName"; +// $predefinedPort = null; +// } elseif ($isService) { +// $containerName = "$serviceName-{$resource->uuid}"; - if ($serviceName === 'registry') { - $tempServiceName = 'docker-registry'; - } else { - $tempServiceName = $serviceName; - } - if (str(data_get($service, 'image'))->contains('glitchtip')) { - $tempServiceName = 'glitchtip'; - } - if ($serviceName === 'supabase-kong') { - $tempServiceName = 'supabase'; - } - $serviceDefinition = data_get($allServices, $tempServiceName); - $predefinedPort = data_get($serviceDefinition, 'port'); - if ($serviceName === 'plausible') { - $predefinedPort = '8000'; - } +// if ($serviceName === 'registry') { +// $tempServiceName = 'docker-registry'; +// } else { +// $tempServiceName = $serviceName; +// } +// if (str(data_get($service, 'image'))->contains('glitchtip')) { +// $tempServiceName = 'glitchtip'; +// } +// if ($serviceName === 'supabase-kong') { +// $tempServiceName = 'supabase'; +// } +// $serviceDefinition = data_get($allServices, $tempServiceName); +// $predefinedPort = data_get($serviceDefinition, 'port'); +// if ($serviceName === 'plausible') { +// $predefinedPort = '8000'; +// } - if ($isDatabase) { - $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); - if ($applicationFound) { - $savedService = $applicationFound; - } else { - $savedService = ServiceDatabase::firstOrCreate([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); - } - } else { - $savedService = ServiceApplication::firstOrCreate([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); - } - $fileStorages = $savedService->fileStorages(); - if ($savedService->image !== $image) { - $savedService->image = $image; - $savedService->save(); - } - } +// if ($isDatabase) { +// $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); +// if ($applicationFound) { +// $savedService = $applicationFound; +// } else { +// $savedService = ServiceDatabase::firstOrCreate([ +// 'name' => $serviceName, +// 'image' => $image, +// 'service_id' => $resource->id, +// ]); +// } +// } else { +// $savedService = ServiceApplication::firstOrCreate([ +// 'name' => $serviceName, +// 'image' => $image, +// 'service_id' => $resource->id, +// ]); +// } +// $fileStorages = $savedService->fileStorages(); +// if ($savedService->image !== $image) { +// $savedService->image = $image; +// $savedService->save(); +// } +// } - $originalResource = $isApplication ? $resource : $savedService; +// $originalResource = $isApplication ? $resource : $savedService; - if ($volumes->count() > 0) { - foreach ($volumes as $index => $volume) { - $type = null; - $source = null; - $target = null; - $content = null; - $isDirectory = false; - if (is_string($volume)) { - $source = str($volume)->before(':'); - $target = str($volume)->after(':')->beforeLast(':'); - $foundConfig = $fileStorages->whereMountPath($target)->first(); - if (sourceIsLocal($source)) { - $type = str('bind'); - if ($foundConfig) { - $contentNotNull_temp = data_get($foundConfig, 'content'); - if ($contentNotNull_temp) { - $content = $contentNotNull_temp; - } - $isDirectory = data_get($foundConfig, 'is_directory'); - } else { - // By default, we cannot determine if the bind is a directory or not, so we set it to directory - $isDirectory = true; - } - } else { - $type = str('volume'); - } - } elseif (is_array($volume)) { - $type = data_get_str($volume, 'type'); - $source = data_get_str($volume, 'source'); - $target = data_get_str($volume, 'target'); - $content = data_get($volume, 'content'); - $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); +// if ($volumes->count() > 0) { +// foreach ($volumes as $index => $volume) { +// $type = null; +// $source = null; +// $target = null; +// $content = null; +// $isDirectory = false; +// if (is_string($volume)) { +// $source = str($volume)->before(':'); +// $target = str($volume)->after(':')->beforeLast(':'); +// $foundConfig = $fileStorages->whereMountPath($target)->first(); +// if (sourceIsLocal($source)) { +// $type = str('bind'); +// if ($foundConfig) { +// $contentNotNull_temp = data_get($foundConfig, 'content'); +// if ($contentNotNull_temp) { +// $content = $contentNotNull_temp; +// } +// $isDirectory = data_get($foundConfig, 'is_directory'); +// } else { +// // By default, we cannot determine if the bind is a directory or not, so we set it to directory +// $isDirectory = true; +// } +// } else { +// $type = str('volume'); +// } +// } elseif (is_array($volume)) { +// $type = data_get_str($volume, 'type'); +// $source = data_get_str($volume, 'source'); +// $target = data_get_str($volume, 'target'); +// $content = data_get($volume, 'content'); +// $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); - $foundConfig = $fileStorages->whereMountPath($target)->first(); - if ($foundConfig) { - $contentNotNull_temp = data_get($foundConfig, 'content'); - if ($contentNotNull_temp) { - $content = $contentNotNull_temp; - } - $isDirectory = data_get($foundConfig, 'is_directory'); - } else { - // if isDirectory is not set (or false) & content is also not set, we assume it is a directory - if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { - $isDirectory = true; - } - } - } - if ($type->value() === 'bind') { - if ($source->value() === '/var/run/docker.sock') { - $volume = $source->value().':'.$target->value(); - } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { - $volume = $source->value().':'.$target->value(); - } else { - if ((int) $resource->compose_parsing_version >= 4) { - if ($isApplication) { - $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); - } elseif ($isService) { - $mainDirectory = str(base_configuration_dir().'/services/'.$uuid); - } - } else { - $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); - } - $source = replaceLocalSource($source, $mainDirectory); - if ($isApplication && $isPullRequest) { - $source = $source."-pr-$pullRequestId"; - } - LocalFileVolume::updateOrCreate( - [ - 'mount_path' => $target, - 'resource_id' => $originalResource->id, - 'resource_type' => get_class($originalResource), - ], - [ - 'fs_path' => $source, - 'mount_path' => $target, - 'content' => $content, - 'is_directory' => $isDirectory, - 'resource_id' => $originalResource->id, - 'resource_type' => get_class($originalResource), - ] - ); - if (isDev()) { - if ((int) $resource->compose_parsing_version >= 4) { - if ($isApplication) { - $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); - } elseif ($isService) { - $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid); - } - } else { - $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); - } - } - $volume = "$source:$target"; - } - } elseif ($type->value() === 'volume') { - if ($topLevel->get('volumes')->has($source->value())) { - $temp = $topLevel->get('volumes')->get($source->value()); - if (data_get($temp, 'driver_opts.type') === 'cifs') { - continue; - } - if (data_get($temp, 'driver_opts.type') === 'nfs') { - continue; - } - } - $slugWithoutUuid = Str::slug($source, '-'); - $name = "{$uuid}_{$slugWithoutUuid}"; +// $foundConfig = $fileStorages->whereMountPath($target)->first(); +// if ($foundConfig) { +// $contentNotNull_temp = data_get($foundConfig, 'content'); +// if ($contentNotNull_temp) { +// $content = $contentNotNull_temp; +// } +// $isDirectory = data_get($foundConfig, 'is_directory'); +// } else { +// // if isDirectory is not set (or false) & content is also not set, we assume it is a directory +// if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { +// $isDirectory = true; +// } +// } +// } +// if ($type->value() === 'bind') { +// if ($source->value() === '/var/run/docker.sock') { +// $volume = $source->value().':'.$target->value(); +// } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { +// $volume = $source->value().':'.$target->value(); +// } else { +// if ((int) $resource->compose_parsing_version >= 4) { +// if ($isApplication) { +// $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); +// } elseif ($isService) { +// $mainDirectory = str(base_configuration_dir().'/services/'.$uuid); +// } +// } else { +// $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); +// } +// $source = replaceLocalSource($source, $mainDirectory); +// if ($isApplication && $isPullRequest) { +// $source = $source."-pr-$pullRequestId"; +// } +// LocalFileVolume::updateOrCreate( +// [ +// 'mount_path' => $target, +// 'resource_id' => $originalResource->id, +// 'resource_type' => get_class($originalResource), +// ], +// [ +// 'fs_path' => $source, +// 'mount_path' => $target, +// 'content' => $content, +// 'is_directory' => $isDirectory, +// 'resource_id' => $originalResource->id, +// 'resource_type' => get_class($originalResource), +// ] +// ); +// if (isDev()) { +// if ((int) $resource->compose_parsing_version >= 4) { +// if ($isApplication) { +// $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); +// } elseif ($isService) { +// $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid); +// } +// } else { +// $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); +// } +// } +// $volume = "$source:$target"; +// } +// } elseif ($type->value() === 'volume') { +// if ($topLevel->get('volumes')->has($source->value())) { +// $temp = $topLevel->get('volumes')->get($source->value()); +// if (data_get($temp, 'driver_opts.type') === 'cifs') { +// continue; +// } +// if (data_get($temp, 'driver_opts.type') === 'nfs') { +// continue; +// } +// } +// $slugWithoutUuid = Str::slug($source, '-'); +// $name = "{$uuid}_{$slugWithoutUuid}"; - if ($isApplication && $isPullRequest) { - $name = "{$name}-pr-$pullRequestId"; - } - if (is_string($volume)) { - $source = str($volume)->before(':'); - $target = str($volume)->after(':')->beforeLast(':'); - $source = $name; - $volume = "$source:$target"; - } elseif (is_array($volume)) { - data_set($volume, 'source', $name); - } - $topLevel->get('volumes')->put($name, [ - 'name' => $name, - ]); - LocalPersistentVolume::updateOrCreate( - [ - 'name' => $name, - 'resource_id' => $originalResource->id, - 'resource_type' => get_class($originalResource), - ], - [ - 'name' => $name, - 'mount_path' => $target, - 'resource_id' => $originalResource->id, - 'resource_type' => get_class($originalResource), - ] - ); - } - dispatch(new ServerFilesFromServerJob($originalResource)); - $volumesParsed->put($index, $volume); - } - } +// if ($isApplication && $isPullRequest) { +// $name = "{$name}-pr-$pullRequestId"; +// } +// if (is_string($volume)) { +// $source = str($volume)->before(':'); +// $target = str($volume)->after(':')->beforeLast(':'); +// $source = $name; +// $volume = "$source:$target"; +// } elseif (is_array($volume)) { +// data_set($volume, 'source', $name); +// } +// $topLevel->get('volumes')->put($name, [ +// 'name' => $name, +// ]); +// LocalPersistentVolume::updateOrCreate( +// [ +// 'name' => $name, +// 'resource_id' => $originalResource->id, +// 'resource_type' => get_class($originalResource), +// ], +// [ +// 'name' => $name, +// 'mount_path' => $target, +// 'resource_id' => $originalResource->id, +// 'resource_type' => get_class($originalResource), +// ] +// ); +// } +// dispatch(new ServerFilesFromServerJob($originalResource)); +// $volumesParsed->put($index, $volume); +// } +// } - if ($depends_on?->count() > 0) { - if ($isApplication && $isPullRequest) { - $newDependsOn = collect([]); - $depends_on->each(function ($dependency, $condition) use ($pullRequestId, $newDependsOn) { - if (is_numeric($condition)) { - $dependency = "$dependency-pr-$pullRequestId"; +// if ($depends_on?->count() > 0) { +// if ($isApplication && $isPullRequest) { +// $newDependsOn = collect([]); +// $depends_on->each(function ($dependency, $condition) use ($pullRequestId, $newDependsOn) { +// if (is_numeric($condition)) { +// $dependency = "$dependency-pr-$pullRequestId"; - $newDependsOn->put($condition, $dependency); - } else { - $condition = "$condition-pr-$pullRequestId"; - $newDependsOn->put($condition, $dependency); - } - }); - $depends_on = $newDependsOn; - } - } - if (! $use_network_mode) { - if ($topLevel->get('networks')?->count() > 0) { - foreach ($topLevel->get('networks') as $networkName => $network) { - if ($networkName === 'default') { - continue; - } - // ignore aliases - if ($network['aliases'] ?? false) { - continue; - } - $networkExists = $networks->contains(function ($value, $key) use ($networkName) { - return $value == $networkName || $key == $networkName; - }); - if (! $networkExists) { - $networks->put($networkName, null); - } - } - } - $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { - return $value == $baseNetwork; - }); - if (! $baseNetworkExists) { - foreach ($baseNetwork as $network) { - $topLevel->get('networks')->put($network, [ - 'name' => $network, - 'external' => true, - ]); - } - } - } +// $newDependsOn->put($condition, $dependency); +// } else { +// $condition = "$condition-pr-$pullRequestId"; +// $newDependsOn->put($condition, $dependency); +// } +// }); +// $depends_on = $newDependsOn; +// } +// } +// if (! $use_network_mode) { +// if ($topLevel->get('networks')?->count() > 0) { +// foreach ($topLevel->get('networks') as $networkName => $network) { +// if ($networkName === 'default') { +// continue; +// } +// // ignore aliases +// if ($network['aliases'] ?? false) { +// continue; +// } +// $networkExists = $networks->contains(function ($value, $key) use ($networkName) { +// return $value == $networkName || $key == $networkName; +// }); +// if (! $networkExists) { +// $networks->put($networkName, null); +// } +// } +// } +// $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { +// return $value == $baseNetwork; +// }); +// if (! $baseNetworkExists) { +// foreach ($baseNetwork as $network) { +// $topLevel->get('networks')->put($network, [ +// 'name' => $network, +// 'external' => true, +// ]); +// } +// } +// } - // Collect/create/update ports - $collectedPorts = collect([]); - if ($ports->count() > 0) { - foreach ($ports as $sport) { - if (is_string($sport) || is_numeric($sport)) { - $collectedPorts->push($sport); - } - if (is_array($sport)) { - $target = data_get($sport, 'target'); - $published = data_get($sport, 'published'); - $protocol = data_get($sport, 'protocol'); - $collectedPorts->push("$target:$published/$protocol"); - } - } - } - if ($isService) { - $originalResource->ports = $collectedPorts->implode(','); - $originalResource->save(); - } +// // Collect/create/update ports +// $collectedPorts = collect([]); +// if ($ports->count() > 0) { +// foreach ($ports as $sport) { +// if (is_string($sport) || is_numeric($sport)) { +// $collectedPorts->push($sport); +// } +// if (is_array($sport)) { +// $target = data_get($sport, 'target'); +// $published = data_get($sport, 'published'); +// $protocol = data_get($sport, 'protocol'); +// $collectedPorts->push("$target:$published/$protocol"); +// } +// } +// } +// if ($isService) { +// $originalResource->ports = $collectedPorts->implode(','); +// $originalResource->save(); +// } - $networks_temp = collect(); +// $networks_temp = collect(); - if (! $use_network_mode) { - foreach ($networks as $key => $network) { - if (gettype($network) === 'string') { - // networks: - // - appwrite - $networks_temp->put($network, null); - } elseif (gettype($network) === 'array') { - // networks: - // default: - // ipv4_address: 192.168.203.254 - $networks_temp->put($key, $network); - } - } - foreach ($baseNetwork as $key => $network) { - $networks_temp->put($network, null); - } +// if (! $use_network_mode) { +// foreach ($networks as $key => $network) { +// if (gettype($network) === 'string') { +// // networks: +// // - appwrite +// $networks_temp->put($network, null); +// } elseif (gettype($network) === 'array') { +// // networks: +// // default: +// // ipv4_address: 192.168.203.254 +// $networks_temp->put($key, $network); +// } +// } +// foreach ($baseNetwork as $key => $network) { +// $networks_temp->put($network, null); +// } - if ($isApplication) { - if (data_get($resource, 'settings.connect_to_docker_network')) { - $network = $resource->destination->network; - $networks_temp->put($network, null); - $topLevel->get('networks')->put($network, [ - 'name' => $network, - 'external' => true, - ]); - } - } - } +// if ($isApplication) { +// if (data_get($resource, 'settings.connect_to_docker_network')) { +// $network = $resource->destination->network; +// $networks_temp->put($network, null); +// $topLevel->get('networks')->put($network, [ +// 'name' => $network, +// 'external' => true, +// ]); +// } +// } +// } - $normalEnvironments = $environment->diffKeys($allMagicEnvironments); - $normalEnvironments = $normalEnvironments->filter(function ($value, $key) { - return ! str($value)->startsWith('SERVICE_'); - }); - foreach ($normalEnvironments as $key => $value) { - $key = str($key); - $value = str($value); - $originalValue = $value; - $parsedValue = replaceVariables($value); - if ($value->startsWith('$SERVICE_')) { - $resource->environment_variables()->firstOrCreate([ - 'key' => $key, - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $value, - 'is_build_time' => false, - 'is_preview' => false, - ]); +// $normalEnvironments = $environment->diffKeys($allMagicEnvironments); +// $normalEnvironments = $normalEnvironments->filter(function ($value, $key) { +// return ! str($value)->startsWith('SERVICE_'); +// }); +// foreach ($normalEnvironments as $key => $value) { +// $key = str($key); +// $value = str($value); +// $originalValue = $value; +// $parsedValue = replaceVariables($value); +// if ($value->startsWith('$SERVICE_')) { +// $resource->environment_variables()->firstOrCreate([ +// 'key' => $key, +// 'resourceable_type' => get_class($resource), +// 'resourceable_id' => $resource->id, +// ], [ +// 'value' => $value, +// 'is_build_time' => false, +// 'is_preview' => false, +// ]); - continue; - } - if (! $value->startsWith('$')) { - continue; - } - if ($key->value() === $parsedValue->value()) { - $value = null; - $resource->environment_variables()->firstOrCreate([ - 'key' => $key, - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $value, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } else { - if ($value->startsWith('$')) { - $isRequired = false; - if ($value->contains(':-')) { - $value = replaceVariables($value); - $key = $value->before(':'); - $value = $value->after(':-'); - } elseif ($value->contains('-')) { - $value = replaceVariables($value); +// continue; +// } +// if (! $value->startsWith('$')) { +// continue; +// } +// if ($key->value() === $parsedValue->value()) { +// $value = null; +// $resource->environment_variables()->firstOrCreate([ +// 'key' => $key, +// 'resourceable_type' => get_class($resource), +// 'resourceable_id' => $resource->id, +// ], [ +// 'value' => $value, +// 'is_build_time' => false, +// 'is_preview' => false, +// ]); +// } else { +// if ($value->startsWith('$')) { +// $isRequired = false; +// if ($value->contains(':-')) { +// $value = replaceVariables($value); +// $key = $value->before(':'); +// $value = $value->after(':-'); +// } elseif ($value->contains('-')) { +// $value = replaceVariables($value); - $key = $value->before('-'); - $value = $value->after('-'); - } elseif ($value->contains(':?')) { - $value = replaceVariables($value); +// $key = $value->before('-'); +// $value = $value->after('-'); +// } elseif ($value->contains(':?')) { +// $value = replaceVariables($value); - $key = $value->before(':'); - $value = $value->after(':?'); - $isRequired = true; - } elseif ($value->contains('?')) { - $value = replaceVariables($value); +// $key = $value->before(':'); +// $value = $value->after(':?'); +// $isRequired = true; +// } elseif ($value->contains('?')) { +// $value = replaceVariables($value); - $key = $value->before('?'); - $value = $value->after('?'); - $isRequired = true; - } - if ($originalValue->value() === $value->value()) { - // This means the variable does not have a default value, so it needs to be created in Coolify - $parsedKeyValue = replaceVariables($value); - $resource->environment_variables()->firstOrCreate([ - 'key' => $parsedKeyValue, - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'is_build_time' => false, - 'is_preview' => false, - 'is_required' => $isRequired, - ]); - // Add the variable to the environment so it will be shown in the deployable compose file - // $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first()->real_value; - $environment[$parsedKeyValue->value()] = $value; +// $key = $value->before('?'); +// $value = $value->after('?'); +// $isRequired = true; +// } +// if ($originalValue->value() === $value->value()) { +// // This means the variable does not have a default value, so it needs to be created in Coolify +// $parsedKeyValue = replaceVariables($value); +// $resource->environment_variables()->firstOrCreate([ +// 'key' => $parsedKeyValue, +// 'resourceable_type' => get_class($resource), +// 'resourceable_id' => $resource->id, +// ], [ +// 'is_build_time' => false, +// 'is_preview' => false, +// 'is_required' => $isRequired, +// ]); +// // Add the variable to the environment so it will be shown in the deployable compose file +// // $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first()->real_value; +// $environment[$parsedKeyValue->value()] = $value; - continue; - } - $resource->environment_variables()->firstOrCreate([ - 'key' => $key, - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $value, - 'is_build_time' => false, - 'is_preview' => false, - 'is_required' => $isRequired, - ]); - } - } - } - if ($isApplication) { - $branch = $originalResource->git_branch; - if ($pullRequestId !== 0) { - $branch = "pull/{$pullRequestId}/head"; - } - if ($originalResource->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $coolifyEnvironments->put('COOLIFY_BRANCH', "\"{$branch}\""); - } - } +// continue; +// } +// $resource->environment_variables()->firstOrCreate([ +// 'key' => $key, +// 'resourceable_type' => get_class($resource), +// 'resourceable_id' => $resource->id, +// ], [ +// 'value' => $value, +// 'is_build_time' => false, +// 'is_preview' => false, +// 'is_required' => $isRequired, +// ]); +// } +// } +// } +// if ($isApplication) { +// $branch = $originalResource->git_branch; +// if ($pullRequestId !== 0) { +// $branch = "pull/{$pullRequestId}/head"; +// } +// if ($originalResource->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { +// $coolifyEnvironments->put('COOLIFY_BRANCH', "\"{$branch}\""); +// } +// } - // Add COOLIFY_RESOURCE_UUID to environment - if ($resource->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { - $coolifyEnvironments->put('COOLIFY_RESOURCE_UUID', "{$resource->uuid}"); - } +// // Add COOLIFY_RESOURCE_UUID to environment +// if ($resource->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { +// $coolifyEnvironments->put('COOLIFY_RESOURCE_UUID', "{$resource->uuid}"); +// } - // Add COOLIFY_CONTAINER_NAME to environment - if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', "{$containerName}"); - } +// // Add COOLIFY_CONTAINER_NAME to environment +// if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { +// $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', "{$containerName}"); +// } - if ($isApplication) { - if ($isPullRequest) { - $preview = $resource->previews()->find($preview_id); - $domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))) ?? collect([]); - } else { - $domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]); - } - $fqdns = data_get($domains, "$serviceName.domain"); - // Generate SERVICE_FQDN & SERVICE_URL for dockercompose - if ($resource->build_pack === 'dockercompose') { +// if ($isApplication) { +// if ($isPullRequest) { +// $preview = $resource->previews()->find($preview_id); +// $domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))) ?? collect([]); +// } else { +// $domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]); +// } +// $fqdns = data_get($domains, "$serviceName.domain"); +// // Generate SERVICE_FQDN & SERVICE_URL for dockercompose +// if ($resource->build_pack === 'dockercompose') { - foreach ($domains as $forServiceName => $domain) { - $parsedDomain = data_get($domain, 'domain'); - if (filled($parsedDomain)) { - $parsedDomain = str($parsedDomain)->explode(',')->first(); - $coolifyUrl = Url::fromString($parsedDomain); - $coolifyScheme = $coolifyUrl->getScheme(); - $coolifyFqdn = $coolifyUrl->getHost(); - $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); - $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyUrl->__toString()); - $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyFqdn); - } - } - } - // If the domain is set, we need to generate the FQDNs for the preview - if (filled($fqdns)) { - $fqdns = str($fqdns)->explode(','); - if ($isPullRequest) { - $preview = $resource->previews()->find($preview_id); - $docker_compose_domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))); - if ($docker_compose_domains->count() > 0) { - $found_fqdn = data_get($docker_compose_domains, "$serviceName.domain"); - if ($found_fqdn) { - $fqdns = collect($found_fqdn); - } else { - $fqdns = collect([]); - } - } else { - $fqdns = $fqdns->map(function ($fqdn) use ($pullRequestId, $resource) { - $preview = ApplicationPreview::findPreviewByApplicationAndPullId($resource->id, $pullRequestId); - $url = Url::fromString($fqdn); - $template = $resource->preview_url_template; - $host = $url->getHost(); - $schema = $url->getScheme(); - $random = new Cuid2; - $preview_fqdn = str_replace('{{random}}', $random, $template); - $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); - $preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn); - $preview_fqdn = "$schema://$preview_fqdn"; - $preview->fqdn = $preview_fqdn; - $preview->save(); +// foreach ($domains as $forServiceName => $domain) { +// $parsedDomain = data_get($domain, 'domain'); +// if (filled($parsedDomain)) { +// $parsedDomain = str($parsedDomain)->explode(',')->first(); +// $coolifyUrl = Url::fromString($parsedDomain); +// $coolifyScheme = $coolifyUrl->getScheme(); +// $coolifyFqdn = $coolifyUrl->getHost(); +// $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); +// $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyUrl->__toString()); +// $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyFqdn); +// } +// } +// } +// // If the domain is set, we need to generate the FQDNs for the preview +// if (filled($fqdns)) { +// $fqdns = str($fqdns)->explode(','); +// if ($isPullRequest) { +// $preview = $resource->previews()->find($preview_id); +// $docker_compose_domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))); +// if ($docker_compose_domains->count() > 0) { +// $found_fqdn = data_get($docker_compose_domains, "$serviceName.domain"); +// if ($found_fqdn) { +// $fqdns = collect($found_fqdn); +// } else { +// $fqdns = collect([]); +// } +// } else { +// $fqdns = $fqdns->map(function ($fqdn) use ($pullRequestId, $resource) { +// $preview = ApplicationPreview::findPreviewByApplicationAndPullId($resource->id, $pullRequestId); +// $url = Url::fromString($fqdn); +// $template = $resource->preview_url_template; +// $host = $url->getHost(); +// $schema = $url->getScheme(); +// $random = new Cuid2; +// $preview_fqdn = str_replace('{{random}}', $random, $template); +// $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); +// $preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn); +// $preview_fqdn = "$schema://$preview_fqdn"; +// $preview->fqdn = $preview_fqdn; +// $preview->save(); - return $preview_fqdn; - }); - } - } - } - $defaultLabels = defaultLabels( - id: $resource->id, - name: $containerName, - projectName: $resource->project()->name, - resourceName: $resource->name, - pull_request_id: $pullRequestId, - type: 'application', - environment: $resource->environment->name, - ); +// return $preview_fqdn; +// }); +// } +// } +// } +// $defaultLabels = defaultLabels( +// id: $resource->id, +// name: $containerName, +// projectName: $resource->project()->name, +// resourceName: $resource->name, +// pull_request_id: $pullRequestId, +// type: 'application', +// environment: $resource->environment->name, +// ); - } elseif ($isService) { - if ($savedService->serviceType()) { - $fqdns = generateServiceSpecificFqdns($savedService); - } else { - $fqdns = collect(data_get($savedService, 'fqdns'))->filter(); - } - $defaultLabels = defaultLabels( - id: $resource->id, - name: $containerName, - projectName: $resource->project()->name, - resourceName: $resource->name, - type: 'service', - subType: $isDatabase ? 'database' : 'application', - subId: $savedService->id, - subName: $savedService->human_name ?? $savedService->name, - environment: $resource->environment->name, - ); - } - // Add COOLIFY_FQDN & COOLIFY_URL to environment - if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { - $fqdnsWithoutPort = $fqdns->map(function ($fqdn) { - return str($fqdn)->after('://')->before(':')->prepend(str($fqdn)->before('://')->append('://')); - }); - $coolifyEnvironments->put('COOLIFY_URL', $fqdnsWithoutPort->implode(',')); +// } elseif ($isService) { +// if ($savedService->serviceType()) { +// $fqdns = generateServiceSpecificFqdns($savedService); +// } else { +// $fqdns = collect(data_get($savedService, 'fqdns'))->filter(); +// } +// $defaultLabels = defaultLabels( +// id: $resource->id, +// name: $containerName, +// projectName: $resource->project()->name, +// resourceName: $resource->name, +// type: 'service', +// subType: $isDatabase ? 'database' : 'application', +// subId: $savedService->id, +// subName: $savedService->human_name ?? $savedService->name, +// environment: $resource->environment->name, +// ); +// } +// // Add COOLIFY_FQDN & COOLIFY_URL to environment +// if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { +// $fqdnsWithoutPort = $fqdns->map(function ($fqdn) { +// return str($fqdn)->after('://')->before(':')->prepend(str($fqdn)->before('://')->append('://')); +// }); +// $coolifyEnvironments->put('COOLIFY_URL', $fqdnsWithoutPort->implode(',')); - $urls = $fqdns->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->before(':'); - }); - $coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(',')); - } - add_coolify_default_environment_variables($resource, $coolifyEnvironments, $resource->environment_variables); - if ($environment->count() > 0) { - $environment = $environment->filter(function ($value, $key) { - return ! str($key)->startsWith('SERVICE_FQDN_'); - })->map(function ($value, $key) use ($resource) { - // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used - if (str($value)->isEmpty()) { - if ($resource->environment_variables()->where('key', $key)->exists()) { - $value = $resource->environment_variables()->where('key', $key)->first()->value; - } else { - $value = null; - } - } +// $urls = $fqdns->map(function ($fqdn) { +// return str($fqdn)->replace('http://', '')->replace('https://', '')->before(':'); +// }); +// $coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(',')); +// } +// add_coolify_default_environment_variables($resource, $coolifyEnvironments, $resource->environment_variables); +// if ($environment->count() > 0) { +// $environment = $environment->filter(function ($value, $key) { +// return ! str($key)->startsWith('SERVICE_FQDN_'); +// })->map(function ($value, $key) use ($resource) { +// // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used +// if (str($value)->isEmpty()) { +// if ($resource->environment_variables()->where('key', $key)->exists()) { +// $value = $resource->environment_variables()->where('key', $key)->first()->value; +// } else { +// $value = null; +// } +// } - return $value; - }); - } - $serviceLabels = $labels->merge($defaultLabels); - if ($serviceLabels->count() > 0) { - if ($isApplication) { - $isContainerLabelEscapeEnabled = data_get($resource, 'settings.is_container_label_escape_enabled'); - } else { - $isContainerLabelEscapeEnabled = data_get($resource, 'is_container_label_escape_enabled'); - } - if ($isContainerLabelEscapeEnabled) { - $serviceLabels = $serviceLabels->map(function ($value, $key) { - return escapeDollarSign($value); - }); - } - } - if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { - if ($isApplication) { - $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; - $uuid = $resource->uuid; - $network = data_get($resource, 'destination.network'); - if ($isPullRequest) { - $uuid = "{$resource->uuid}-{$pullRequestId}"; - } - if ($isPullRequest) { - $network = "{$resource->destination->network}-{$pullRequestId}"; - } - } else { - $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; - $uuid = $resource->uuid; - $network = data_get($resource, 'destination.network'); - } - if ($shouldGenerateLabelsExactly) { - switch ($server->proxyType()) { - case ProxyTypes::TRAEFIK->value: - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( - uuid: $uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $originalResource->isGzipEnabled(), - is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), - service_name: $serviceName, - image: $image - )); - break; - case ProxyTypes::CADDY->value: - $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( - network: $network, - uuid: $uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $originalResource->isGzipEnabled(), - is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), - service_name: $serviceName, - image: $image, - predefinedPort: $predefinedPort - )); - break; - } - } else { - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( - uuid: $uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $originalResource->isGzipEnabled(), - is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), - service_name: $serviceName, - image: $image - )); - $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( - network: $network, - uuid: $uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $originalResource->isGzipEnabled(), - is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), - service_name: $serviceName, - image: $image, - predefinedPort: $predefinedPort - )); - } - } - if ($isService) { - if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) { - $savedService->update(['exclude_from_status' => true]); - } - } - data_forget($service, 'volumes.*.content'); - data_forget($service, 'volumes.*.isDirectory'); - data_forget($service, 'volumes.*.is_directory'); - data_forget($service, 'exclude_from_hc'); +// return $value; +// }); +// } +// $serviceLabels = $labels->merge($defaultLabels); +// if ($serviceLabels->count() > 0) { +// if ($isApplication) { +// $isContainerLabelEscapeEnabled = data_get($resource, 'settings.is_container_label_escape_enabled'); +// } else { +// $isContainerLabelEscapeEnabled = data_get($resource, 'is_container_label_escape_enabled'); +// } +// if ($isContainerLabelEscapeEnabled) { +// $serviceLabels = $serviceLabels->map(function ($value, $key) { +// return escapeDollarSign($value); +// }); +// } +// } +// if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { +// if ($isApplication) { +// $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; +// $uuid = $resource->uuid; +// $network = data_get($resource, 'destination.network'); +// if ($isPullRequest) { +// $uuid = "{$resource->uuid}-{$pullRequestId}"; +// } +// if ($isPullRequest) { +// $network = "{$resource->destination->network}-{$pullRequestId}"; +// } +// } else { +// $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; +// $uuid = $resource->uuid; +// $network = data_get($resource, 'destination.network'); +// } +// if ($shouldGenerateLabelsExactly) { +// switch ($server->proxyType()) { +// case ProxyTypes::TRAEFIK->value: +// $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( +// uuid: $uuid, +// domains: $fqdns, +// is_force_https_enabled: true, +// serviceLabels: $serviceLabels, +// is_gzip_enabled: $originalResource->isGzipEnabled(), +// is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), +// service_name: $serviceName, +// image: $image +// )); +// break; +// case ProxyTypes::CADDY->value: +// $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( +// network: $network, +// uuid: $uuid, +// domains: $fqdns, +// is_force_https_enabled: true, +// serviceLabels: $serviceLabels, +// is_gzip_enabled: $originalResource->isGzipEnabled(), +// is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), +// service_name: $serviceName, +// image: $image, +// predefinedPort: $predefinedPort +// )); +// break; +// } +// } else { +// $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( +// uuid: $uuid, +// domains: $fqdns, +// is_force_https_enabled: true, +// serviceLabels: $serviceLabels, +// is_gzip_enabled: $originalResource->isGzipEnabled(), +// is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), +// service_name: $serviceName, +// image: $image +// )); +// $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( +// network: $network, +// uuid: $uuid, +// domains: $fqdns, +// is_force_https_enabled: true, +// serviceLabels: $serviceLabels, +// is_gzip_enabled: $originalResource->isGzipEnabled(), +// is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), +// service_name: $serviceName, +// image: $image, +// predefinedPort: $predefinedPort +// )); +// } +// } +// if ($isService) { +// if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) { +// $savedService->update(['exclude_from_status' => true]); +// } +// } +// data_forget($service, 'volumes.*.content'); +// data_forget($service, 'volumes.*.isDirectory'); +// data_forget($service, 'volumes.*.is_directory'); +// data_forget($service, 'exclude_from_hc'); - $volumesParsed = $volumesParsed->map(function ($volume) { - data_forget($volume, 'content'); - data_forget($volume, 'is_directory'); - data_forget($volume, 'isDirectory'); +// $volumesParsed = $volumesParsed->map(function ($volume) { +// data_forget($volume, 'content'); +// data_forget($volume, 'is_directory'); +// data_forget($volume, 'isDirectory'); - return $volume; - }); +// return $volume; +// }); - $payload = collect($service)->merge([ - 'container_name' => $containerName, - 'restart' => $restart->value(), - 'labels' => $serviceLabels, - ]); - if (! $use_network_mode) { - $payload['networks'] = $networks_temp; - } - if ($ports->count() > 0) { - $payload['ports'] = $ports; - } - if ($volumesParsed->count() > 0) { - $payload['volumes'] = $volumesParsed; - } - if ($environment->count() > 0 || $coolifyEnvironments->count() > 0) { - $payload['environment'] = $environment->merge($coolifyEnvironments); - } - if ($logging) { - $payload['logging'] = $logging; - } - if ($depends_on->count() > 0) { - $payload['depends_on'] = $depends_on; - } - if ($isApplication && $isPullRequest) { - $serviceName = "{$serviceName}-pr-{$pullRequestId}"; - } +// $payload = collect($service)->merge([ +// 'container_name' => $containerName, +// 'restart' => $restart->value(), +// 'labels' => $serviceLabels, +// ]); +// if (! $use_network_mode) { +// $payload['networks'] = $networks_temp; +// } +// if ($ports->count() > 0) { +// $payload['ports'] = $ports; +// } +// if ($volumesParsed->count() > 0) { +// $payload['volumes'] = $volumesParsed; +// } +// if ($environment->count() > 0 || $coolifyEnvironments->count() > 0) { +// $payload['environment'] = $environment->merge($coolifyEnvironments); +// } +// if ($logging) { +// $payload['logging'] = $logging; +// } +// if ($depends_on->count() > 0) { +// $payload['depends_on'] = $depends_on; +// } +// if ($isApplication && $isPullRequest) { +// $serviceName = "{$serviceName}-pr-{$pullRequestId}"; +// } - $parsedServices->put($serviceName, $payload); - } - $topLevel->put('services', $parsedServices); +// $parsedServices->put($serviceName, $payload); +// } +// $topLevel->put('services', $parsedServices); - $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets']; +// $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets']; - $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) { - return array_search($key, $customOrder); - }); +// $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) { +// return array_search($key, $customOrder); +// }); - $resource->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2); - data_forget($resource, 'environment_variables'); - data_forget($resource, 'environment_variables_preview'); - $resource->save(); +// $resource->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2); +// data_forget($resource, 'environment_variables'); +// data_forget($resource, 'environment_variables_preview'); +// $resource->save(); - return $topLevel; -} +// return $topLevel; +// } function generate_fluentd_configuration(): array { From 0e7cc988a6d7b091be0c903b700c245b120cce43 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 7 Aug 2025 21:57:00 +0200 Subject: [PATCH 0147/1717] feat(user): add changelog read tracking and unread count method --- app/Console/Commands/InitChangelog.php | 98 ++++++ app/Livewire/SettingsDropdown.php | 52 ++++ app/Models/User.php | 10 + app/Models/UserChangelogRead.php | 48 +++ app/Services/ChangelogService.php | 294 ++++++++++++++++++ config/services.php | 2 +- ...2403_create_user_changelog_reads_table.php | 34 ++ docker/production/Dockerfile | 1 + resources/views/components/navbar.blade.php | 187 +++++------ .../livewire/settings-dropdown.blade.php | 271 ++++++++++++++++ 10 files changed, 887 insertions(+), 110 deletions(-) create mode 100644 app/Console/Commands/InitChangelog.php create mode 100644 app/Livewire/SettingsDropdown.php create mode 100644 app/Models/UserChangelogRead.php create mode 100644 app/Services/ChangelogService.php create mode 100644 database/migrations/2025_08_07_142403_create_user_changelog_reads_table.php create mode 100644 resources/views/livewire/settings-dropdown.blade.php diff --git a/app/Console/Commands/InitChangelog.php b/app/Console/Commands/InitChangelog.php new file mode 100644 index 000000000..f9eb12f04 --- /dev/null +++ b/app/Console/Commands/InitChangelog.php @@ -0,0 +1,98 @@ +argument('month') ?: Carbon::now()->format('Y-m'); + + // Validate month format + if (! preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) { + $this->error('Invalid month format. Use YYYY-MM format with valid months 01-12 (e.g., 2025-08)'); + + return self::FAILURE; + } + + $changelogsDir = base_path('changelogs'); + $filePath = $changelogsDir."/{$month}.json"; + + // Create changelogs directory if it doesn't exist + if (! is_dir($changelogsDir)) { + mkdir($changelogsDir, 0755, true); + $this->info("Created changelogs directory: {$changelogsDir}"); + } + + // Check if file already exists + if (file_exists($filePath)) { + if (! $this->confirm("File {$month}.json already exists. Overwrite?")) { + $this->info('Operation cancelled'); + + return self::SUCCESS; + } + } + + // Parse the month for example data + $carbonMonth = Carbon::createFromFormat('Y-m', $month); + $monthName = $carbonMonth->format('F Y'); + $sampleDate = $carbonMonth->addDays(14)->toISOString(); // Mid-month + + // Get version from config + $version = 'v'.config('constants.coolify.version'); + + // Create example changelog structure + $exampleData = [ + 'entries' => [ + [ + 'version' => $version, + 'title' => 'Example Feature Release', + 'content' => "This is an example changelog entry for {$monthName}. Replace this with your actual release notes. Include details about new features, improvements, bug fixes, and any breaking changes.", + 'published_at' => $sampleDate, + ], + ], + ]; + + // Write the file + $jsonContent = json_encode($exampleData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + if (file_put_contents($filePath, $jsonContent) === false) { + $this->error("Failed to create changelog file: {$filePath}"); + + return self::FAILURE; + } + + $this->info("✅ Created changelog file: changelogs/{$month}.json"); + $this->line(" Example entry created for {$monthName}"); + $this->line(' Edit the file to add your actual changelog entries'); + + // Show the file contents + if ($this->option('verbose')) { + $this->newLine(); + $this->line('File contents:'); + $this->line($jsonContent); + } + + return self::SUCCESS; + } +} diff --git a/app/Livewire/SettingsDropdown.php b/app/Livewire/SettingsDropdown.php new file mode 100644 index 000000000..5bb3e37a4 --- /dev/null +++ b/app/Livewire/SettingsDropdown.php @@ -0,0 +1,52 @@ +getUnreadChangelogCount(); + } + + public function getEntriesProperty() + { + $user = Auth::user(); + + return app(ChangelogService::class)->getEntriesForUser($user); + } + + public function openWhatsNewModal() + { + $this->showWhatsNewModal = true; + } + + public function closeWhatsNewModal() + { + $this->showWhatsNewModal = false; + } + + public function markAsRead($identifier) + { + app(ChangelogService::class)->markAsReadForUser($identifier, Auth::user()); + } + + public function markAllAsRead() + { + app(ChangelogService::class)->markAllAsReadForUser(Auth::user()); + } + + public function render() + { + return view('livewire.settings-dropdown', [ + 'entries' => $this->entries, + 'unreadCount' => $this->unreadCount, + ]); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 6cd1b66db..3c5a220f8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -203,6 +203,16 @@ public function teams() return $this->belongsToMany(Team::class)->withPivot('role'); } + public function changelogReads() + { + return $this->hasMany(UserChangelogRead::class); + } + + public function getUnreadChangelogCount(): int + { + return app(\App\Services\ChangelogService::class)->getUnreadCountForUser($this); + } + public function getRecipients(): array { return [$this->email]; diff --git a/app/Models/UserChangelogRead.php b/app/Models/UserChangelogRead.php new file mode 100644 index 000000000..28c384cb5 --- /dev/null +++ b/app/Models/UserChangelogRead.php @@ -0,0 +1,48 @@ + 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public static function markAsRead(int $userId, string $identifier): void + { + self::firstOrCreate([ + 'user_id' => $userId, + 'changelog_identifier' => $identifier, + ], [ + 'read_at' => now(), + ]); + } + + public static function isReadByUser(int $userId, string $identifier): bool + { + return self::where('user_id', $userId) + ->where('changelog_identifier', $identifier) + ->exists(); + } + + public static function getReadIdentifiersForUser(int $userId): array + { + return self::where('user_id', $userId) + ->pluck('changelog_identifier') + ->toArray(); + } +} diff --git a/app/Services/ChangelogService.php b/app/Services/ChangelogService.php new file mode 100644 index 000000000..c6f4c8752 --- /dev/null +++ b/app/Services/ChangelogService.php @@ -0,0 +1,294 @@ +fetchChangelogData(); + + if (! $data || ! isset($data['entries'])) { + return collect(); + } + + return collect($data['entries']) + ->filter(fn ($entry) => $this->validateEntryData($entry)) + ->map(function ($entry) { + $entry['published_at'] = Carbon::parse($entry['published_at']); + $entry['content_html'] = $this->parseMarkdown($entry['content']); + + return (object) $entry; + }) + ->filter(fn ($entry) => $entry->published_at <= now()) + ->sortBy('published_at') + ->reverse() + ->values(); + } + + // Load entries from recent months for performance + $availableMonths = $this->getAvailableMonths(); + $monthsToLoad = $availableMonths->take($recentMonths); + + return $monthsToLoad + ->flatMap(fn ($month) => $this->getEntriesForMonth($month)) + ->sortBy('published_at') + ->reverse() + ->values(); + } + + public function getAllEntries(): Collection + { + $availableMonths = $this->getAvailableMonths(); + + return $availableMonths + ->flatMap(fn ($month) => $this->getEntriesForMonth($month)) + ->sortBy('published_at') + ->reverse() + ->values(); + } + + public function getEntriesForUser(User $user): Collection + { + $entries = $this->getEntries(); + $readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id); + + return $entries->map(function ($entry) use ($readIdentifiers) { + $entry->is_read = in_array($entry->version, $readIdentifiers); + + return $entry; + })->sortBy([ + ['is_read', 'asc'], // unread first + ['published_at', 'desc'], // then by date + ])->values(); + } + + public function getUnreadCountForUser(User $user): int + { + if (isDev()) { + $entries = $this->getEntries(); + $readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id); + + return $entries->reject(fn ($entry) => in_array($entry->version, $readIdentifiers))->count(); + } else { + return Cache::remember( + 'user_unread_changelog_count_'.$user->id, + now()->addHour(), + function () use ($user) { + $entries = $this->getEntries(); + $readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id); + + return $entries->reject(fn ($entry) => in_array($entry->version, $readIdentifiers))->count(); + } + ); + } + } + + public function getAvailableMonths(): Collection + { + $pattern = base_path('changelogs/*.json'); + $files = glob($pattern); + + if ($files === false) { + return collect(); + } + + return collect($files) + ->map(fn ($file) => basename($file, '.json')) + ->filter(fn ($name) => preg_match('/^\d{4}-\d{2}$/', $name)) + ->sort() + ->reverse() + ->values(); + } + + public function getEntriesForMonth(string $month): Collection + { + $path = base_path("changelogs/{$month}.json"); + + if (! file_exists($path)) { + return collect(); + } + + $content = file_get_contents($path); + + if ($content === false) { + Log::error("Failed to read changelog file: {$month}.json"); + + return collect(); + } + + $data = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + Log::error("Invalid JSON in {$month}.json: ".json_last_error_msg()); + + return collect(); + } + + if (! isset($data['entries']) || ! is_array($data['entries'])) { + return collect(); + } + + return collect($data['entries']) + ->filter(fn ($entry) => $this->validateEntryData($entry)) + ->map(function ($entry) { + $entry['published_at'] = Carbon::parse($entry['published_at']); + $entry['content_html'] = $this->parseMarkdown($entry['content']); + + return (object) $entry; + }) + ->filter(fn ($entry) => $entry->published_at <= now()) + ->sortBy('published_at') + ->reverse() + ->values(); + } + + private function fetchChangelogData(): ?array + { + // Legacy support for old changelog.json + $path = base_path('changelog.json'); + + if (file_exists($path)) { + $content = file_get_contents($path); + + if ($content === false) { + Log::error('Failed to read changelog.json file'); + + return null; + } + + $data = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + Log::error('Invalid JSON in changelog.json: '.json_last_error_msg()); + + return null; + } + + return $data; + } + + // New monthly structure - combine all months + $allEntries = []; + foreach ($this->getAvailableMonths() as $month) { + $monthEntries = $this->getEntriesForMonth($month); + foreach ($monthEntries as $entry) { + $allEntries[] = (array) $entry; + } + } + + return ['entries' => $allEntries]; + } + + public function markAsReadForUser(string $version, User $user): void + { + UserChangelogRead::markAsRead($user->id, $version); + Cache::forget('user_unread_changelog_count_'.$user->id); + } + + public function markAllAsReadForUser(User $user): void + { + $entries = $this->getEntries(); + + foreach ($entries as $entry) { + UserChangelogRead::markAsRead($user->id, $entry->version); + } + + Cache::forget('user_unread_changelog_count_'.$user->id); + } + + private function validateEntryData(array $data): bool + { + $required = ['version', 'title', 'content', 'published_at']; + + foreach ($required as $field) { + if (! isset($data[$field]) || empty($data[$field])) { + return false; + } + } + + return true; + } + + public function clearAllReadStatus(): array + { + try { + $count = UserChangelogRead::count(); + UserChangelogRead::truncate(); + + // Clear all user caches + $this->clearAllUserCaches(); + + return [ + 'success' => true, + 'message' => "Successfully cleared {$count} read status records", + ]; + } catch (\Exception $e) { + Log::error('Failed to clear read status: '.$e->getMessage()); + + return [ + 'success' => false, + 'message' => 'Failed to clear read status: '.$e->getMessage(), + ]; + } + } + + private function clearAllUserCaches(): void + { + $users = User::select('id')->get(); + + foreach ($users as $user) { + Cache::forget('user_unread_changelog_count_'.$user->id); + } + } + + private function parseMarkdown(string $content): string + { + // Convert markdown to HTML using simple regex patterns + $html = $content; + + // Headers + $html = preg_replace('/^### (.*?)$/m', '

$1

', $html); + $html = preg_replace('/^## (.*?)$/m', '

$1

', $html); + $html = preg_replace('/^# (.*?)$/m', '

$1

', $html); + + // Bold text + $html = preg_replace('/\*\*(.*?)\*\*/', '$1', $html); + $html = preg_replace('/__(.*?)__/', '$1', $html); + + // Italic text + $html = preg_replace('/\*(.*?)\*/', '$1', $html); + $html = preg_replace('/_(.*?)_/', '$1', $html); + + // Code blocks + $html = preg_replace('/```(.*?)```/s', '
$1
', $html); + + // Inline code + $html = preg_replace('/`([^`]+)`/', '$1', $html); + + // Links + $html = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1', $html); + + // Line breaks (convert double newlines to paragraphs) + $paragraphs = preg_split('/\n\s*\n/', trim($html)); + $html = '

'.implode('

', $paragraphs).'

'; + + // Single line breaks + $html = preg_replace('/\n/', '
', $html); + + // Unordered lists + $html = preg_replace('/^\- (.*)$/m', '
  • • $1
  • ', $html); + $html = preg_replace('/(
  • .*<\/li>)/s', '
      $1
    ', $html); + + return $html; + } +} diff --git a/config/services.php b/config/services.php index 7add50a5c..6a21cda18 100644 --- a/config/services.php +++ b/config/services.php @@ -65,6 +65,6 @@ 'client_secret' => env('ZITADEL_CLIENT_SECRET'), 'redirect' => env('ZITADEL_REDIRECT_URI'), 'base_url' => env('ZITADEL_BASE_URL'), - ] + ], ]; diff --git a/database/migrations/2025_08_07_142403_create_user_changelog_reads_table.php b/database/migrations/2025_08_07_142403_create_user_changelog_reads_table.php new file mode 100644 index 000000000..4c340d106 --- /dev/null +++ b/database/migrations/2025_08_07_142403_create_user_changelog_reads_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->string('changelog_identifier'); + $table->timestamp('read_at'); + $table->timestamps(); + + $table->unique(['user_id', 'changelog_identifier']); + $table->index('user_id'); + $table->index('changelog_identifier'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_changelog_reads'); + } +}; diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index a2a4b5fa3..6c9628a81 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -120,6 +120,7 @@ COPY --chown=www-data:www-data templates ./templates COPY --chown=www-data:www-data resources/views ./resources/views COPY --chown=www-data:www-data artisan artisan COPY --chown=www-data:www-data openapi.yaml ./openapi.yaml +COPY --chown=www-data:www-data changelogs/ ./changelogs/ RUN composer dump-autoload diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index be26d55ca..1a145aa4b 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -1,119 +1,88 @@ -
  • diff --git a/resources/views/livewire/security/cloud-provider-tokens.blade.php b/resources/views/livewire/security/cloud-provider-tokens.blade.php new file mode 100644 index 000000000..d29631c4c --- /dev/null +++ b/resources/views/livewire/security/cloud-provider-tokens.blade.php @@ -0,0 +1,66 @@ +
    +

    Cloud Provider Tokens

    +
    Manage API tokens for cloud providers (Hetzner, DigitalOcean, etc.). Tokens are saved encrypted and shared with your team.
    + +

    New Token

    + @can('create', App\Models\CloudProviderToken::class) +
    +
    +
    + + + + +
    +
    + +
    +
    +
    +
    + +
    + Add Token +
    +
    + @endcan + +

    Saved Tokens

    +
    + @forelse ($tokens as $savedToken) +
    +
    + + {{ strtoupper($savedToken->provider) }} + + {{ $savedToken->name }} +
    +
    Token: ***{{ substr($savedToken->token, -4) }}
    +
    Created: {{ $savedToken->created_at->diffForHumans() }}
    + + @can('delete', $savedToken) + + @endcan +
    + @empty +
    +
    No cloud provider tokens found.
    +
    + @endforelse +
    +
    diff --git a/resources/views/livewire/security/cloud-tokens.blade.php b/resources/views/livewire/security/cloud-tokens.blade.php new file mode 100644 index 000000000..2edbcd30f --- /dev/null +++ b/resources/views/livewire/security/cloud-tokens.blade.php @@ -0,0 +1,7 @@ +
    + + Cloud Tokens | Coolify + + + +
    diff --git a/resources/views/livewire/server/create.blade.php b/resources/views/livewire/server/create.blade.php index acab92374..619a827e7 100644 --- a/resources/views/livewire/server/create.blade.php +++ b/resources/views/livewire/server/create.blade.php @@ -1,3 +1,23 @@
    - +
    + @can('viewAny', App\Models\CloudProviderToken::class) +
    +

    Add Server from Cloud Provider

    +
    + + + +
    +
    + +
    + @endcan + +
    +

    Add Server by IP Address

    + +
    +
    diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php new file mode 100644 index 000000000..83de355e3 --- /dev/null +++ b/resources/views/livewire/server/new/by-hetzner.blade.php @@ -0,0 +1,52 @@ +
    + @if ($limit_reached) + + @else +
    + @if ($available_tokens->count() > 0) +
    + + + @foreach ($available_tokens as $token) + + @endforeach + +
    +
    OR
    + @endif + +
    + +
    + +
    + +
    + + @if ($save_token) +
    + +
    + @endif + + + Continue + +
    + @endif +
    diff --git a/routes/web.php b/routes/web.php index fd2ed8730..e967586c4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -34,6 +34,7 @@ use App\Livewire\Project\Shared\ScheduledTask\Show as ScheduledTaskShow; use App\Livewire\Project\Show as ProjectShow; use App\Livewire\Security\ApiTokens; +use App\Livewire\Security\CloudTokens; use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex; use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow; use App\Livewire\Server\Advanced as ServerAdvanced; @@ -271,6 +272,7 @@ // Route::get('/security/private-key/new', SecurityPrivateKeyCreate::class)->name('security.private-key.create'); Route::get('/security/private-key/{private_key_uuid}', SecurityPrivateKeyShow::class)->name('security.private-key.show'); + Route::get('/security/cloud-tokens', CloudTokens::class)->name('security.cloud-tokens'); Route::get('/security/api-tokens', ApiTokens::class)->name('security.api-tokens'); }); From 354eadd02f18872ef9aa50476f689e5186fbba19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C3=96zg=C3=BCr?= <226712+halilim@users.noreply.github.com> Date: Thu, 9 Oct 2025 00:14:17 +0300 Subject: [PATCH 0806/1717] Gramps Web: Pin to a version --- templates/compose/gramps-web.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/compose/gramps-web.yaml b/templates/compose/gramps-web.yaml index 04a284ff2..fa3b02c6f 100644 --- a/templates/compose/gramps-web.yaml +++ b/templates/compose/gramps-web.yaml @@ -7,7 +7,7 @@ services: grampsweb: - image: ghcr.io/gramps-project/grampsweb:latest + image: ghcr.io/gramps-project/grampsweb:25.9.0 environment: - SERVICE_URL_GRAMPSWEB_5000 - GRAMPSWEB_TREE=${GRAMPSWEB_TREE:-Gramps Web} # will create a new tree if not exists @@ -33,7 +33,7 @@ services: retries: 3 grampsweb_celery: - image: ghcr.io/gramps-project/grampsweb:latest + image: ghcr.io/gramps-project/grampsweb:25.9.0 environment: - GRAMPSWEB_TREE=${GRAMPSWEB_TREE:-Gramps Web} # will create a new tree if not exists - GRAMPSWEB_CELERY_CONFIG__broker_url=redis://grampsweb_redis:6379/0 From ee6b8c9c042b746dcd39cdd1b1991b8efe4b8663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C3=96zg=C3=BCr?= <226712+halilim@users.noreply.github.com> Date: Thu, 9 Oct 2025 00:19:17 +0300 Subject: [PATCH 0807/1717] Revert unintentional changes --- .github/workflows/claude-code-review.yml | 39 ++++++++++++++++----- .github/workflows/claude.yml | 44 +++++++++++++++--------- 2 files changed, 57 insertions(+), 26 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index d73398046..a2c92df59 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -34,13 +34,15 @@ jobs: - name: Run Claude Code Review id: claude-review - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@beta with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - prompt: | - REPO: ${{ github.repository }} - PR NUMBER: ${{ github.event.pull_request.number }} + # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1) + # model: "claude-opus-4-1-20250805" + + # Direct prompt for automated review (no @claude mention needed) + direct_prompt: | Please review this pull request and provide feedback on: - Code quality and best practices - Potential bugs or issues @@ -48,11 +50,30 @@ jobs: - Security concerns - Test coverage - Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + Be constructive and helpful in your feedback. - Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR + # use_sticky_comment: true - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options - claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' + # Optional: Customize review based on file types + # direct_prompt: | + # Review this PR focusing on: + # - For TypeScript files: Type safety and proper interface usage + # - For API endpoints: Security, input validation, and error handling + # - For React components: Performance, accessibility, and best practices + # - For tests: Coverage, edge cases, and test quality + + # Optional: Different prompts for different authors + # direct_prompt: | + # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && + # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || + # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} + + # Optional: Add specific tools for running tests or linting + # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" + + # Optional: Skip review for certain conditions + # if: | + # !contains(github.event.pull_request.title, '[skip-review]') && + # !contains(github.event.pull_request.title, '[WIP]') diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index e93c95bff..9daf0e90e 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -16,6 +16,8 @@ jobs: (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'Claude') || + (github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'Claude') || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) runs-on: ubuntu-latest permissions: @@ -34,22 +36,30 @@ jobs: id: claude uses: anthropics/claude-code-action@v1 with: - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read + anthropic_api_key: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | actions: read - - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' - - # Optional: Add claude_args to customize behavior and configuration - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options - # claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)' - + + # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1) + # model: "claude-opus-4-1-20250805" + + # Optional: Customize the trigger phrase (default: @claude) + # trigger_phrase: "/claude" + + # Optional: Trigger when specific user is assigned to an issue + # assignee_trigger: "claude-bot" + + # Optional: Allow Claude to run specific commands + # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" + + # Optional: Add custom instructions for Claude to customize its behavior for your project + # custom_instructions: | + # Follow our coding standards + # Ensure all new code has tests + # Use TypeScript for new files + + # Optional: Custom environment variables for Claude + # claude_env: | + # NODE_ENV: test From 42f671cfa249b5630c20331cb152c8b3caa824d3 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:49:33 +0530 Subject: [PATCH 0808/1717] Added healthcheck to moodle service --- templates/compose/moodle.yaml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/templates/compose/moodle.yaml b/templates/compose/moodle.yaml index 3a8e02478..41b7916b9 100644 --- a/templates/compose/moodle.yaml +++ b/templates/compose/moodle.yaml @@ -18,9 +18,18 @@ services: - MARIADB_COLLATE=utf8mb4_unicode_ci volumes: - mariadb-data:/var/lib/mysql + healthcheck: + test: + - CMD-SHELL + - bash -c ' Date: Thu, 9 Oct 2025 13:26:10 +0530 Subject: [PATCH 0809/1717] Changed docker --time command to -t since --time is deprecated on v28 --- app/Actions/Application/StopApplication.php | 2 +- app/Actions/Application/StopApplicationOneServer.php | 2 +- app/Actions/Database/StopDatabase.php | 2 +- app/Actions/Proxy/StopProxy.php | 2 +- app/Actions/Service/StopService.php | 2 +- app/Jobs/ApplicationDeploymentJob.php | 2 +- app/Jobs/DeleteResourceJob.php | 2 +- app/Livewire/Project/Application/Previews.php | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index ee3398b04..94651a3c1 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -39,7 +39,7 @@ public function handle(Application $application, bool $previewDeployments = fals foreach ($containersToStop as $containerName) { instant_remote_process(command: [ - "docker stop --time=30 $containerName", + "docker stop -t 30 $containerName", "docker rm -f $containerName", ], server: $server, throwError: false); } diff --git a/app/Actions/Application/StopApplicationOneServer.php b/app/Actions/Application/StopApplicationOneServer.php index 600b1cb9a..bf9fdee72 100644 --- a/app/Actions/Application/StopApplicationOneServer.php +++ b/app/Actions/Application/StopApplicationOneServer.php @@ -26,7 +26,7 @@ public function handle(Application $application, Server $server) if ($containerName) { instant_remote_process( [ - "docker stop --time=30 $containerName", + "docker stop -t 30 $containerName", "docker rm -f $containerName", ], $server diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 5c881e743..c024c14e1 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -49,7 +49,7 @@ private function stopContainer($database, string $containerName, int $timeout = { $server = $database->destination->server; instant_remote_process(command: [ - "docker stop --time=$timeout $containerName", + "docker stop -t $timeout $containerName", "docker rm -f $containerName", ], server: $server, throwError: false); } diff --git a/app/Actions/Proxy/StopProxy.php b/app/Actions/Proxy/StopProxy.php index 29cc63b40..310185473 100644 --- a/app/Actions/Proxy/StopProxy.php +++ b/app/Actions/Proxy/StopProxy.php @@ -21,7 +21,7 @@ public function handle(Server $server, bool $forceStop = true, int $timeout = 30 ProxyStatusChangedUI::dispatch($server->team_id); instant_remote_process(command: [ - "docker stop --time=$timeout $containerName", + "docker stop -t $timeout $containerName", "docker rm -f $containerName", ], server: $server, throwError: false); diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 3f4e96479..23b41e3f2 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -54,7 +54,7 @@ private function stopContainersInParallel(array $containersToStop, Server $serve $timeout = count($containersToStop) > 5 ? 10 : 30; $commands = []; $containerList = implode(' ', $containersToStop); - $commands[] = "docker stop --time=$timeout $containerList"; + $commands[] = "docker stop -t $timeout $containerList"; $commands[] = "docker rm -f $containerList"; instant_remote_process( command: $commands, diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 8ffaabde5..381857f83 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2890,7 +2890,7 @@ private function graceful_shutdown_container(string $containerName) try { $timeout = isDev() ? 1 : 30; $this->execute_remote_command( - ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true], + ["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true], ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] ); } catch (Exception $error) { diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index b9fbebcc9..45f113d96 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -153,7 +153,7 @@ private function stopPreviewContainers(array $containers, $server, int $timeout $containerList = implode(' ', array_map('escapeshellarg', $containerNames)); $commands = [ - "docker stop --time=$timeout $containerList", + "docker stop -t $timeout $containerList", "docker rm -f $containerList", ]; diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 1cb2ef2c5..7a6e8c415 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -237,7 +237,7 @@ private function stopContainers(array $containers, $server) foreach ($containersToStop as $containerName) { instant_remote_process(command: [ - "docker stop --time=30 $containerName", + "docker stop -t 30 $containerName", "docker rm -f $containerName", ], server: $server, throwError: false); } From 215301fa8f957164982a21c3af67b71e579c1daf Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:41:29 +0200 Subject: [PATCH 0810/1717] basics of adding / removing hetzner servers --- app/Actions/Server/DeleteServer.php | 49 ++- app/Livewire/Security/CloudProviderTokens.php | 2 +- app/Livewire/Server/Delete.php | 19 +- app/Livewire/Server/New/ByHetzner.php | 372 ++++++++++++++++-- app/Models/PrivateKey.php | 11 + app/Models/Server.php | 1 + app/Services/HetznerService.php | 96 +++++ ...5_10_03_154100_update_clickhouse_image.php | 64 +-- ...add_hetzner_server_id_to_servers_table.php | 28 ++ public/svgs/hetzner.svg | 6 + .../components/modal-confirmation.blade.php | 12 + .../views/livewire/server/create.blade.php | 14 +- .../views/livewire/server/delete.blade.php | 17 +- .../livewire/server/new/by-hetzner.blade.php | 157 ++++++-- .../views/livewire/server/show.blade.php | 15 +- 15 files changed, 744 insertions(+), 119 deletions(-) create mode 100644 app/Services/HetznerService.php create mode 100644 database/migrations/2025_10_08_185203_add_hetzner_server_id_to_servers_table.php create mode 100644 public/svgs/hetzner.svg diff --git a/app/Actions/Server/DeleteServer.php b/app/Actions/Server/DeleteServer.php index 15c892e75..db197a019 100644 --- a/app/Actions/Server/DeleteServer.php +++ b/app/Actions/Server/DeleteServer.php @@ -2,16 +2,63 @@ namespace App\Actions\Server; +use App\Models\CloudProviderToken; use App\Models\Server; +use App\Services\HetznerService; use Lorisleiva\Actions\Concerns\AsAction; class DeleteServer { use AsAction; - public function handle(Server $server) + public function handle(Server $server, bool $deleteFromHetzner = false) { + // Delete from Hetzner Cloud if requested and server has hetzner_server_id + if ($deleteFromHetzner && $server->hetzner_server_id) { + $this->deleteFromHetzner($server); + } + StopSentinel::run($server); $server->forceDelete(); } + + private function deleteFromHetzner(Server $server): void + { + try { + // Get the cloud provider token for Hetzner + $token = CloudProviderToken::where('team_id', $server->team_id) + ->where('provider', 'hetzner') + ->first(); + + if (! $token) { + ray('No Hetzner token found for team, skipping Hetzner deletion', [ + 'team_id' => $server->team_id, + 'server_id' => $server->id, + ]); + + return; + } + + $hetznerService = new HetznerService($token->token); + $hetznerService->deleteServer($server->hetzner_server_id); + + ray('Deleted server from Hetzner', [ + 'hetzner_server_id' => $server->hetzner_server_id, + 'server_id' => $server->id, + ]); + } catch (\Throwable $e) { + ray('Failed to delete server from Hetzner', [ + 'error' => $e->getMessage(), + 'hetzner_server_id' => $server->hetzner_server_id, + 'server_id' => $server->id, + ]); + + // Log the error but don't prevent the server from being deleted from Coolify + logger()->error('Failed to delete server from Hetzner', [ + 'error' => $e->getMessage(), + 'hetzner_server_id' => $server->hetzner_server_id, + 'server_id' => $server->id, + ]); + } + } } diff --git a/app/Livewire/Security/CloudProviderTokens.php b/app/Livewire/Security/CloudProviderTokens.php index f35a3a806..f5726e424 100644 --- a/app/Livewire/Security/CloudProviderTokens.php +++ b/app/Livewire/Security/CloudProviderTokens.php @@ -100,7 +100,7 @@ public function addNewToken() public function deleteToken(int $tokenId) { try { - $token = CloudProviderToken::findOrFail($tokenId); + $token = CloudProviderToken::ownedByCurrentTeam()->findOrFail($tokenId); $this->authorize('delete', $token); $token->delete(); diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php index b9e3944b5..6d12895eb 100644 --- a/app/Livewire/Server/Delete.php +++ b/app/Livewire/Server/Delete.php @@ -16,6 +16,8 @@ class Delete extends Component public Server $server; + public bool $delete_from_hetzner = false; + public function mount(string $server_uuid) { try { @@ -41,8 +43,9 @@ public function delete($password) return; } + $this->server->delete(); - DeleteServer::dispatch($this->server); + DeleteServer::dispatch($this->server, $this->delete_from_hetzner); return redirect()->route('server.index'); } catch (\Throwable $e) { @@ -52,6 +55,18 @@ public function delete($password) public function render() { - return view('livewire.server.delete'); + $checkboxes = []; + + if ($this->server->hetzner_server_id) { + $checkboxes[] = [ + 'id' => 'delete_from_hetzner', + 'label' => 'Also delete server from Hetzner Cloud', + 'default_warning' => 'The actual server on Hetzner Cloud will NOT be deleted.', + ]; + } + + return view('livewire.server.delete', [ + 'checkboxes' => $checkboxes, + ]); } } diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php index d509adcb6..b67411e17 100644 --- a/app/Livewire/Server/New/ByHetzner.php +++ b/app/Livewire/Server/New/ByHetzner.php @@ -2,9 +2,12 @@ namespace App\Livewire\Server\New; +use App\Enums\ProxyTypes; use App\Models\CloudProviderToken; +use App\Models\PrivateKey; use App\Models\Server; use App\Models\Team; +use App\Services\HetznerService; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; @@ -15,6 +18,10 @@ class ByHetzner extends Component { use AuthorizesRequests; + // Step tracking + public int $current_step = 1; + + // Locked data #[Locked] public Collection $available_tokens; @@ -24,6 +31,7 @@ class ByHetzner extends Component #[Locked] public $limit_reached; + // Step 1: Token selection public ?int $selected_token_id = null; public string $hetzner_token = ''; @@ -32,22 +40,69 @@ class ByHetzner extends Component public ?string $token_name = null; + // Step 2: Server configuration + public array $locations = []; + + public array $images = []; + + public array $serverTypes = []; + + public ?string $selected_location = null; + + public ?int $selected_image = null; + + public ?string $selected_server_type = null; + + public string $server_name = ''; + + public bool $start_after_create = true; + + public ?int $private_key_id = null; + + public bool $loading_data = false; + public function mount() { $this->authorize('viewAny', CloudProviderToken::class); $this->available_tokens = CloudProviderToken::ownedByCurrentTeam() ->where('provider', 'hetzner') ->get(); + $this->server_name = generate_random_name(); + if ($this->private_keys->count() > 0) { + $this->private_key_id = $this->private_keys->first()->id; + } } protected function rules(): array { - return [ + $rules = [ 'selected_token_id' => 'nullable|integer', 'hetzner_token' => 'required_without:selected_token_id|string', 'save_token' => 'boolean', - 'token_name' => 'required_if:save_token,true|nullable|string|max:255', + 'token_name' => [ + 'nullable', + 'string', + 'max:255', + function ($attribute, $value, $fail) { + if ($this->save_token && ! empty($this->hetzner_token) && empty($value)) { + $fail('Please provide a name for the token.'); + } + }, + ], ]; + + if ($this->current_step === 2) { + $rules = array_merge($rules, [ + 'server_name' => 'required|string|max:255', + 'selected_location' => 'required|string', + 'selected_image' => 'required|integer', + 'selected_server_type' => 'required|string', + 'private_key_id' => 'required|integer|exists:private_keys,id,team_id,'.currentTeam()->id, + 'start_after_create' => 'boolean', + ]); + } + + return $rules; } protected function messages(): array @@ -76,6 +131,275 @@ private function validateHetznerToken(string $token): bool } } + private function getHetznerToken(): string + { + if ($this->selected_token_id) { + $token = $this->available_tokens->firstWhere('id', $this->selected_token_id); + + return $token ? $token->token : ''; + } + + return $this->hetzner_token; + } + + public function nextStep() + { + // Validate step 1 + $this->validate([ + 'selected_token_id' => 'nullable|integer', + 'hetzner_token' => 'required_without:selected_token_id|string', + 'save_token' => 'boolean', + 'token_name' => [ + 'nullable', + 'string', + 'max:255', + function ($attribute, $value, $fail) { + if ($this->save_token && ! empty($this->hetzner_token) && empty($value)) { + $fail('Please provide a name for the token.'); + } + }, + ], + ]); + + try { + $hetznerToken = $this->getHetznerToken(); + + if (! $hetznerToken) { + return $this->dispatch('error', 'Please provide a valid Hetzner API token.'); + } + + // Validate token if it's a new one + if (! $this->selected_token_id) { + if (! $this->validateHetznerToken($hetznerToken)) { + return $this->dispatch('error', 'Invalid Hetzner API token. Please check your token and try again.'); + } + + // Save token if requested + if ($this->save_token) { + CloudProviderToken::create([ + 'team_id' => currentTeam()->id, + 'provider' => 'hetzner', + 'token' => $this->hetzner_token, + 'name' => $this->token_name, + ]); + } + } + + // Load Hetzner data + $this->loadHetznerData($hetznerToken); + + // Move to step 2 + $this->current_step = 2; + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function previousStep() + { + $this->current_step = 1; + } + + private function loadHetznerData(string $token) + { + $this->loading_data = true; + + try { + $hetznerService = new HetznerService($token); + + $this->locations = $hetznerService->getLocations(); + $this->serverTypes = $hetznerService->getServerTypes(); + + // Get images and sort by name + $images = $hetznerService->getImages(); + + ray('Raw images from Hetzner API', [ + 'total_count' => count($images), + 'types' => collect($images)->pluck('type')->unique()->values(), + 'sample' => array_slice($images, 0, 3), + ]); + + $this->images = collect($images) + ->filter(function ($image) { + // Only system images + if (! isset($image['type']) || $image['type'] !== 'system') { + return false; + } + + // Filter out deprecated images + if (isset($image['deprecated']) && $image['deprecated'] === true) { + return false; + } + + return true; + }) + ->sortBy('name') + ->values() + ->toArray(); + + ray('Filtered images', [ + 'filtered_count' => count($this->images), + 'debian_images' => collect($this->images)->filter(fn ($img) => str_contains($img['name'] ?? '', 'debian'))->values(), + ]); + + $this->loading_data = false; + } catch (\Throwable $e) { + $this->loading_data = false; + throw $e; + } + } + + public function getAvailableServerTypesProperty() + { + ray('Getting available server types', [ + 'selected_location' => $this->selected_location, + 'total_server_types' => count($this->serverTypes), + ]); + + if (! $this->selected_location) { + return $this->serverTypes; + } + + $filtered = collect($this->serverTypes) + ->filter(function ($type) { + if (! isset($type['locations'])) { + return false; + } + + $locationNames = collect($type['locations'])->pluck('name')->toArray(); + + return in_array($this->selected_location, $locationNames); + }) + ->values() + ->toArray(); + + ray('Filtered server types', [ + 'selected_location' => $this->selected_location, + 'filtered_count' => count($filtered), + ]); + + return $filtered; + } + + public function getAvailableImagesProperty() + { + ray('Getting available images', [ + 'selected_server_type' => $this->selected_server_type, + 'total_images' => count($this->images), + 'images' => $this->images, + ]); + + if (! $this->selected_server_type) { + return $this->images; + } + + $serverType = collect($this->serverTypes)->firstWhere('name', $this->selected_server_type); + + ray('Server type data', $serverType); + + if (! $serverType || ! isset($serverType['architecture'])) { + ray('No architecture in server type, returning all'); + + return $this->images; + } + + $architecture = $serverType['architecture']; + + $filtered = collect($this->images) + ->filter(fn ($image) => ($image['architecture'] ?? null) === $architecture) + ->values() + ->toArray(); + + ray('Filtered images', [ + 'architecture' => $architecture, + 'filtered_count' => count($filtered), + ]); + + return $filtered; + } + + public function updatedSelectedLocation($value) + { + ray('Location selected', $value); + + // Reset server type and image when location changes + $this->selected_server_type = null; + $this->selected_image = null; + } + + public function updatedSelectedServerType($value) + { + ray('Server type selected', $value); + + // Reset image when server type changes + $this->selected_image = null; + } + + public function updatedSelectedImage($value) + { + ray('Image selected', $value); + } + + private function createHetznerServer(string $token): array + { + $hetznerService = new HetznerService($token); + + // Get the private key and extract public key + $privateKey = PrivateKey::ownedByCurrentTeam()->findOrFail($this->private_key_id); + + $publicKey = $privateKey->getPublicKey(); + $md5Fingerprint = PrivateKey::generateMd5Fingerprint($privateKey->private_key); + + ray('Private Key Info', [ + 'private_key_id' => $this->private_key_id, + 'sha256_fingerprint' => $privateKey->fingerprint, + 'md5_fingerprint' => $md5Fingerprint, + ]); + + // Check if SSH key already exists on Hetzner by comparing MD5 fingerprints + $existingSshKeys = $hetznerService->getSshKeys(); + $existingKey = null; + + ray('Existing SSH Keys on Hetzner', $existingSshKeys); + + foreach ($existingSshKeys as $key) { + if ($key['fingerprint'] === $md5Fingerprint) { + $existingKey = $key; + break; + } + } + + // Upload SSH key if it doesn't exist + if ($existingKey) { + $sshKeyId = $existingKey['id']; + ray('Using existing SSH key', ['ssh_key_id' => $sshKeyId]); + } else { + $sshKeyName = $privateKey->name; + $uploadedKey = $hetznerService->uploadSshKey($sshKeyName, $publicKey); + $sshKeyId = $uploadedKey['id']; + ray('Uploaded new SSH key', ['ssh_key_id' => $sshKeyId, 'name' => $sshKeyName]); + } + + // Prepare server creation parameters + $params = [ + 'name' => $this->server_name, + 'server_type' => $this->selected_server_type, + 'image' => $this->selected_image, + 'location' => $this->selected_location, + 'start_after_create' => $this->start_after_create, + 'ssh_keys' => [$sshKeyId], + ]; + + ray('Server creation parameters', $params); + + // Create server on Hetzner + $hetznerServer = $hetznerService->createServer($params); + + ray('Hetzner server created', $hetznerServer); + + return $hetznerServer; + } + public function submit() { $this->validate(); @@ -87,35 +411,27 @@ public function submit() return $this->dispatch('error', 'You have reached the server limit for your subscription.'); } - // Determine which token to use - if ($this->selected_token_id) { - $token = $this->available_tokens->firstWhere('id', $this->selected_token_id); - if (! $token) { - return $this->dispatch('error', 'Selected token not found.'); - } - $hetznerToken = $token->token; - } else { - $hetznerToken = $this->hetzner_token; + $hetznerToken = $this->getHetznerToken(); - // Validate the new token before saving - if (! $this->validateHetznerToken($hetznerToken)) { - return $this->dispatch('error', 'Invalid Hetzner API token. Please check your token and try again.'); - } + // Create server on Hetzner + $hetznerServer = $this->createHetznerServer($hetznerToken); - // If saving the new token - if ($this->save_token) { - CloudProviderToken::create([ - 'team_id' => currentTeam()->id, - 'provider' => 'hetzner', - 'token' => $this->hetzner_token, - 'name' => $this->token_name, - ]); - } - } + // Create server in Coolify database + $server = Server::create([ + 'name' => $this->server_name, + 'ip' => $hetznerServer['public_net']['ipv4']['ip'], + 'user' => 'root', + 'port' => 22, + 'team_id' => currentTeam()->id, + 'private_key_id' => $this->private_key_id, + 'hetzner_server_id' => $hetznerServer['id'], + ]); - // TODO: Actual Hetzner server provisioning will be implemented in future phase - // The $hetznerToken variable contains the token to use - return $this->dispatch('success', 'Hetzner token validated successfully! Server provisioning coming soon.'); + $server->proxy->set('status', 'exited'); + $server->proxy->set('type', ProxyTypes::TRAEFIK->value); + $server->save(); + + return redirect()->route('server.show', $server->uuid); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index c210f3c5b..08f3f1ebd 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -289,6 +289,17 @@ public static function generateFingerprint($privateKey) } } + public static function generateMd5Fingerprint($privateKey) + { + try { + $key = PublicKeyLoader::load($privateKey); + + return $key->getPublicKey()->getFingerprint('md5'); + } catch (\Throwable $e) { + return null; + } + } + public static function fingerprintExists($fingerprint, $excludeId = null) { $query = self::query() diff --git a/app/Models/Server.php b/app/Models/Server.php index 829a4b5aa..e30b10043 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -162,6 +162,7 @@ protected static function booted() 'description', 'private_key_id', 'team_id', + 'hetzner_server_id', ]; protected $guarded = []; diff --git a/app/Services/HetznerService.php b/app/Services/HetznerService.php new file mode 100644 index 000000000..039eb81a9 --- /dev/null +++ b/app/Services/HetznerService.php @@ -0,0 +1,96 @@ +token = $token; + } + + private function request(string $method, string $endpoint, array $data = []) + { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.$this->token, + ])->timeout(30)->{$method}($this->baseUrl.$endpoint, $data); + + if (! $response->successful()) { + throw new \Exception('Hetzner API error: '.$response->json('error.message', 'Unknown error')); + } + + return $response->json(); + } + + private function requestPaginated(string $method, string $endpoint, string $resourceKey, array $data = []): array + { + $allResults = []; + $page = 1; + + do { + $data['page'] = $page; + $data['per_page'] = 50; + + $response = $this->request($method, $endpoint, $data); + + if (isset($response[$resourceKey])) { + $allResults = array_merge($allResults, $response[$resourceKey]); + } + + $nextPage = $response['meta']['pagination']['next_page'] ?? null; + $page = $nextPage; + } while ($nextPage !== null); + + return $allResults; + } + + public function getLocations(): array + { + return $this->requestPaginated('get', '/locations', 'locations'); + } + + public function getImages(): array + { + return $this->requestPaginated('get', '/images', 'images', [ + 'type' => 'system', + ]); + } + + public function getServerTypes(): array + { + return $this->requestPaginated('get', '/server_types', 'server_types'); + } + + public function getSshKeys(): array + { + return $this->requestPaginated('get', '/ssh_keys', 'ssh_keys'); + } + + public function uploadSshKey(string $name, string $publicKey): array + { + $response = $this->request('post', '/ssh_keys', [ + 'name' => $name, + 'public_key' => $publicKey, + ]); + + return $response['ssh_key'] ?? []; + } + + public function createServer(array $params): array + { + $response = $this->request('post', '/servers', $params); + + return $response['server'] ?? []; + } + + public function deleteServer(int $serverId): void + { + $this->request('delete', "/servers/{$serverId}"); + } +} diff --git a/database/migrations/2025_10_03_154100_update_clickhouse_image.php b/database/migrations/2025_10_03_154100_update_clickhouse_image.php index e52bbcc16..e57354037 100644 --- a/database/migrations/2025_10_03_154100_update_clickhouse_image.php +++ b/database/migrations/2025_10_03_154100_update_clickhouse_image.php @@ -1,32 +1,32 @@ -string('image')->default('bitnamilegacy/clickhouse')->change(); - }); - // Optionally, update any existing rows with the old default to the new one - DB::table('standalone_clickhouses') - ->where('image', 'bitnami/clickhouse') - ->update(['image' => 'bitnamilegacy/clickhouse']); - } - - public function down() - { - Schema::table('standalone_clickhouses', function (Blueprint $table) { - $table->string('image')->default('bitnami/clickhouse')->change(); - }); - // Optionally, revert any changed values - DB::table('standalone_clickhouses') - ->where('image', 'bitnamilegacy/clickhouse') - ->update(['image' => 'bitnami/clickhouse']); - } -}; \ No newline at end of file +string('image')->default('bitnamilegacy/clickhouse')->change(); + }); + // Optionally, update any existing rows with the old default to the new one + DB::table('standalone_clickhouses') + ->where('image', 'bitnami/clickhouse') + ->update(['image' => 'bitnamilegacy/clickhouse']); + } + + public function down() + { + Schema::table('standalone_clickhouses', function (Blueprint $table) { + $table->string('image')->default('bitnami/clickhouse')->change(); + }); + // Optionally, revert any changed values + DB::table('standalone_clickhouses') + ->where('image', 'bitnamilegacy/clickhouse') + ->update(['image' => 'bitnami/clickhouse']); + } +}; diff --git a/database/migrations/2025_10_08_185203_add_hetzner_server_id_to_servers_table.php b/database/migrations/2025_10_08_185203_add_hetzner_server_id_to_servers_table.php new file mode 100644 index 000000000..b1c9ec48b --- /dev/null +++ b/database/migrations/2025_10_08_185203_add_hetzner_server_id_to_servers_table.php @@ -0,0 +1,28 @@ +bigInteger('hetzner_server_id')->nullable()->after('id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('hetzner_server_id'); + }); + } +}; diff --git a/public/svgs/hetzner.svg b/public/svgs/hetzner.svg new file mode 100644 index 000000000..68b1b868d --- /dev/null +++ b/public/svgs/hetzner.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index 103f18316..46164840d 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -250,6 +250,18 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300"> {{ $checkbox['label'] }} + @if (isset($checkbox['default_warning'])) + + @endif @endforeach @if (!$disableTwoStepConfirmation) diff --git a/resources/views/livewire/server/create.blade.php b/resources/views/livewire/server/create.blade.php index 619a827e7..a941b7ee2 100644 --- a/resources/views/livewire/server/create.blade.php +++ b/resources/views/livewire/server/create.blade.php @@ -2,11 +2,17 @@
    @can('viewAny', App\Models\CloudProviderToken::class)
    -

    Add Server from Cloud Provider

    - + + +
    + + + + + Hetzner +
    +
    diff --git a/resources/views/livewire/server/delete.blade.php b/resources/views/livewire/server/delete.blade.php index c61775ee8..073849452 100644 --- a/resources/views/livewire/server/delete.blade.php +++ b/resources/views/livewire/server/delete.blade.php @@ -15,16 +15,15 @@
    @if ($server->definedResources()->count() > 0)
    You need to delete all resources before deleting this server.
    - - @else - @endif + + @endif
    diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php index 83de355e3..7ed5b1495 100644 --- a/resources/views/livewire/server/new/by-hetzner.blade.php +++ b/resources/views/livewire/server/new/by-hetzner.blade.php @@ -2,51 +2,128 @@ @if ($limit_reached) @else -
    - @if ($available_tokens->count() > 0) + @if ($current_step === 1) + + @if ($available_tokens->count() > 0) +
    + + + @foreach ($available_tokens as $token) + + @endforeach + +
    +
    OR
    + @endif +
    - - - @foreach ($available_tokens as $token) - - @endforeach - +
    -
    OR
    - @endif -
    - -
    - -
    - -
    - - @if ($save_token)
    - +
    - @endif - - Continue - -
    +
    + +
    + + + Continue + + + @elseif ($current_step === 2) + @if ($loading_data) +
    +
    +
    +

    Loading Hetzner data...

    +
    +
    + @else +
    +
    + +
    + +
    + + + @foreach ($locations as $location) + + @endforeach + +
    + +
    + + + @foreach ($this->availableServerTypes as $serverType) + + @endforeach + +
    + +
    + + + @foreach ($this->availableImages as $image) + + @endforeach + +
    + +
    + + + @foreach ($private_keys as $key) + + @endforeach + +
    + +
    + +
    + +
    + + Back + + + Create Server + +
    +
    + @endif + @endif @endif
    diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index a25e245e9..f1f1180e8 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -9,6 +9,16 @@

    General

    + @if ($server->hetzner_server_id) +
    + + + + + Hetzner +
    + @endif @if ($server->id === 0) - + From d837aa1473caa09ea809047bc8933285c23141e9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:22:10 +0200 Subject: [PATCH 0811/1717] fix(api-tokens): update settings link for API enablement message - Changed the link in the API tokens view to direct users to the advanced settings page instead of the general settings page, providing clearer guidance for enabling the API. --- resources/views/livewire/security/api-tokens.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index bf6bcf76c..b1f25a584 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -7,7 +7,7 @@

    API Tokens

    @if (!$isApiEnabled)
    API is disabled. If you want to use the API, please enable it in the Settings menu.
    + href="{{ route('settings.advanced') }}" class="underline dark:text-white">Settings menu.
    @else
    Tokens are created with the current team as scope.
    From 61e688affd5158ea4b7c138a5ef152c9ee6e4565 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:46:36 +0200 Subject: [PATCH 0812/1717] refactor(checkbox, utilities, global-search): enhance focus styles for better accessibility --- app/View/Components/Forms/Checkbox.php | 2 +- resources/css/utilities.css | 8 ++++---- resources/views/livewire/global-search.blade.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/View/Components/Forms/Checkbox.php b/app/View/Components/Forms/Checkbox.php index ece7f0e35..ea4f4ead2 100644 --- a/app/View/Components/Forms/Checkbox.php +++ b/app/View/Components/Forms/Checkbox.php @@ -22,7 +22,7 @@ public function __construct( public string|bool|null $checked = false, public string|bool $instantSave = false, public bool $disabled = false, - public string $defaultClass = 'dark:border-neutral-700 text-coolgray-400 focus:ring-warning dark:bg-coolgray-100 rounded-sm cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed', + public string $defaultClass = 'dark:border-neutral-700 text-coolgray-400 dark:bg-coolgray-100 rounded-sm cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100', public ?string $canGate = null, public mixed $canResource = null, public bool $autoDisable = true, diff --git a/resources/css/utilities.css b/resources/css/utilities.css index bedfb51bc..67b1e7f80 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -32,7 +32,7 @@ @utility apexcharts-tooltip-custom-title { } @utility input-sticky { - @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300; + @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100; } @utility input-sticky-active { @@ -41,7 +41,7 @@ @utility input-sticky-active { /* Focus */ @utility input-focus { - @apply focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300; + @apply focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100; } /* input, select before */ @@ -52,14 +52,14 @@ @utility input-select { /* Readonly */ @utility input { @apply dark:read-only:text-neutral-500 dark:read-only:ring-0 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200; - @apply input-focus; @apply input-select; + @apply focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100; } @utility select { @apply w-full; - @apply input-focus; @apply input-select; + @apply focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100; } @utility button { diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php index b7203c329..3bf29d392 100644 --- a/resources/views/livewire/global-search.blade.php +++ b/resources/views/livewire/global-search.blade.php @@ -266,7 +266,7 @@ class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen + class="w-full pl-12 pr-32 py-4 text-base bg-white dark:bg-coolgray-100 border-none rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100" />
    / or ⌘K to focus From c9e641854294d98c8c33cdd3d0f6edd31e50c9c3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:52:59 +0200 Subject: [PATCH 0813/1717] refactor(forms): simplify wire:dirty class bindings for input, select, and textarea components --- resources/views/components/forms/input.blade.php | 6 ++---- resources/views/components/forms/select.blade.php | 3 +-- resources/views/components/forms/textarea.blade.php | 7 +++---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index 858f5ac1c..f6c86f177 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -28,8 +28,7 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov merge(['class' => $defaultClass]) }} @required($required) @if ($id !== 'null') wire:model={{ $id }} @endif - wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' - wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" + wire:dirty.class="dark:ring-warning ring-warning" wire:loading.attr="disabled" type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" aria-placeholder="{{ $attributes->get('placeholder') }}" @@ -40,8 +39,7 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly) @if ($id !== 'null') wire:model={{ $id }} @endif - wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' - wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" + wire:dirty.class="dark:ring-warning ring-warning" wire:loading.attr="disabled" type="{{ $type }}" @disabled($disabled) min="{{ $attributes->get('min') }}" max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}" maxlength="{{ $attributes->get('maxlength') }}" diff --git a/resources/views/components/forms/select.blade.php b/resources/views/components/forms/select.blade.php index 508a85e0c..3c8eea25a 100644 --- a/resources/views/components/forms/select.blade.php +++ b/resources/views/components/forms/select.blade.php @@ -11,8 +11,7 @@ class="flex gap-1 items-center mb-1 text-sm font-medium {{ $disabled ? 'text-neu @endif diff --git a/resources/views/components/forms/textarea.blade.php b/resources/views/components/forms/textarea.blade.php index b4dec192a..a1c57e775 100644 --- a/resources/views/components/forms/textarea.blade.php +++ b/resources/views/components/forms/textarea.blade.php @@ -46,8 +46,7 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer merge(['class' => $defaultClassInput]) }} @required($required) @if ($id !== 'null') wire:model={{ $id }} @endif - wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' - wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" + wire:dirty.class="dark:ring-warning ring-warning" wire:loading.attr="disabled" type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" aria-placeholder="{{ $attributes->get('placeholder') }}"> @@ -56,7 +55,7 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer @if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}" @else wire:model={{ $value ?? $id }} - wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' wire:dirty.class="dark:focus:ring-warning dark:ring-warning" @endif + wire:dirty.class="dark:ring-warning ring-warning" @endif @disabled($disabled) @readonly($readonly) @required($required) id="{{ $id }}" name="{{ $name }}" name={{ $id }}> @@ -68,7 +67,7 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer @if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}" @else wire:model={{ $value ?? $id }} - wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' wire:dirty.class="dark:focus:ring-warning dark:ring-warning" @endif + wire:dirty.class="dark:ring-warning ring-warning" @endif @disabled($disabled) @readonly($readonly) @required($required) id="{{ $id }}" name="{{ $name }}" name={{ $id }}> @endif From 704ddf2968ef4dbaf5c836fca2c9ceb30964e925 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:53:57 +0200 Subject: [PATCH 0814/1717] improved hetzner features --- app/Actions/Server/DeleteServer.php | 12 +- .../Security/CloudProviderTokenForm.php | 99 ++++++++++++ app/Livewire/Security/CloudProviderTokens.php | 79 ++-------- .../Server/CloudProviderToken/Show.php | 144 ++++++++++++++++++ app/Livewire/Server/Create.php | 8 + app/Livewire/Server/New/ByHetzner.php | 99 +++++------- app/Models/CloudProviderToken.php | 10 ++ app/Models/Server.php | 6 + ...oud_provider_token_id_to_servers_table.php | 29 ++++ .../views/components/modal-input.blade.php | 13 +- .../views/components/server/sidebar.blade.php | 5 + .../cloud-provider-token-form.blade.php | 43 ++++++ .../security/cloud-provider-tokens.blade.php | 36 +---- .../cloud-provider-token/show.blade.php | 61 ++++++++ .../views/livewire/server/create.blade.php | 2 +- .../livewire/server/new/by-hetzner.blade.php | 65 ++++---- routes/web.php | 2 + 17 files changed, 514 insertions(+), 199 deletions(-) create mode 100644 app/Livewire/Security/CloudProviderTokenForm.php create mode 100644 app/Livewire/Server/CloudProviderToken/Show.php create mode 100644 database/migrations/2025_10_09_095905_add_cloud_provider_token_id_to_servers_table.php create mode 100644 resources/views/livewire/security/cloud-provider-token-form.blade.php create mode 100644 resources/views/livewire/server/cloud-provider-token/show.blade.php diff --git a/app/Actions/Server/DeleteServer.php b/app/Actions/Server/DeleteServer.php index db197a019..b7523714f 100644 --- a/app/Actions/Server/DeleteServer.php +++ b/app/Actions/Server/DeleteServer.php @@ -25,10 +25,14 @@ public function handle(Server $server, bool $deleteFromHetzner = false) private function deleteFromHetzner(Server $server): void { try { - // Get the cloud provider token for Hetzner - $token = CloudProviderToken::where('team_id', $server->team_id) - ->where('provider', 'hetzner') - ->first(); + // Use the server's associated token, or fallback to first available team token + $token = $server->cloudProviderToken; + + if (! $token) { + $token = CloudProviderToken::where('team_id', $server->team_id) + ->where('provider', 'hetzner') + ->first(); + } if (! $token) { ray('No Hetzner token found for team, skipping Hetzner deletion', [ diff --git a/app/Livewire/Security/CloudProviderTokenForm.php b/app/Livewire/Security/CloudProviderTokenForm.php new file mode 100644 index 000000000..7affb1531 --- /dev/null +++ b/app/Livewire/Security/CloudProviderTokenForm.php @@ -0,0 +1,99 @@ +authorize('create', CloudProviderToken::class); + } + + protected function rules(): array + { + return [ + 'provider' => 'required|string|in:hetzner,digitalocean', + 'token' => 'required|string', + 'name' => 'required|string|max:255', + ]; + } + + protected function messages(): array + { + return [ + 'provider.required' => 'Please select a cloud provider.', + 'provider.in' => 'Invalid cloud provider selected.', + 'token.required' => 'API token is required.', + 'name.required' => 'Token name is required.', + ]; + } + + private function validateToken(string $provider, string $token): bool + { + try { + if ($provider === 'hetzner') { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); + ray($response); + + return $response->successful(); + } + + // Add other providers here in the future + // if ($provider === 'digitalocean') { ... } + + return false; + } catch (\Throwable $e) { + return false; + } + } + + public function addToken() + { + $this->validate(); + + try { + // Validate the token with the provider's API + if (! $this->validateToken($this->provider, $this->token)) { + return $this->dispatch('error', 'Invalid API token. Please check your token and try again.'); + } + + $savedToken = CloudProviderToken::create([ + 'team_id' => currentTeam()->id, + 'provider' => $this->provider, + 'token' => $this->token, + 'name' => $this->name, + ]); + + $this->reset(['token', 'name']); + + // Dispatch event with token ID so parent components can react + $this->dispatch('tokenAdded', tokenId: $savedToken->id); + + $this->dispatch('success', 'Cloud provider token added successfully.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.security.cloud-provider-token-form'); + } +} diff --git a/app/Livewire/Security/CloudProviderTokens.php b/app/Livewire/Security/CloudProviderTokens.php index f5726e424..f05b3c0ca 100644 --- a/app/Livewire/Security/CloudProviderTokens.php +++ b/app/Livewire/Security/CloudProviderTokens.php @@ -4,7 +4,6 @@ use App\Models\CloudProviderToken; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Http; use Livewire\Component; class CloudProviderTokens extends Component @@ -13,34 +12,16 @@ class CloudProviderTokens extends Component public $tokens; - public string $provider = 'hetzner'; - - public string $token = ''; - - public string $name = ''; - public function mount() { $this->authorize('viewAny', CloudProviderToken::class); $this->loadTokens(); } - protected function rules(): array + public function getListeners() { return [ - 'provider' => 'required|string|in:hetzner,digitalocean', - 'token' => 'required|string', - 'name' => 'required|string|max:255', - ]; - } - - protected function messages(): array - { - return [ - 'provider.required' => 'Please select a cloud provider.', - 'provider.in' => 'Invalid cloud provider selected.', - 'token.required' => 'API token is required.', - 'name.required' => 'Token name is required.', + 'tokenAdded' => 'loadTokens', ]; } @@ -49,60 +30,20 @@ public function loadTokens() $this->tokens = CloudProviderToken::ownedByCurrentTeam()->get(); } - private function validateToken(string $provider, string $token): bool - { - try { - if ($provider === 'hetzner') { - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$token, - ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); - - return $response->successful(); - } - - // Add other providers here in the future - // if ($provider === 'digitalocean') { ... } - - return false; - } catch (\Throwable $e) { - return false; - } - } - - public function addNewToken() - { - $this->validate(); - - try { - $this->authorize('create', CloudProviderToken::class); - - // Validate the token with the provider's API - if (! $this->validateToken($this->provider, $this->token)) { - return $this->dispatch('error', 'Invalid API token. Please check your token and try again.'); - } - - CloudProviderToken::create([ - 'team_id' => currentTeam()->id, - 'provider' => $this->provider, - 'token' => $this->token, - 'name' => $this->name, - ]); - - $this->reset(['token', 'name']); - $this->loadTokens(); - - $this->dispatch('success', 'Cloud provider token added successfully.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - public function deleteToken(int $tokenId) { try { $token = CloudProviderToken::ownedByCurrentTeam()->findOrFail($tokenId); $this->authorize('delete', $token); + // Check if any servers are using this token + if ($token->hasServers()) { + $serverCount = $token->servers()->count(); + $this->dispatch('error', "Cannot delete this token. It is currently used by {$serverCount} server(s). Please reassign those servers to a different token first."); + + return; + } + $token->delete(); $this->loadTokens(); diff --git a/app/Livewire/Server/CloudProviderToken/Show.php b/app/Livewire/Server/CloudProviderToken/Show.php new file mode 100644 index 000000000..6b22fddc6 --- /dev/null +++ b/app/Livewire/Server/CloudProviderToken/Show.php @@ -0,0 +1,144 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->loadTokens(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function getListeners() + { + return [ + 'tokenAdded' => 'handleTokenAdded', + ]; + } + + public function loadTokens() + { + $this->cloudProviderTokens = CloudProviderToken::ownedByCurrentTeam() + ->where('provider', 'hetzner') + ->get(); + } + + public function handleTokenAdded($tokenId) + { + $this->loadTokens(); + } + + public function setCloudProviderToken($tokenId) + { + $ownedToken = CloudProviderToken::ownedByCurrentTeam()->find($tokenId); + if (is_null($ownedToken)) { + $this->dispatch('error', 'You are not allowed to use this token.'); + + return; + } + try { + $this->authorize('update', $this->server); + + // Validate the token works and can access this specific server + $validationResult = $this->validateTokenForServer($ownedToken); + if (! $validationResult['valid']) { + $this->dispatch('error', $validationResult['error']); + + return; + } + + $this->server->cloudProviderToken()->associate($ownedToken); + $this->server->save(); + $this->dispatch('success', 'Hetzner token updated successfully.'); + $this->dispatch('refreshServerShow'); + } catch (\Exception $e) { + $this->server->refresh(); + $this->dispatch('error', $e->getMessage()); + } + } + + private function validateTokenForServer(CloudProviderToken $token): array + { + try { + // First, validate the token itself + $response = \Illuminate\Support\Facades\Http::withHeaders([ + 'Authorization' => 'Bearer '.$token->token, + ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); + + if (! $response->successful()) { + return [ + 'valid' => false, + 'error' => 'This token is invalid or has insufficient permissions.', + ]; + } + + // Check if this token can access the specific Hetzner server + if ($this->server->hetzner_server_id) { + $serverResponse = \Illuminate\Support\Facades\Http::withHeaders([ + 'Authorization' => 'Bearer '.$token->token, + ])->timeout(10)->get("https://api.hetzner.cloud/v1/servers/{$this->server->hetzner_server_id}"); + + if (! $serverResponse->successful()) { + return [ + 'valid' => false, + 'error' => 'This token cannot access this server. It may belong to a different Hetzner project.', + ]; + } + } + + return ['valid' => true]; + } catch (\Throwable $e) { + return [ + 'valid' => false, + 'error' => 'Failed to validate token: '.$e->getMessage(), + ]; + } + } + + public function validateToken() + { + try { + $token = $this->server->cloudProviderToken; + if (! $token) { + $this->dispatch('error', 'No Hetzner token is associated with this server.'); + + return; + } + + $response = \Illuminate\Support\Facades\Http::withHeaders([ + 'Authorization' => 'Bearer '.$token->token, + ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); + + if ($response->successful()) { + $this->dispatch('success', 'Hetzner token is valid and working.'); + } else { + $this->dispatch('error', 'Hetzner token is invalid or has insufficient permissions.'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.cloud-provider-token.show'); + } +} diff --git a/app/Livewire/Server/Create.php b/app/Livewire/Server/Create.php index 2d4ba4430..cf77664fe 100644 --- a/app/Livewire/Server/Create.php +++ b/app/Livewire/Server/Create.php @@ -2,6 +2,7 @@ namespace App\Livewire\Server; +use App\Models\CloudProviderToken; use App\Models\PrivateKey; use App\Models\Team; use Livewire\Component; @@ -12,6 +13,8 @@ class Create extends Component public bool $limit_reached = false; + public bool $has_hetzner_tokens = false; + public function mount() { $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); @@ -21,6 +24,11 @@ public function mount() return; } $this->limit_reached = Team::serverLimitReached(); + + // Check if user has Hetzner tokens + $this->has_hetzner_tokens = CloudProviderToken::ownedByCurrentTeam() + ->where('provider', 'hetzner') + ->exists(); } public function render() diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php index b67411e17..d0a7582cd 100644 --- a/app/Livewire/Server/New/ByHetzner.php +++ b/app/Livewire/Server/New/ByHetzner.php @@ -34,12 +34,6 @@ class ByHetzner extends Component // Step 1: Token selection public ?int $selected_token_id = null; - public string $hetzner_token = ''; - - public bool $save_token = false; - - public ?string $token_name = null; - // Step 2: Server configuration public array $locations = []; @@ -64,31 +58,50 @@ class ByHetzner extends Component public function mount() { $this->authorize('viewAny', CloudProviderToken::class); - $this->available_tokens = CloudProviderToken::ownedByCurrentTeam() - ->where('provider', 'hetzner') - ->get(); + $this->loadTokens(); $this->server_name = generate_random_name(); if ($this->private_keys->count() > 0) { $this->private_key_id = $this->private_keys->first()->id; } } + public function getListeners() + { + return [ + 'tokenAdded' => 'handleTokenAdded', + 'modalClosed' => 'resetSelection', + ]; + } + + public function resetSelection() + { + $this->selected_token_id = null; + $this->current_step = 1; + } + + public function loadTokens() + { + $this->available_tokens = CloudProviderToken::ownedByCurrentTeam() + ->where('provider', 'hetzner') + ->get(); + } + + public function handleTokenAdded($tokenId) + { + // Refresh token list + $this->loadTokens(); + + // Auto-select the new token + $this->selected_token_id = $tokenId; + + // Automatically proceed to next step + $this->nextStep(); + } + protected function rules(): array { $rules = [ - 'selected_token_id' => 'nullable|integer', - 'hetzner_token' => 'required_without:selected_token_id|string', - 'save_token' => 'boolean', - 'token_name' => [ - 'nullable', - 'string', - 'max:255', - function ($attribute, $value, $fail) { - if ($this->save_token && ! empty($this->hetzner_token) && empty($value)) { - $fail('Please provide a name for the token.'); - } - }, - ], + 'selected_token_id' => 'required|integer|exists:cloud_provider_tokens,id', ]; if ($this->current_step === 2) { @@ -108,8 +121,8 @@ function ($attribute, $value, $fail) { protected function messages(): array { return [ - 'hetzner_token.required_without' => 'Please provide a Hetzner API token or select a saved token.', - 'token_name.required_if' => 'Please provide a name for the token.', + 'selected_token_id.required' => 'Please select a Hetzner token.', + 'selected_token_id.exists' => 'Selected token not found.', ]; } @@ -139,50 +152,21 @@ private function getHetznerToken(): string return $token ? $token->token : ''; } - return $this->hetzner_token; + return ''; } public function nextStep() { - // Validate step 1 + // Validate step 1 - just need a token selected $this->validate([ - 'selected_token_id' => 'nullable|integer', - 'hetzner_token' => 'required_without:selected_token_id|string', - 'save_token' => 'boolean', - 'token_name' => [ - 'nullable', - 'string', - 'max:255', - function ($attribute, $value, $fail) { - if ($this->save_token && ! empty($this->hetzner_token) && empty($value)) { - $fail('Please provide a name for the token.'); - } - }, - ], + 'selected_token_id' => 'required|integer|exists:cloud_provider_tokens,id', ]); try { $hetznerToken = $this->getHetznerToken(); if (! $hetznerToken) { - return $this->dispatch('error', 'Please provide a valid Hetzner API token.'); - } - - // Validate token if it's a new one - if (! $this->selected_token_id) { - if (! $this->validateHetznerToken($hetznerToken)) { - return $this->dispatch('error', 'Invalid Hetzner API token. Please check your token and try again.'); - } - - // Save token if requested - if ($this->save_token) { - CloudProviderToken::create([ - 'team_id' => currentTeam()->id, - 'provider' => 'hetzner', - 'token' => $this->hetzner_token, - 'name' => $this->token_name, - ]); - } + return $this->dispatch('error', 'Please select a valid Hetzner token.'); } // Load Hetzner data @@ -424,6 +408,7 @@ public function submit() 'port' => 22, 'team_id' => currentTeam()->id, 'private_key_id' => $this->private_key_id, + 'cloud_provider_token_id' => $this->selected_token_id, 'hetzner_server_id' => $hetznerServer['id'], ]); diff --git a/app/Models/CloudProviderToken.php b/app/Models/CloudProviderToken.php index 9ce216b25..607040269 100644 --- a/app/Models/CloudProviderToken.php +++ b/app/Models/CloudProviderToken.php @@ -17,6 +17,16 @@ public function team() return $this->belongsTo(Team::class); } + public function servers() + { + return $this->hasMany(Server::class); + } + + public function hasServers(): bool + { + return $this->servers()->exists(); + } + public static function ownedByCurrentTeam(array $select = ['*']) { $selectArray = collect($select)->concat(['id']); diff --git a/app/Models/Server.php b/app/Models/Server.php index e30b10043..e1a004755 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -161,6 +161,7 @@ protected static function booted() 'user', 'description', 'private_key_id', + 'cloud_provider_token_id', 'team_id', 'hetzner_server_id', ]; @@ -890,6 +891,11 @@ public function privateKey() return $this->belongsTo(PrivateKey::class); } + public function cloudProviderToken() + { + return $this->belongsTo(CloudProviderToken::class); + } + public function muxFilename() { return 'mux_'.$this->uuid; diff --git a/database/migrations/2025_10_09_095905_add_cloud_provider_token_id_to_servers_table.php b/database/migrations/2025_10_09_095905_add_cloud_provider_token_id_to_servers_table.php new file mode 100644 index 000000000..a25a4ce83 --- /dev/null +++ b/database/migrations/2025_10_09_095905_add_cloud_provider_token_id_to_servers_table.php @@ -0,0 +1,29 @@ +foreignId('cloud_provider_token_id')->nullable()->after('private_key_id')->constrained()->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropForeign(['cloud_provider_token_id']); + $table->dropColumn('cloud_provider_token_id'); + }); + } +}; diff --git a/resources/views/components/modal-input.blade.php b/resources/views/components/modal-input.blade.php index c15985d03..a9ad39871 100644 --- a/resources/views/components/modal-input.blade.php +++ b/resources/views/components/modal-input.blade.php @@ -8,8 +8,11 @@ 'content' => null, 'closeOutside' => true, 'minWidth' => '36rem', + 'isFullWidth' => false, ]) -
    @if ($content)
    @@ -17,13 +20,13 @@ class="relative w-auto h-auto" wire:ignore>
    @else @if ($disabled) - {{ $buttonTitle }} + $isFullWidth])>{{ $buttonTitle }} @elseif ($isErrorButton) - {{ $buttonTitle }} + $isFullWidth])>{{ $buttonTitle }} @elseif ($isHighlightedButton) - {{ $buttonTitle }} + $isFullWidth])>{{ $buttonTitle }} @else - {{ $buttonTitle }} + $isFullWidth])>{{ $buttonTitle }} @endif @endif
    -
    +
    \ No newline at end of file From ff889e658d94290bd68cc0c8c64ae7a63560fc20 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:47:26 +0200 Subject: [PATCH 0885/1717] refactor: improve cloud-init script management UI and cache control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add manual cache clearing command (search:clear) for testing - Integrate cloud-init scripts into global search navigation - Improve form UX by preventing field reset during edit operations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/GlobalSearch.php | 1 + resources/views/livewire/global-search.blade.php | 16 ---------------- .../security/cloud-init-script-form.blade.php | 4 ++-- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php index 680ac7701..5fcedd94d 100644 --- a/app/Livewire/GlobalSearch.php +++ b/app/Livewire/GlobalSearch.php @@ -79,6 +79,7 @@ public function mount() public function openSearchModal() { + sleep(4); $this->isModalOpen = true; $this->loadSearchableItems(); $this->loadCreatableItems(); diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php index 3df03ea0d..3bf21f8aa 100644 --- a/resources/views/livewire/global-search.blade.php +++ b/resources/views/livewire/global-search.blade.php @@ -290,22 +290,6 @@ class="pointer-events-auto px-2 py-1 text-xs font-medium text-neutral-500 dark:t
    - - {{--
    -
    -

    - ✓ Data loaded successfully! -

    -

    - searchable items available -

    -

    - Start typing to search... -

    -
    -
    --}} -
    diff --git a/resources/views/livewire/security/cloud-init-script-form.blade.php b/resources/views/livewire/security/cloud-init-script-form.blade.php index 545c49a7f..1632b48d3 100644 --- a/resources/views/livewire/security/cloud-init-script-form.blade.php +++ b/resources/views/livewire/security/cloud-init-script-form.blade.php @@ -1,4 +1,4 @@ - +
    - + \ No newline at end of file From a17b105a92420163200715b8a2f983aeb83f4ce9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:48:12 +0200 Subject: [PATCH 0886/1717] fix: hide 'No results found' message while data is loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent showing 'No results found' when user types during initial data loading phase. The message now only appears after data has fully loaded and the search still returns no results. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- resources/views/livewire/global-search.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php index 3bf21f8aa..06da31354 100644 --- a/resources/views/livewire/global-search.blade.php +++ b/resources/views/livewire/global-search.blade.php @@ -828,7 +828,7 @@ class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400 self-center"