Compare commits

..

20 commits

Author SHA1 Message Date
972f66317e feat(branding): replace text logos with PNG from marketing API
Some checks failed
Staging Build / build-push (aarch64, linux/aarch64, ubuntu-24.04-arm) (push) Has been cancelled
Staging Build / build-push (amd64, linux/amd64, ubuntu-24.04) (push) Has been cancelled
Staging Build / merge-manifest (push) Has been cancelled
Use mapledeploy.ca/api/logo/lockup for light/dark logo images
in navbar, mobile header, auth pages, and force-password-reset.
Remove unused mapledeploy-logo.svg.
2026-02-08 17:40:49 -05:00
06072915d1 feat(branding): apply MapleDeploy branding to Coolify fork
Replace all Coolify branding with MapleDeploy across 111 files:
- Logo, favicon, and color palette (Canadian red scale, stone greys)
- Fonts: Overlock for headings, Inter for body (self-hosted woff2/ttf)
- All ~70 page titles updated to "| MapleDeploy"
- Auth pages, navbar, footer, email templates, settings, boarding flow
- Remove Hetzner provider, Coolify Cloud upsells, sponsorship popups
- Disable telemetry (Sentry DSN null, undead.coolify.io ping disabled)
- Point auto-update to MapleDeploy Forgejo registry image
- Redirect help/support links to mapledeploy.ca/contact
- Add AGPL source code link to Forgejo repo in navbar
- OpenAPI docs rebranded to MapleDeploy
2026-02-08 17:34:51 -05:00
Andras Bacsai
a2fa98deb7
v4.0.0-beta.462 (#7967) 2026-01-16 12:04:54 +01:00
Andras Bacsai
b971440202 fix: update version numbers to 4.0.0-beta.462 and 4.0.0-beta.463 2026-01-16 12:03:31 +01:00
Andras Bacsai
e0f8ac4159
fix(perf): eliminate N+1 queries from InstanceSettings and Server lookups (#7966) 2026-01-16 11:58:05 +01:00
Andras Bacsai
95091e918f fix: optimize queries and caching for projects and environments 2026-01-16 11:51:26 +01:00
Andras Bacsai
04e71916e5
v4.0.0-beta.461 (#7849) 2026-01-16 08:23:45 +01:00
🏔️ Peak
51301fd12e
feat(notifications): add mattermost notifications (#7963) 2026-01-15 21:59:51 +01:00
🏔️ Peak
fbacf7076e
fix(api): application endpoint issues part 2 (#7948) 2026-01-15 14:06:07 +01:00
🏔️ Peak
650186b1ab
fix(preview): docker compose preview URLs (#7959) 2026-01-15 14:03:10 +01:00
peaklabs-dev
a05c198554
chore(api): update openapi files 2026-01-15 13:44:27 +01:00
peaklabs-dev
e53c71908f
fix(api): if domains field is empty clear the fqdn column
- providing an empty string for `domains` allows the ability to remove all URLs from the domains field
2026-01-15 13:12:49 +01:00
peaklabs-dev
161e0d2b05
chore(api): improve current request error message 2026-01-14 15:37:02 +01:00
peaklabs-dev
6ca04b5613
feat(api): add more allowed fields
- added dockerfile_location as it is needed for Dockerfile deployments to work properly
- added is_spa as it makes sense together with is_static
- added is_auto_deploy_enabled and is_force_https_enabled
2026-01-14 15:28:02 +01:00
peaklabs-dev
8a1d76cd99
fix(api): is_static and docker network missing
- GitHub App and Private Deploy Key where missing is_static and connect_to_docker_network
2026-01-14 15:27:54 +01:00
peaklabs-dev
fb56959418
fix(api): include docker_compose_domains in domain conflict check 2026-01-14 15:22:43 +01:00
peaklabs-dev
5f5c26d841
fix(api): check domain conflicts within the request 2026-01-14 15:22:30 +01:00
peaklabs-dev
754448d9d4
feat(api): improve docker_compose_domains
- add url conflict checking and force_domain_override support
- refactor docker_compose_domains URL validation function
2026-01-14 15:22:18 +01:00
peaklabs-dev
c66b6490e6
docs(api): improve domains API docs 2026-01-14 15:21:59 +01:00
peaklabs-dev
f4acf7ca10
refactor(api): application urls validation
- rename fqdn to urls as that is what it actually is
- improve URL validation to allow urls without a TLD
- improve error messages to make it clear that URLs are needed
- improve code by combining some actions
2026-01-14 15:21:44 +01:00
126 changed files with 1068 additions and 550 deletions

View file

@ -102,7 +102,8 @@ public function handle(Server $server, $fromUI = false): bool
foreach ($conflicts as $port => $conflict) {
if ($conflict) {
if ($fromUI) {
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br><br>Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a>");
// MapleDeploy branding: support links
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br><br>Support: <a target='_blank' class='dark:text-white hover:underline' href='https://mapledeploy.ca/contact'>https://mapledeploy.ca/contact</a>");
} else {
return false;
}

View file

@ -263,15 +263,11 @@ private function restoreCoolifyDbBackup()
}
}
// MapleDeploy branding: telemetry disabled — no phone-home signal
private function sendAliveSignal()
{
$id = config('app.id');
$version = config('constants.coolify.version');
try {
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
} catch (\Throwable $e) {
echo "Error in sending live signal: {$e->getMessage()}\n";
}
// Disabled for MapleDeploy: do not send telemetry to coolify.io
return;
}
private function replaceSlashInEnvironmentName()

View file

@ -153,11 +153,14 @@ public function applications(Request $request)
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
'name' => ['type' => 'string', 'description' => 'The application name.'],
'description' => ['type' => 'string', 'description' => 'The application description.'],
'domains' => ['type' => 'string', 'description' => 'The application domains.'],
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'],
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
@ -198,6 +201,7 @@ public function applications(Request $request)
// 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
@ -315,11 +319,14 @@ public function create_public_application(Request $request)
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
'name' => ['type' => 'string', 'description' => 'The application name.'],
'description' => ['type' => 'string', 'description' => 'The application description.'],
'domains' => ['type' => 'string', 'description' => 'The application domains.'],
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'],
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
@ -359,6 +366,7 @@ public function create_public_application(Request $request)
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
@ -476,11 +484,14 @@ public function create_private_gh_app_application(Request $request)
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
'name' => ['type' => 'string', 'description' => 'The application name.'],
'description' => ['type' => 'string', 'description' => 'The application description.'],
'domains' => ['type' => 'string', 'description' => 'The application domains.'],
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'],
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
@ -520,6 +531,7 @@ public function create_private_gh_app_application(Request $request)
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
@ -635,7 +647,7 @@ public function create_private_deploy_key_application(Request $request)
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
'name' => ['type' => 'string', 'description' => 'The application name.'],
'description' => ['type' => 'string', 'description' => 'The application description.'],
'domains' => ['type' => 'string', 'description' => 'The application domains.'],
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'],
@ -671,6 +683,7 @@ public function create_private_deploy_key_application(Request $request)
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
@ -771,7 +784,7 @@ public function create_dockerfile_application(Request $request)
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
'name' => ['type' => 'string', 'description' => 'The application name.'],
'description' => ['type' => 'string', 'description' => 'The application description.'],
'domains' => ['type' => 'string', 'description' => 'The application domains.'],
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'],
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
@ -804,6 +817,7 @@ public function create_dockerfile_application(Request $request)
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
@ -987,7 +1001,7 @@ private function create_application(Request $request, $type)
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled'];
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled'];
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
@ -1030,6 +1044,9 @@ private function create_application(Request $request, $type)
$githubAppUuid = $request->github_app_uuid;
$useBuildServer = $request->use_build_server;
$isStatic = $request->is_static;
$isSpa = $request->is_spa;
$isAutoDeployEnabled = $request->is_auto_deploy_enabled;
$isForceHttpsEnabled = $request->is_force_https_enabled;
$connectToDockerNetwork = $request->connect_to_docker_network;
$customNginxConfiguration = $request->custom_nginx_configuration;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true);
@ -1130,39 +1147,63 @@ private function create_application(Request $request, $type)
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
$dockerComposeDomains = collect($request->docker_compose_domains);
$domainErrors = [];
foreach ($dockerComposeDomains as $index => $item) {
// Collect all URLs from all docker_compose_domains items
$urls = $dockerComposeDomains->flatMap(function ($item) {
$domainValue = data_get($item, 'domain');
if (filled($domainValue)) {
$urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim();
str($urls)->explode(',')->each(function ($url) use (&$domainErrors) {
$url = trim($url);
if (empty($url)) {
return;
}
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$domainErrors[] = "Invalid URL: {$url}";
return;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
});
if (blank($domainValue)) {
return [];
}
return str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter();
});
$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$errors[] = "Invalid URL: {$url}";
return $url;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
return $url;
});
$duplicates = $urls->duplicates()->unique()->values();
if ($duplicates->isNotEmpty() && ! $request->boolean('force_domain_override')) {
$errors[] = 'The current request contains conflicting URLs: '.implode(', ', $duplicates->toArray()).' Use force_domain_override=true to proceed.';
}
if (! empty($domainErrors)) {
if (count($errors) > 0) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_domains' => $domainErrors,
],
'errors' => ['docker_compose_domains' => $errors],
], 422);
}
// Check for domain conflicts
if ($urls->isNotEmpty()) {
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId);
if (isset($result['error'])) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['docker_compose_domains' => $result['error']],
], 422);
}
if ($result['hasConflicts'] && ! $request->boolean('force_domain_override')) {
return response()->json([
'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.',
'conflicts' => $result['conflicts'],
'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.',
], 409);
}
}
$dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) {
$dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]);
});
@ -1187,6 +1228,18 @@ private function create_application(Request $request, $type)
$application->settings->is_static = $isStatic;
$application->settings->save();
}
if (isset($isSpa)) {
$application->settings->is_spa = $isSpa;
$application->settings->save();
}
if (isset($isAutoDeployEnabled)) {
$application->settings->is_auto_deploy_enabled = $isAutoDeployEnabled;
$application->settings->save();
}
if (isset($isForceHttpsEnabled)) {
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
$application->settings->save();
}
if (isset($connectToDockerNetwork)) {
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
$application->settings->save();
@ -1319,39 +1372,63 @@ private function create_application(Request $request, $type)
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
$dockerComposeDomains = collect($request->docker_compose_domains);
$domainErrors = [];
foreach ($dockerComposeDomains as $index => $item) {
// Collect all URLs from all docker_compose_domains items
$urls = $dockerComposeDomains->flatMap(function ($item) {
$domainValue = data_get($item, 'domain');
if (filled($domainValue)) {
$urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim();
str($urls)->explode(',')->each(function ($url) use (&$domainErrors) {
$url = trim($url);
if (empty($url)) {
return;
}
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$domainErrors[] = "Invalid URL: {$url}";
return;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
});
if (blank($domainValue)) {
return [];
}
return str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter();
});
$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$errors[] = "Invalid URL: {$url}";
return $url;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
return $url;
});
$duplicates = $urls->duplicates()->unique()->values();
if ($duplicates->isNotEmpty() && ! $request->boolean('force_domain_override')) {
$errors[] = 'The current request contains conflicting URLs: '.implode(', ', $duplicates->toArray()).' Use force_domain_override=true to proceed. ';
}
if (! empty($domainErrors)) {
if (count($errors) > 0) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_domains' => $domainErrors,
],
'errors' => ['docker_compose_domains' => $errors],
], 422);
}
// Check for domain conflicts
if ($urls->isNotEmpty()) {
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId);
if (isset($result['error'])) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['docker_compose_domains' => $result['error']],
], 422);
}
if ($result['hasConflicts'] && ! $request->boolean('force_domain_override')) {
return response()->json([
'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.',
'conflicts' => $result['conflicts'],
'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.',
], 409);
}
}
$dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) {
$dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]);
});
@ -1376,6 +1453,26 @@ private function create_application(Request $request, $type)
$application->fqdn = generateUrl(server: $server, random: $application->uuid);
$application->save();
}
if (isset($isStatic)) {
$application->settings->is_static = $isStatic;
$application->settings->save();
}
if (isset($isSpa)) {
$application->settings->is_spa = $isSpa;
$application->settings->save();
}
if (isset($isAutoDeployEnabled)) {
$application->settings->is_auto_deploy_enabled = $isAutoDeployEnabled;
$application->settings->save();
}
if (isset($isForceHttpsEnabled)) {
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
$application->settings->save();
}
if (isset($connectToDockerNetwork)) {
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
$application->settings->save();
}
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
@ -1476,39 +1573,63 @@ private function create_application(Request $request, $type)
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
$dockerComposeDomains = collect($request->docker_compose_domains);
$domainErrors = [];
foreach ($dockerComposeDomains as $index => $item) {
// Collect all URLs from all docker_compose_domains items
$urls = $dockerComposeDomains->flatMap(function ($item) {
$domainValue = data_get($item, 'domain');
if (filled($domainValue)) {
$urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim();
str($urls)->explode(',')->each(function ($url) use (&$domainErrors) {
$url = trim($url);
if (empty($url)) {
return;
}
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$domainErrors[] = "Invalid URL: {$url}";
return;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
});
if (blank($domainValue)) {
return [];
}
return str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter();
});
$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$errors[] = "Invalid URL: {$url}";
return $url;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
return $url;
});
$duplicates = $urls->duplicates()->unique()->values();
if ($duplicates->isNotEmpty() && ! $request->boolean('force_domain_override')) {
$errors[] = 'The current request contains conflicting URLs: '.implode(', ', $duplicates->toArray()).' Use force_domain_override=true to proceed.';
}
if (! empty($domainErrors)) {
if (count($errors) > 0) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_domains' => $domainErrors,
],
'errors' => ['docker_compose_domains' => $errors],
], 422);
}
// Check for domain conflicts
if ($urls->isNotEmpty()) {
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId);
if (isset($result['error'])) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['docker_compose_domains' => $result['error']],
], 422);
}
if ($result['hasConflicts'] && ! $request->boolean('force_domain_override')) {
return response()->json([
'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.',
'conflicts' => $result['conflicts'],
'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.',
], 409);
}
}
$dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) {
$dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]);
});
@ -1529,6 +1650,26 @@ private function create_application(Request $request, $type)
$application->fqdn = generateUrl(server: $server, random: $application->uuid);
$application->save();
}
if (isset($isStatic)) {
$application->settings->is_static = $isStatic;
$application->settings->save();
}
if (isset($isSpa)) {
$application->settings->is_spa = $isSpa;
$application->settings->save();
}
if (isset($isAutoDeployEnabled)) {
$application->settings->is_auto_deploy_enabled = $isAutoDeployEnabled;
$application->settings->save();
}
if (isset($isForceHttpsEnabled)) {
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
$application->settings->save();
}
if (isset($connectToDockerNetwork)) {
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
$application->settings->save();
}
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
@ -1632,6 +1773,14 @@ private function create_application(Request $request, $type)
$application->fqdn = generateUrl(server: $server, random: $application->uuid);
$application->save();
}
if (isset($isForceHttpsEnabled)) {
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
$application->settings->save();
}
if (isset($connectToDockerNetwork)) {
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
$application->settings->save();
}
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
@ -1734,6 +1883,14 @@ private function create_application(Request $request, $type)
$application->fqdn = generateUrl(server: $server, random: $application->uuid);
$application->save();
}
if (isset($isForceHttpsEnabled)) {
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
$application->settings->save();
}
if (isset($connectToDockerNetwork)) {
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
$application->settings->save();
}
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
@ -2150,11 +2307,14 @@ public function delete_by_uuid(Request $request)
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
'name' => ['type' => 'string', 'description' => 'The application name.'],
'description' => ['type' => 'string', 'description' => 'The application description.'],
'domains' => ['type' => 'string', 'description' => 'The application domains.'],
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'],
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
@ -2193,6 +2353,7 @@ public function delete_by_uuid(Request $request)
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
@ -2297,7 +2458,7 @@ public function update_by_uuid(Request $request)
$this->authorize('update', $application);
$server = $application->destination->server;
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings','custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
$validationRules = [
'name' => 'string|max:255',
@ -2411,18 +2572,30 @@ public function update_by_uuid(Request $request)
$requestHasDomains = $request->has('domains');
if ($requestHasDomains && $server->isProxyShouldRun()) {
$uuid = $request->uuid;
$fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
$urls = $request->domains;
$urls = str($urls)->replaceStart(',', '')->replaceEnd(',', '')->trim();
$errors = [];
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
$domain = trim($domain);
if (filter_var($domain, FILTER_VALIDATE_URL) === false || ! preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}/', $domain)) {
$errors[] = 'Invalid domain: '.$domain;
$urls = str($urls)->trim()->explode(',')->map(function ($url) use (&$errors) {
$url = trim($url);
// If "domains" is empty clear all URLs from the fqdn column
if (blank($url)) {
return null;
}
return $domain;
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$errors[] = 'Invalid URL: '.$url;
return $url;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
return str($url)->lower();
});
if (count($errors) > 0) {
return response()->json([
'message' => 'Validation failed.',
@ -2430,7 +2603,7 @@ public function update_by_uuid(Request $request)
], 422);
}
// Check for domain conflicts
$result = checkIfDomainIsAlreadyUsedViaAPI($fqdn, $teamId, $uuid);
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $uuid);
if (isset($result['error'])) {
return response()->json([
'message' => 'Validation failed.',
@ -2460,39 +2633,63 @@ public function update_by_uuid(Request $request)
}
$dockerComposeDomains = collect($request->docker_compose_domains);
$domainErrors = [];
foreach ($dockerComposeDomains as $item) {
// Collect all URLs from all docker_compose_domains items
$urls = $dockerComposeDomains->flatMap(function ($item) {
$domainValue = data_get($item, 'domain');
if (filled($domainValue)) {
$urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim();
str($urls)->explode(',')->each(function ($url) use (&$domainErrors) {
$url = trim($url);
if (empty($url)) {
return;
}
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$domainErrors[] = "Invalid URL: {$url}";
return;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
});
if (blank($domainValue)) {
return [];
}
return str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter();
});
$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$errors[] = "Invalid URL: {$url}";
return $url;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
return $url;
});
$duplicates = $urls->duplicates()->unique()->values();
if ($duplicates->isNotEmpty() && ! $request->boolean('force_domain_override')) {
$errors[] = 'The current request contains conflicting URLs: '.implode(', ', $duplicates->toArray()).' Use force_domain_override=true to proceed.';
}
if (! empty($domainErrors)) {
if (count($errors) > 0) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_domains' => $domainErrors,
],
'errors' => ['docker_compose_domains' => $errors],
], 422);
}
// Check for domain conflicts
if ($urls->isNotEmpty()) {
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $request->uuid);
if (isset($result['error'])) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['docker_compose_domains' => $result['error']],
], 422);
}
if ($result['hasConflicts'] && ! $request->boolean('force_domain_override')) {
return response()->json([
'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.',
'conflicts' => $result['conflicts'],
'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.',
], 409);
}
}
$yaml = Yaml::parse($application->docker_compose_raw);
$services = data_get($yaml, 'services', []);
$dockerComposeDomains->each(function ($domain) use ($services, $dockerComposeDomainsJson) {
@ -2505,6 +2702,9 @@ public function update_by_uuid(Request $request)
}
$instantDeploy = $request->instant_deploy;
$isStatic = $request->is_static;
$isSpa = $request->is_spa;
$isAutoDeployEnabled = $request->is_auto_deploy_enabled;
$isForceHttpsEnabled = $request->is_force_https_enabled;
$connectToDockerNetwork = $request->connect_to_docker_network;
$useBuildServer = $request->use_build_server;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled');
@ -2519,6 +2719,21 @@ public function update_by_uuid(Request $request)
$application->settings->save();
}
if (isset($isSpa)) {
$application->settings->is_spa = $isSpa;
$application->settings->save();
}
if (isset($isAutoDeployEnabled)) {
$application->settings->is_auto_deploy_enabled = $isAutoDeployEnabled;
$application->settings->save();
}
if (isset($isForceHttpsEnabled)) {
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
$application->settings->save();
}
if (isset($connectToDockerNetwork)) {
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
$application->settings->save();
@ -3626,17 +3841,29 @@ private function validateDataApplications(Request $request, Server $server)
}
if ($request->has('domains') && $server->isProxyShouldRun()) {
$uuid = $request->uuid;
$fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
$urls = $request->domains;
$urls = str($urls)->replaceEnd(',', '')->trim();
$urls = str($urls)->replaceStart(',', '')->trim();
$errors = [];
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
$domain = trim($domain);
if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
$errors[] = 'Invalid domain: '.$domain;
$urls = str($urls)->trim()->explode(',')->map(function ($url) use (&$errors) {
$url = trim($url);
// If "domains" is empty clear all URLs from the fqdn column
if (blank($url)) {
return null;
}
return str($domain)->lower();
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$errors[] = 'Invalid URL: '.$url;
return str($url)->lower();
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
return str($url)->lower();
});
if (count($errors) > 0) {
return response()->json([
@ -3645,7 +3872,7 @@ private function validateDataApplications(Request $request, Server $server)
], 422);
}
// Check for domain conflicts
$result = checkIfDomainIsAlreadyUsedViaAPI($fqdn, $teamId, $uuid);
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $uuid);
if (isset($result['error'])) {
return response()->json([
'message' => 'Validation failed.',

View file

@ -4,8 +4,9 @@
use OpenApi\Attributes as OA;
#[OA\Info(title: 'Coolify', version: '0.1')]
#[OA\Server(url: 'https://app.coolify.io/api/v1', description: 'Coolify Cloud API. Change the host to your own instance if you are self-hosting.')]
// MapleDeploy branding: API documentation
#[OA\Info(title: 'MapleDeploy', version: '0.1')]
#[OA\Server(url: '/api/v1', description: 'MapleDeploy API. Powered by Coolify.')]
#[OA\SecurityScheme(
type: 'http',
scheme: 'bearer',

View file

@ -47,7 +47,7 @@ public function handle()
match ($this->status) {
ProcessStatus::QUEUED => $this->body = "The preview deployment for **{$serviceName}** is queued. ⏳\n\n",
ProcessStatus::IN_PROGRESS => $this->body = "The preview deployment for **{$serviceName}** is in progress. 🟡\n\n",
ProcessStatus::FINISHED => $this->body = "The preview deployment for **{$serviceName}** is ready. 🟢\n\n".($this->preview->fqdn ? "[Open Preview]({$this->preview->fqdn}) | " : ''),
ProcessStatus::FINISHED => $this->body = "The preview deployment for **{$serviceName}** is ready. 🟢\n\n".$this->getPreviewLinks(),
ProcessStatus::ERROR => $this->body = "The preview deployment for **{$serviceName}** failed. 🔴\n\n",
ProcessStatus::KILLED => $this->body = "The preview deployment for **{$serviceName}** was killed. ⚫\n\n",
ProcessStatus::CANCELLED => $this->body = "The preview deployment for **{$serviceName}** was cancelled. 🚫\n\n",
@ -91,4 +91,27 @@ private function delete_comment()
{
githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/comments/{$this->preview->pull_request_issue_comment_id}", method: 'delete');
}
private function getPreviewLinks(): string
{
if ($this->application->build_pack === 'dockercompose') {
$dockerComposeDomains = json_decode($this->preview->docker_compose_domains, true) ?? [];
$links = [];
foreach ($dockerComposeDomains as $serviceName => $config) {
$domain = data_get($config, 'domain');
if (! empty($domain)) {
$firstDomain = str($domain)->explode(',')->first();
$firstDomain = trim($firstDomain);
if (! empty($firstDomain)) {
$links[] = "[Open {$serviceName}]({$firstDomain})";
}
}
}
return ! empty($links) ? implode(' | ', $links).' | ' : '';
}
return $this->preview->fqdn ? "[Open Preview]({$this->preview->fqdn}) | " : '';
}
}

View file

@ -22,6 +22,36 @@ public function __construct(
}
public function handle(): void
{
if ($this->isSlackWebhook()) {
$this->sendToSlack();
return;
}
/**
* This works with Mattermost and as a fallback also with Slack, the notifications just look slightly different and advanced formatting for slack is not supported with Mattermost.
*
* @see https://github.com/coollabsio/coolify/pull/6139#issuecomment-3756777708
*/
$this->sendToMattermost();
}
private function isSlackWebhook(): bool
{
$parsedUrl = parse_url($this->webhookUrl);
if ($parsedUrl === false) {
return false;
}
$scheme = $parsedUrl['scheme'] ?? '';
$host = $parsedUrl['host'] ?? '';
return $scheme === 'https' && $host === 'hooks.slack.com';
}
private function sendToSlack(): void
{
Http::post($this->webhookUrl, [
'text' => $this->message->title,
@ -57,4 +87,24 @@ public function handle(): void
],
]);
}
/**
* @todo v5 refactor: Extract this into a separate SendMessageToMattermostJob.php triggered via the "mattermost" notification channel type.
*/
private function sendToMattermost(): void
{
$username = config('app.name');
Http::post($this->webhookUrl, [
'username' => $username,
'attachments' => [
[
'title' => $this->message->title,
'color' => $this->message->color,
'text' => $this->message->description,
'footer' => $username,
],
],
]);
}
}

View file

@ -35,14 +35,12 @@ public function submit()
$mail->subject("[HELP]: {$this->subject}");
$type = set_transanctional_email_settings($settings);
// Sending feedback through Cloud API
// MapleDeploy branding: feedback sent to MapleDeploy support
if (blank($type)) {
$url = 'https://app.coolify.io/api/feedback';
Http::post($url, [
'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`',
]);
// No external API — log locally when SMTP not configured
\Illuminate\Support\Facades\Log::info('Feedback from '.auth()->user()?->email.': '.$this->subject.' — '.$this->description);
} else {
send_user_an_email($mail, auth()->user()?->email, 'feedback@coollabs.io');
send_user_an_email($mail, auth()->user()?->email, 'support@mapledeploy.ca');
}
$this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.');
$this->reset('description', 'subject');

View file

@ -33,6 +33,10 @@ class Index extends Component
public Collection $services;
public Collection $allProjects;
public Collection $allEnvironments;
public array $parameters;
public function mount()
@ -50,6 +54,33 @@ public function mount()
->firstOrFail();
$this->project = $project;
// Load projects and environments for breadcrumb navigation (avoids inline queries in view)
$this->allProjects = Project::ownedByCurrentTeamCached();
$this->allEnvironments = $project->environments()
->with([
'applications.additional_servers',
'applications.destination.server',
'services',
'services.destination.server',
'postgresqls',
'postgresqls.destination.server',
'redis',
'redis.destination.server',
'mongodbs',
'mongodbs.destination.server',
'mysqls',
'mysqls.destination.server',
'mariadbs',
'mariadbs.destination.server',
'keydbs',
'keydbs.destination.server',
'dragonflies',
'dragonflies.destination.server',
'clickhouses',
'clickhouses.destination.server',
])->get();
$this->environment = $environment->loadCount([
'applications',
'redis',
@ -71,11 +102,13 @@ public function mount()
'destination.server.settings',
'settings',
])->get()->sortBy('name');
$this->applications = $this->applications->map(function ($application) {
$projectUuid = $this->project->uuid;
$environmentUuid = $this->environment->uuid;
$this->applications = $this->applications->map(function ($application) use ($projectUuid, $environmentUuid) {
$application->hrefLink = route('project.application.configuration', [
'project_uuid' => data_get($application, 'environment.project.uuid'),
'environment_uuid' => data_get($application, 'environment.uuid'),
'application_uuid' => data_get($application, 'uuid'),
'project_uuid' => $projectUuid,
'environment_uuid' => $environmentUuid,
'application_uuid' => $application->uuid,
]);
return $application;
@ -98,11 +131,11 @@ public function mount()
'tags',
'destination.server.settings',
])->get()->sortBy('name');
$this->{$property} = $this->{$property}->map(function ($db) {
$this->{$property} = $this->{$property}->map(function ($db) use ($projectUuid, $environmentUuid) {
$db->hrefLink = route('project.database.configuration', [
'project_uuid' => $this->project->uuid,
'project_uuid' => $projectUuid,
'database_uuid' => $db->uuid,
'environment_uuid' => data_get($this->environment, 'uuid'),
'environment_uuid' => $environmentUuid,
]);
return $db;
@ -114,11 +147,11 @@ public function mount()
'tags',
'destination.server.settings',
])->get()->sortBy('name');
$this->services = $this->services->map(function ($service) {
$this->services = $this->services->map(function ($service) use ($projectUuid, $environmentUuid) {
$service->hrefLink = route('project.service.configuration', [
'project_uuid' => data_get($service, 'environment.project.uuid'),
'environment_uuid' => data_get($service, 'environment.uuid'),
'service_uuid' => data_get($service, 'uuid'),
'project_uuid' => $projectUuid,
'environment_uuid' => $environmentUuid,
'service_uuid' => $service->uuid,
]);
return $service;

View file

@ -4,6 +4,7 @@
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Once;
use Spatie\Url\Url;
class InstanceSettings extends Model
@ -35,6 +36,9 @@ class InstanceSettings extends Model
protected static function booted(): void
{
static::updated(function ($settings) {
// Clear once() cache so subsequent calls get fresh data
Once::flush();
// Clear trusted hosts cache when FQDN changes
if ($settings->wasChanged('fqdn')) {
\Cache::forget('instance_settings_fqdn_host');
@ -82,7 +86,7 @@ public function autoUpdateFrequency(): Attribute
public static function get()
{
return InstanceSettings::findOrFail(0);
return once(fn () => InstanceSettings::findOrFail(0));
}
// public function getRecipients($notification)

View file

@ -108,6 +108,12 @@ class Server extends BaseModel
public static $batch_counter = 0;
/**
* Identity map cache for request-scoped Server lookups.
* Prevents N+1 queries when the same Server is accessed multiple times.
*/
private static ?array $identityMapCache = null;
protected $appends = ['is_coolify_host'];
protected static function booted()
@ -186,6 +192,40 @@ protected static function booted()
$server->settings()->delete();
$server->sslCertificates()->delete();
});
static::updated(function () {
static::flushIdentityMap();
});
}
/**
* Find a Server by ID using the identity map cache.
* This prevents N+1 queries when the same Server is accessed multiple times.
*/
public static function findCached($id): ?static
{
if ($id === null) {
return null;
}
if (static::$identityMapCache === null) {
static::$identityMapCache = [];
}
if (! isset(static::$identityMapCache[$id])) {
static::$identityMapCache[$id] = static::query()->find($id);
}
return static::$identityMapCache[$id];
}
/**
* Flush the identity map cache.
* Called automatically on update, and should be called in tests.
*/
public static function flushIdentityMap(): void
{
static::$identityMapCache = null;
}
protected $casts = [

View file

@ -73,6 +73,28 @@ public function server()
return $this->belongsTo(Server::class);
}
/**
* Get the server attribute using identity map caching.
* This intercepts lazy-loading to use cached Server lookups.
*/
public function getServerAttribute(): ?Server
{
// Use eager loaded data if available
if ($this->relationLoaded('server')) {
return $this->getRelation('server');
}
// Use identity map for lazy loading
$server = Server::findCached($this->server_id);
// Cache in relation for future access on this instance
if ($server) {
$this->setRelation('server', $server);
}
return $server;
}
public function services()
{
return $this->morphMany(Service::class, 'destination');

View file

@ -56,6 +56,28 @@ public function server()
return $this->belongsTo(Server::class);
}
/**
* Get the server attribute using identity map caching.
* This intercepts lazy-loading to use cached Server lookups.
*/
public function getServerAttribute(): ?Server
{
// Use eager loaded data if available
if ($this->relationLoaded('server')) {
return $this->getRelation('server');
}
// Use identity map for lazy loading
$server = Server::findCached($this->server_id);
// Cache in relation for future access on this instance
if ($server) {
$this->setRelation('server', $server);
}
return $server;
}
public function services()
{
return $this->morphMany(Service::class, 'destination');

View file

@ -86,8 +86,11 @@ function sharedDataApplications()
'git_branch' => 'string',
'build_pack' => Rule::enum(BuildPackTypes::class),
'is_static' => 'boolean',
'is_spa' => 'boolean',
'is_auto_deploy_enabled' => 'boolean',
'is_force_https_enabled' => 'boolean',
'static_image' => Rule::enum(StaticImageTypes::class),
'domains' => 'string',
'domains' => 'string|nullable',
'redirect' => Rule::enum(RedirectTypes::class),
'git_commit_sha' => 'string',
'docker_registry_image_name' => 'string|nullable',
@ -129,6 +132,7 @@ function sharedDataApplications()
'manual_webhook_secret_gitlab' => 'string|nullable',
'manual_webhook_secret_bitbucket' => 'string|nullable',
'manual_webhook_secret_gitea' => 'string|nullable',
'dockerfile_location' => 'string|nullable',
'docker_compose_location' => 'string',
'docker_compose' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
@ -177,6 +181,10 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('private_key_uuid');
$request->offsetUnset('use_build_server');
$request->offsetUnset('is_static');
$request->offsetUnset('is_spa');
$request->offsetUnset('is_auto_deploy_enabled');
$request->offsetUnset('is_force_https_enabled');
$request->offsetUnset('connect_to_docker_network');
$request->offsetUnset('force_domain_override');
$request->offsetUnset('autogenerate_domain');
$request->offsetUnset('is_container_label_escape_enabled');

View file

@ -158,8 +158,7 @@ function checkIfDomainIsAlreadyUsedViaAPI(Collection|array $domains, ?string $te
return str($domain);
});
// Check applications within the same team
$applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid', 'name', 'id']);
$applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid', 'name', 'id', 'docker_compose_domains', 'build_pack']);
$serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->with('service:id,name')->get(['fqdn', 'uuid', 'id', 'service_id']);
if ($uuid) {
@ -168,23 +167,51 @@ function checkIfDomainIsAlreadyUsedViaAPI(Collection|array $domains, ?string $te
}
foreach ($applications as $app) {
if (is_null($app->fqdn)) {
continue;
}
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
if (! is_null($app->fqdn)) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
$conflicts[] = [
'domain' => $naked_domain,
'resource_name' => $app->name,
'resource_uuid' => $app->uuid,
'resource_type' => 'application',
'message' => "Domain $naked_domain is already in use by application '{$app->name}'",
];
}
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
$conflicts[] = [
'domain' => $naked_domain,
'resource_name' => $app->name,
'resource_uuid' => $app->uuid,
'resource_type' => 'application',
'message' => "Domain $naked_domain is already in use by application '{$app->name}'",
];
}
if ($app->build_pack === 'dockercompose' && ! empty($app->docker_compose_domains)) {
$dockerComposeDomains = json_decode($app->docker_compose_domains, true);
if (is_array($dockerComposeDomains)) {
foreach ($dockerComposeDomains as $serviceName => $domainConfig) {
$domainValue = data_get($domainConfig, 'domain');
if (empty($domainValue)) {
continue;
}
$list_of_domains = collect(explode(',', $domainValue))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
$conflicts[] = [
'domain' => $naked_domain,
'resource_name' => $app->name,
'resource_uuid' => $app->uuid,
'resource_type' => 'application',
'service_name' => $serviceName,
'message' => "Domain $naked_domain is already in use by application '{$app->name}' (service: {$serviceName})",
];
}
}
}
}
}
}

View file

@ -17,7 +17,7 @@
|
*/
'name' => env('APP_NAME', 'Coolify'),
'name' => env('APP_NAME', 'MapleDeploy'), // MapleDeploy branding
/*
|--------------------------------------------------------------------------

View file

@ -1,16 +1,17 @@
<?php
return [
// MapleDeploy branding: registry pointed to Forgejo, auto-update disabled by default
'coolify' => [
'version' => '4.0.0-beta.461',
'version' => '4.0.0-beta.462',
'helper_version' => '1.0.12',
'realtime_version' => '1.0.10',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'autoupdate' => env('AUTOUPDATE', false),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
'registry_url' => env('REGISTRY_URL', 'ghcr.io'),
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'),
'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'),
'registry_url' => env('REGISTRY_URL', 'forgejo.mapledeploy.ca'),
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'),
'realtime_image' => env('REALTIME_IMAGE', 'ghcr.io/coollabsio/coolify-realtime'),
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
'cdn_url' => env('CDN_URL', 'https://cdn.coollabs.io'),
'versions_url' => env('VERSIONS_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/versions.json'),
@ -19,8 +20,8 @@
],
'urls' => [
'docs' => 'https://coolify.io/docs',
'contact' => 'https://coolify.io/docs/contact',
'docs' => 'https://mapledeploy.ca/docs',
'contact' => 'https://mapledeploy.ca/contact',
],
'services' => [
@ -85,12 +86,13 @@
'verification_code_expiry_minutes' => 10,
],
// MapleDeploy branding: telemetry disabled
'sentry' => [
'sentry_dsn' => env('SENTRY_DSN'),
'sentry_dsn' => null,
],
'webhooks' => [
'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'),
'feedback_discord_webhook' => null,
'dev_webhook' => env('SERVEO_URL'),
],

View file

@ -135,7 +135,7 @@
},
"domains": {
"type": "string",
"description": "The application domains."
"description": "The application URLs in a comma-separated list."
},
"git_commit_sha": {
"type": "string",
@ -153,6 +153,18 @@
"type": "boolean",
"description": "The flag to indicate if the application is static."
},
"is_spa": {
"type": "boolean",
"description": "The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true."
},
"is_auto_deploy_enabled": {
"type": "boolean",
"description": "The flag to indicate if auto-deploy is enabled on git push. Defaults to true."
},
"is_force_https_enabled": {
"type": "boolean",
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
},
"static_image": {
"type": "string",
"enum": [
@ -322,6 +334,10 @@
"type": "string",
"description": "The Dockerfile content."
},
"dockerfile_location": {
"type": "string",
"description": "The Dockerfile location in the repository."
},
"docker_compose_location": {
"type": "string",
"description": "The Docker Compose location."
@ -564,7 +580,7 @@
},
"domains": {
"type": "string",
"description": "The application domains."
"description": "The application URLs in a comma-separated list."
},
"git_commit_sha": {
"type": "string",
@ -582,6 +598,18 @@
"type": "boolean",
"description": "The flag to indicate if the application is static."
},
"is_spa": {
"type": "boolean",
"description": "The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true."
},
"is_auto_deploy_enabled": {
"type": "boolean",
"description": "The flag to indicate if auto-deploy is enabled on git push. Defaults to true."
},
"is_force_https_enabled": {
"type": "boolean",
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
},
"static_image": {
"type": "string",
"enum": [
@ -751,6 +779,10 @@
"type": "string",
"description": "The Dockerfile content."
},
"dockerfile_location": {
"type": "string",
"description": "The Dockerfile location in the repository"
},
"docker_compose_location": {
"type": "string",
"description": "The Docker Compose location."
@ -993,7 +1025,7 @@
},
"domains": {
"type": "string",
"description": "The application domains."
"description": "The application URLs in a comma-separated list."
},
"git_commit_sha": {
"type": "string",
@ -1011,6 +1043,18 @@
"type": "boolean",
"description": "The flag to indicate if the application is static."
},
"is_spa": {
"type": "boolean",
"description": "The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true."
},
"is_auto_deploy_enabled": {
"type": "boolean",
"description": "The flag to indicate if auto-deploy is enabled on git push. Defaults to true."
},
"is_force_https_enabled": {
"type": "boolean",
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
},
"static_image": {
"type": "string",
"enum": [
@ -1180,6 +1224,10 @@
"type": "string",
"description": "The Dockerfile content."
},
"dockerfile_location": {
"type": "string",
"description": "The Dockerfile location in the repository."
},
"docker_compose_location": {
"type": "string",
"description": "The Docker Compose location."
@ -1410,7 +1458,7 @@
},
"domains": {
"type": "string",
"description": "The application domains."
"description": "The application URLs in a comma-separated list."
},
"docker_registry_image_name": {
"type": "string",
@ -1562,6 +1610,10 @@
"type": "boolean",
"description": "The flag to indicate if the application should be deployed instantly."
},
"is_force_https_enabled": {
"type": "boolean",
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
},
"use_build_server": {
"type": "boolean",
"nullable": true,
@ -1754,7 +1806,7 @@
},
"domains": {
"type": "string",
"description": "The application domains."
"description": "The application URLs in a comma-separated list."
},
"ports_mappings": {
"type": "string",
@ -1894,6 +1946,10 @@
"type": "boolean",
"description": "The flag to indicate if the application should be deployed instantly."
},
"is_force_https_enabled": {
"type": "boolean",
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
},
"use_build_server": {
"type": "boolean",
"nullable": true,
@ -2402,7 +2458,7 @@
},
"domains": {
"type": "string",
"description": "The application domains."
"description": "The application URLs in a comma-separated list."
},
"git_commit_sha": {
"type": "string",
@ -2420,6 +2476,18 @@
"type": "boolean",
"description": "The flag to indicate if the application is static."
},
"is_spa": {
"type": "boolean",
"description": "The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true."
},
"is_auto_deploy_enabled": {
"type": "boolean",
"description": "The flag to indicate if auto-deploy is enabled on git push. Defaults to true."
},
"is_force_https_enabled": {
"type": "boolean",
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
},
"install_command": {
"type": "string",
"description": "The install command."
@ -2582,6 +2650,10 @@
"type": "string",
"description": "The Dockerfile content."
},
"dockerfile_location": {
"type": "string",
"description": "The Dockerfile location in the repository."
},
"docker_compose_location": {
"type": "string",
"description": "The Docker Compose location."

View file

@ -97,7 +97,7 @@ paths:
description: 'The application description.'
domains:
type: string
description: 'The application domains.'
description: 'The application URLs in a comma-separated list.'
git_commit_sha:
type: string
description: 'The git commit SHA.'
@ -110,6 +110,15 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
is_spa:
type: boolean
description: 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'
is_auto_deploy_enabled:
type: boolean
description: 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'
is_force_https_enabled:
type: boolean
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
static_image:
type: string
enum: ['nginx:alpine']
@ -234,6 +243,9 @@ paths:
dockerfile:
type: string
description: 'The Dockerfile content.'
dockerfile_location:
type: string
description: 'The Dockerfile location in the repository.'
docker_compose_location:
type: string
description: 'The Docker Compose location.'
@ -369,7 +381,7 @@ paths:
description: 'The application description.'
domains:
type: string
description: 'The application domains.'
description: 'The application URLs in a comma-separated list.'
git_commit_sha:
type: string
description: 'The git commit SHA.'
@ -382,6 +394,15 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
is_spa:
type: boolean
description: 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'
is_auto_deploy_enabled:
type: boolean
description: 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'
is_force_https_enabled:
type: boolean
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
static_image:
type: string
enum: ['nginx:alpine']
@ -506,6 +527,9 @@ paths:
dockerfile:
type: string
description: 'The Dockerfile content.'
dockerfile_location:
type: string
description: 'The Dockerfile location in the repository'
docker_compose_location:
type: string
description: 'The Docker Compose location.'
@ -641,7 +665,7 @@ paths:
description: 'The application description.'
domains:
type: string
description: 'The application domains.'
description: 'The application URLs in a comma-separated list.'
git_commit_sha:
type: string
description: 'The git commit SHA.'
@ -654,6 +678,15 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
is_spa:
type: boolean
description: 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'
is_auto_deploy_enabled:
type: boolean
description: 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'
is_force_https_enabled:
type: boolean
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
static_image:
type: string
enum: ['nginx:alpine']
@ -778,6 +811,9 @@ paths:
dockerfile:
type: string
description: 'The Dockerfile content.'
dockerfile_location:
type: string
description: 'The Dockerfile location in the repository.'
docker_compose_location:
type: string
description: 'The Docker Compose location.'
@ -903,7 +939,7 @@ paths:
description: 'The application description.'
domains:
type: string
description: 'The application domains.'
description: 'The application URLs in a comma-separated list.'
docker_registry_image_name:
type: string
description: 'The docker registry image name.'
@ -1015,6 +1051,9 @@ paths:
instant_deploy:
type: boolean
description: 'The flag to indicate if the application should be deployed instantly.'
is_force_https_enabled:
type: boolean
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
use_build_server:
type: boolean
nullable: true
@ -1124,7 +1163,7 @@ paths:
description: 'The application description.'
domains:
type: string
description: 'The application domains.'
description: 'The application URLs in a comma-separated list.'
ports_mappings:
type: string
description: 'The ports mappings.'
@ -1227,6 +1266,9 @@ paths:
instant_deploy:
type: boolean
description: 'The flag to indicate if the application should be deployed instantly.'
is_force_https_enabled:
type: boolean
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
use_build_server:
type: boolean
nullable: true
@ -1524,7 +1566,7 @@ paths:
description: 'The application description.'
domains:
type: string
description: 'The application domains.'
description: 'The application URLs in a comma-separated list.'
git_commit_sha:
type: string
description: 'The git commit SHA.'
@ -1537,6 +1579,15 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
is_spa:
type: boolean
description: 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'
is_auto_deploy_enabled:
type: boolean
description: 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'
is_force_https_enabled:
type: boolean
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
install_command:
type: string
description: 'The install command.'
@ -1657,6 +1708,9 @@ paths:
dockerfile:
type: string
description: 'The Dockerfile content.'
dockerfile_location:
type: string
description: 'The Dockerfile location in the repository.'
docker_compose_location:
type: string
description: 'The Docker Compose location.'

View file

@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.461"
"version": "4.0.0-beta.462"
},
"nightly": {
"version": "4.0.0-beta.462"
"version": "4.0.0-beta.463"
},
"helper": {
"version": "1.0.12"

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -13,10 +13,12 @@
@custom-variant dark (&:where(.dark, .dark *));
/* MapleDeploy branding: Canadian red accent, stone greys */
@theme {
--font-sans: Inter, sans-serif;
--font-display: 'Overlock', sans-serif;
--color-base: #101010;
--color-base: #0c0a09;
--color-warning: #fcd452;
--color-warning-50: #fefce8;
--color-warning-100: #fef9c3;
@ -30,16 +32,16 @@ @theme {
--color-warning-900: #713f12;
--color-success: #22C55E;
--color-error: #dc2626;
--color-coollabs-50: #f5f0ff;
--color-coollabs: #6b16ed;
--color-coollabs-100: #7317ff;
--color-coollabs-200: #5a12c7;
--color-coollabs-300: #4a0fa3;
--color-coolgray-100: #181818;
--color-coolgray-200: #202020;
--color-coolgray-300: #242424;
--color-coolgray-400: #282828;
--color-coolgray-500: #323232;
--color-coollabs-50: #fef3f2;
--color-coollabs: #d52b1e;
--color-coollabs-100: #f34d40;
--color-coollabs-200: #bc2519;
--color-coollabs-300: #9c2118;
--color-coolgray-100: #1c1917;
--color-coolgray-200: #292524;
--color-coolgray-300: #44403c;
--color-coolgray-400: #57534e;
--color-coolgray-500: #78716c;
}
/*

View file

@ -70,3 +70,12 @@ @font-face {
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2') format('woff2');
}
/* MapleDeploy branding: Overlock for headings */
@font-face {
font-display: swap;
font-family: 'Overlock';
font-style: normal;
font-weight: 900;
src: url('../fonts/overlock-v19-latin-900.woff2') format('woff2'),
url('../fonts/overlock-v19-latin-900.ttf') format('truetype');
}

Binary file not shown.

Binary file not shown.

View file

@ -3,9 +3,10 @@
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="w-full max-w-md space-y-8">
<div class="text-center space-y-2">
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Coolify
</h1>
<div class="flex justify-center">
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
</div>
<p class="text-lg dark:text-neutral-400">
Confirm Your Password
</p>

View file

@ -3,9 +3,10 @@
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="w-full max-w-md space-y-8">
<div class="text-center space-y-2">
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Coolify
</h1>
<div class="flex justify-center">
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
</div>
<p class="text-lg dark:text-neutral-400">
{{ __('auth.forgot_password_heading') }}
</p>

View file

@ -3,9 +3,10 @@
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="w-full max-w-md space-y-8">
<div class="text-center space-y-2">
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Coolify
</h1>
<div class="flex justify-center">
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
</div>
</div>
<div class="space-y-6">

View file

@ -13,9 +13,10 @@ function getOldOrLocal($key, $localValue)
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="w-full max-w-md space-y-8">
<div class="text-center space-y-2">
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Coolify
</h1>
<div class="flex justify-center">
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
</div>
<p class="text-lg dark:text-neutral-400">
Create your account
</p>

View file

@ -3,9 +3,10 @@
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="w-full max-w-md space-y-8">
<div class="text-center space-y-2">
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Coolify
</h1>
<div class="flex justify-center">
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
</div>
<p class="text-lg dark:text-neutral-400">
{{ __('auth.reset_password') }}
</p>

View file

@ -47,9 +47,10 @@
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="w-full max-w-md space-y-8">
<div class="text-center space-y-2">
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Coolify
</h1>
<div class="flex justify-center">
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
</div>
<p class="text-lg dark:text-neutral-400">
Two-Factor Authentication
</p>

View file

@ -1,6 +1,7 @@
{{ Illuminate\Mail\Markdown::parse('---') }}
Thank you,<br>
{{ config('app.name') ?? 'Coolify' }}
{{ config('app.name') ?? 'MapleDeploy' }}
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://coolify.io/docs/contact)') }}
{{-- MapleDeploy branding: support link --}}
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://mapledeploy.ca/contact)') }}

View file

@ -78,9 +78,13 @@
}
}">
<div class="flex lg:pt-6 pt-4 pb-4 pl-2">
{{-- MapleDeploy branding --}}
<div class="flex flex-col w-full">
<a href="/" {{ wireNavigate() }} class="text-2xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
<x-version />
<a href="/" {{ wireNavigate() }} class="hover:opacity-80 transition-opacity">
<img src="https://mapledeploy.ca/api/logo/lockup?height=40" alt="MapleDeploy" class="h-6 dark:hidden" />
<img src="https://mapledeploy.ca/api/logo/lockup?height=40&dark=true" alt="MapleDeploy" class="hidden h-6 dark:block" />
</a>
<span class="text-xs opacity-75 dark:text-neutral-400">Powered by Coolify</span>
</div>
<div>
<!-- Search button that triggers global search modal -->
@ -291,20 +295,7 @@ class="{{ request()->is('team*') ? 'menu-item-active menu-item' : 'menu-item' }}
<span class="menu-item-label">Teams</span>
</a>
</li>
@if (isCloud() && auth()->user()->isAdmin())
<li>
<a title="Subscription" {{ wireNavigate() }}
class="{{ request()->is('subscription*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('subscription.show') }}">
<svg class="menu-item-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2"
d="M3 8a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm0 2h18M7 15h.01M11 15h2" />
</svg>
<span class="menu-item-label">Subscription</span>
</a>
</li>
@endif
{{-- MapleDeploy branding: Cloud subscription menu removed --}}
@if (isInstanceAdmin())
<li>
@ -324,20 +315,7 @@ class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item
</li>
@endif
@if (isCloud() || isDev())
@if (isInstanceAdmin() || session('impersonating'))
<li>
<a title="Admin" class="menu-item" href="/admin" {{ wireNavigate() }}>
<svg class="text-pink-500 menu-item-icon" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M177.62 159.6a52 52 0 0 1-34 34a12.2 12.2 0 0 1-3.6.55a12 12 0 0 1-3.6-23.45a28 28 0 0 0 18.32-18.32a12 12 0 0 1 22.9 7.2ZM220 144a92 92 0 0 1-184 0c0-28.81 11.27-58.18 33.48-87.28a12 12 0 0 1 17.9-1.33l19.69 19.11L127 19.89a12 12 0 0 1 18.94-5.12C168.2 33.25 220 82.85 220 144m-24 0c0-41.71-30.61-78.39-52.52-99.29l-20.21 55.4a12 12 0 0 1-19.63 4.5L80.71 82.36C67 103.38 60 124.06 60 144a68 68 0 0 0 136 0" />
</svg>
<span class="menu-item-label">Admin</span>
</a>
</li>
@endif
@endif
{{-- MapleDeploy branding: Cloud admin menu removed --}}
<div class="flex-1"></div>
@if (isInstanceAdmin() && !isCloud())
@persist('upgrade')
@ -357,24 +335,19 @@ class="{{ request()->is('onboarding*') ? 'menu-item-active menu-item' : 'menu-it
Onboarding
</a>
</li> --}}
{{-- MapleDeploy branding: AGPL source code link (license requirement) --}}
<li>
<a title="Sponsor us" class="menu-item" href="https://coolify.io/sponsorships"
<a title="Source code (AGPL-3.0)" class="menu-item" href="https://forgejo.mapledeploy.ca/mapledeploy/coolify"
target="_blank">
<svg class="text-pink-500 menu-item-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<path d="M19.5 12.572L12 20l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 1 1 7.5 6.572" />
<path
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
</g>
<svg class="menu-item-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2"
d="M16 18l6-6-6-6M8 6l-6 6 6 6" />
</svg>
<span class="menu-item-label">Sponsor us</span>
<span class="menu-item-label">Source code</span>
</a>
</li>
@endif
@if (!isSubscribed() && isCloud() && auth()->user()->teams()->get()->count() > 1)
<livewire:navbar-delete-team />
@endif
<li>
<x-modal-input title="How can we help?">
<x-slot:content>

View file

@ -35,19 +35,21 @@ class="font-bold dark:text-warning">{{ config('constants.limits.trial_period') }
</div>
</div>
<div class="p-4 rounded-sm bg-coolgray-400">
{{-- MapleDeploy branding: link to Forgejo source repo --}}
<h2 id="tier-hobby" class="flex items-start gap-4 text-4xl font-bold tracking-tight">Unlimited Trial
<x-forms.button><a class="font-bold dark:text-white hover:no-underline"
href="https://github.com/coollabsio/coolify">Get Started</a></x-forms.button>
href="https://forgejo.mapledeploy.ca/mapledeploy/coolify">Get Started</a></x-forms.button>
</h2>
<p class="mt-4 text-sm leading-6">Start self-hosting <span class="dark:text-warning">without limits</span>
with
our
OSS version. Same features as the paid version, but you have to manage by yourself.</p>
the
open source version. Same features as the paid version, but you have to manage by yourself.</p>
</div>
<div class="flow-root mt-12">
{{-- MapleDeploy branding: link to mapledeploy.ca --}}
<div class="pb-10 text-xl text-center">For the detailed list of features, please visit our landing page: <a
class="font-bold underline dark:text-white" href="https://coolify.io">coolify.io</a></div>
class="font-bold underline dark:text-white" href="https://mapledeploy.ca">mapledeploy.ca</a></div>
<div
class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-3 lg:divide-x lg:divide-y-0 xl:-mx-4">

View file

@ -2,12 +2,28 @@
'lastDeploymentInfo' => null,
'lastDeploymentLink' => null,
'resource' => null,
'projects' => null,
'environments' => null,
])
@php
$projects = auth()->user()->currentTeam()->projects()->get();
$environments = $resource->environment->project
use App\Models\Project;
// Use passed props if available, otherwise query (backwards compatible)
$projects = $projects ?? Project::ownedByCurrentTeamCached();
$environments = $environments ?? $resource->environment->project
->environments()
->with(['applications', 'services'])
->with([
'applications',
'services',
'postgresqls',
'redis',
'mongodbs',
'mysqls',
'mariadbs',
'keydbs',
'dragonflies',
'clickhouses',
])
->get();
$currentProjectUuid = data_get($resource, 'environment.project.uuid');
$currentEnvironmentUuid = data_get($resource, 'environment.uuid');
@ -74,6 +90,16 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
@foreach ($environments as $environment)
@php
// Use pre-loaded relations instead of databases() method to avoid N+1 queries
$envDatabases = collect()
->merge($environment->postgresqls ?? collect())
->merge($environment->redis ?? collect())
->merge($environment->mongodbs ?? collect())
->merge($environment->mysqls ?? collect())
->merge($environment->mariadbs ?? collect())
->merge($environment->keydbs ?? collect())
->merge($environment->dragonflies ?? collect())
->merge($environment->clickhouses ?? collect());
$envResources = collect()
->merge(
$environment->applications->map(
@ -81,9 +107,7 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
),
)
->merge(
$environment
->databases()
->map(fn($db) => ['type' => 'database', 'resource' => $db]),
$envDatabases->map(fn($db) => ['type' => 'database', 'resource' => $db]),
)
->merge(
$environment->services->map(
@ -173,7 +197,9 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
]),
};
$isCurrentResource = $res->uuid === $currentResourceUuid;
$resHasMultipleServers = $resType === 'application' && method_exists($res, 'additional_servers') && $res->additional_servers()->count() > 0;
// Use loaded relation count if available, otherwise check additional_servers_count attribute
$resHasMultipleServers = $resType === 'application' && method_exists($res, 'additional_servers') &&
($res->relationLoaded('additional_servers') ? $res->additional_servers->count() > 0 : ($res->additional_servers_count ?? 0) > 0);
$resServerName = $resHasMultipleServers ? null : data_get($res, 'destination.server.name');
@endphp
<div @mouseenter="openRes('{{ $environment->uuid }}-{{ $res->uuid }}'); resPositions['{{ $environment->uuid }}-{{ $res->uuid }}'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)"
@ -405,7 +431,9 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
$isApplication = $resourceType === 'App\Models\Application';
$isService = $resourceType === 'App\Models\Service';
$isDatabase = str_contains($resourceType, 'Database') || str_contains($resourceType, 'Standalone');
$hasMultipleServers = $isApplication && method_exists($resource, 'additional_servers') && $resource->additional_servers()->count() > 0;
// Use loaded relation count if available, otherwise check additional_servers_count attribute
$hasMultipleServers = $isApplication && method_exists($resource, 'additional_servers') &&
($resource->relationLoaded('additional_servers') ? $resource->additional_servers->count() > 0 : ($resource->additional_servers_count ?? 0) > 0);
$serverName = $hasMultipleServers ? null : data_get($resource, 'destination.server.name');
$routeParams = [
'project_uuid' => $currentProjectUuid,

View file

@ -1,6 +1,6 @@
<div class="pb-5">
<h1>Settings</h1>
<div class="subtitle">Instance wide settings for Coolify.</div>
<div class="subtitle">Instance wide settings for MapleDeploy.</div>
<div class="navbar-main">
<nav class="flex items-center gap-6 min-h-10 whitespace-nowrap">
<a class="{{ request()->routeIs('settings.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}

View file

@ -1,4 +1,4 @@
<a {{ $attributes->merge(['class' => 'text-xs cursor-pointer opacity-90 hover:opacity-100 dark:hover:text-white hover:text-black']) }}
href="https://github.com/coollabsio/coolify/releases/tag/v{{ config('constants.coolify.version') }}" target="_blank">
{{-- MapleDeploy branding: show version without linking to upstream releases --}}
<span {{ $attributes->merge(['class' => 'text-xs opacity-90 dark:text-neutral-500']) }}>
v{{ config('constants.coolify.version') }}
</a>
</span>

View file

@ -1,7 +1,7 @@
<x-emails.layout>
We would like to inform you that a {{ config('constants.limits.trial_period') }} days of trial has been added to all subscription plans.
You can try out Coolify, without payment information for free. If you like it, you can upgrade to a paid plan at any time.
You can try out MapleDeploy, without payment information for free. If you like it, you can upgrade to a paid plan at any time.
[Click here](https://app.coolify.io/subscription/new) to start your trial.
</x-emails.layout>

View file

@ -2,6 +2,7 @@
A resource ({{ $containerName }}) has been restarted automatically on {{ $serverName }}, because it was stopped unexpectedly.
@if ($containerName === 'coolify-proxy')
{{-- Note: Coolify Proxy is the technical component name, not a branding reference --}}
Coolify Proxy should run on your server as you have FQDNs set up in one of your resources.
If you don't want to use Coolify Proxy, please remove FQDN from your resources or set Proxy type to Custom(None).

View file

@ -6,7 +6,7 @@
{{ $errorMessage }}
</pre>
The server has been removed from Coolify, but may still exist in your Hetzner Cloud account.
The server has been removed from MapleDeploy, but may still exist in your Hetzner Cloud account.
Please check your Hetzner Cloud console and manually delete the server if needed to avoid ongoing charges.

View file

@ -9,5 +9,5 @@
---
You can manage your server and view more details in your [Coolify Dashboard]({{ $server_url }}).
You can manage your server and view more details in your [MapleDeploy dashboard]({{ $server_url }}).
</x-emails.layout>

View file

@ -41,7 +41,7 @@
1. Review the available updates
2. Plan maintenance window if critical packages are involved
3. Apply updates through the Coolify dashboard
3. Apply updates through the MapleDeploy dashboard
4. Monitor services after updates are applied
@else
Your server is up to date! No packages require updating at this time.
@ -49,5 +49,5 @@
---
You can manage server patches in your [Coolify Dashboard]({{ $server_url }}).
You can manage server patches in your [MapleDeploy dashboard]({{ $server_url }}).
</x-emails.layout>

View file

@ -1,5 +1,6 @@
{{-- MapleDeploy branding: Coolify Cloud references removed --}}
<x-emails.layout>
Your last invoice has failed to be paid for Coolify Cloud.
Your last invoice has failed to be paid for MapleDeploy.
Please update payment details [here]({{ $stripeCustomerPortal }}).
Please update your payment details [here]({{ $stripeCustomerPortal }}).
</x-emails.layout>

View file

@ -1,5 +1,6 @@
{{-- MapleDeploy branding: Coolify Cloud references removed --}}
<x-emails.layout>
Your trial ended. All automations and integrations are disabled for all of your servers.
Your trial has ended. All automations and integrations are disabled for your servers.
Please update payment details [here]({{ $stripeCustomerPortal }}) or in [Coolify Cloud](https://app.coolify.io) to continue using our services.
Please update your payment details [here]({{ $stripeCustomerPortal }}) to continue using MapleDeploy.
</x-emails.layout>

View file

@ -49,8 +49,10 @@
<div
class="sticky top-0 z-40 flex items-center justify-between px-4 py-4 gap-x-6 sm:px-6 lg:hidden bg-white/95 dark:bg-base/95 backdrop-blur-sm border-b border-neutral-300/50 dark:border-coolgray-200/50">
<div class="flex items-center gap-3 flex-shrink-0">
<a href="/"
class="text-xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
<a href="/" class="hover:opacity-80 transition-opacity">
<img src="https://mapledeploy.ca/api/logo/lockup?height=32" alt="MapleDeploy" class="h-5 dark:hidden" />
<img src="https://mapledeploy.ca/api/logo/lockup?height=32&dark=true" alt="MapleDeploy" class="hidden h-5 dark:block" />
</a>
<livewire:switch-team />
</div>
<button type="button" class="-m-2.5 p-2.5 dark:text-warning" x-on:click="open = !open">

View file

@ -16,19 +16,17 @@
<meta name="robots" content="noindex">
<meta name="theme-color" content="#ffffff" id="theme-color-meta" />
<meta name="color-scheme" content="dark light" />
<meta name="Description" content="Coolify: An open-source & self-hostable Heroku / Netlify / Vercel alternative" />
{{-- MapleDeploy branding --}}
<meta name="Description" content="MapleDeploy: Managed Coolify hosting on Canadian infrastructure" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@coolifyio" />
<meta name="twitter:title" content="Coolify" />
<meta name="twitter:description" content="An open-source & self-hostable Heroku / Netlify / Vercel alternative." />
<meta name="twitter:image" content="https://cdn.coollabs.io/assets/coolify/og-image.png" />
<meta name="twitter:title" content="MapleDeploy" />
<meta name="twitter:description" content="Managed Coolify hosting on Canadian infrastructure." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://coolify.io" />
<meta property="og:title" content="Coolify" />
<meta property="og:description" content="An open-source & self-hostable Heroku / Netlify / Vercel alternative." />
<meta property="og:site_name" content="Coolify" />
<meta property="og:image" content="https://cdn.coollabs.io/assets/coolify/og-image.png" />
<meta property="og:url" content="https://mapledeploy.ca" />
<meta property="og:title" content="MapleDeploy" />
<meta property="og:description" content="Managed Coolify hosting on Canadian infrastructure." />
<meta property="og:site_name" content="MapleDeploy" />
@use('App\Models\InstanceSettings')
@php
@ -43,12 +41,9 @@
}
}
@endphp
<title>{{ $name }}{{ $title ?? 'Coolify' }}</title>
@env('local')
<link rel="icon" href="{{ asset('coolify-logo-dev-transparent.png') }}" type="image/png" />
@else
<link rel="icon" href="{{ asset('coolify-logo.svg') }}" type="image/svg+xml" />
@endenv
<title>{{ $name }}{{ $title ?? 'MapleDeploy' }}</title> {{-- MapleDeploy branding --}}
{{-- MapleDeploy branding: single favicon for all environments --}}
<link rel="icon" href="{{ asset('mapledeploy-favicon.ico') }}" type="image/x-icon" />
<meta name="csrf-token" content="{{ csrf_token() }}">
@vite(['resources/js/app.js', 'resources/css/app.css'])
<script>
@ -62,10 +57,7 @@
display: none !important;
}
</style>
@if (config('app.name') == 'Coolify Cloud')
<script defer data-domain="app.coolify.io" src="https://analytics.coollabs.io/js/plausible.js"></script>
<script src="https://js.sentry-cdn.com/0f8593910512b5cdd48c6da78d4093be.min.js" crossorigin="anonymous"></script>
@endif
{{-- MapleDeploy branding: Coolify Cloud analytics removed --}}
@auth
<script type="text/javascript" src="{{ URL::asset('js/echo.js') }}"></script>
<script type="text/javascript" src="{{ URL::asset('js/pusher.js') }}"></script>

View file

@ -1,13 +1,13 @@
@php use App\Enums\ProxyTypes; @endphp
<x-slot:title>
Onboarding | Coolify
Onboarding | MapleDeploy
</x-slot>
<section class="w-full">
<div class="flex flex-col items-center w-full space-y-8">
@if ($currentState === 'welcome')
<div class="w-full max-w-2xl text-center space-y-8">
<div class="space-y-4">
<h1 class="text-4xl font-bold lg:text-6xl">Welcome to Coolify</h1>
<h1 class="text-4xl font-bold lg:text-6xl">Welcome to MapleDeploy</h1>
<p class="text-lg lg:text-xl dark:text-neutral-400">
Connect your first server and start deploying in minutes
</p>
@ -81,17 +81,17 @@ class="text-sm dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning
<x-boarding-progress :currentStep="0" />
<x-boarding-step title="Platform Overview">
<x-slot:question>
Coolify automates deployment and infrastructure management on your own servers. Deploy applications
MapleDeploy automates deployment and infrastructure management on your own servers. Deploy applications
from Git, manage databases, and monitor everything—without vendor lock-in.
</x-slot:question>
<x-slot:explanation>
<p>
<x-highlighted text="Automation:" /> Coolify handles server configuration, Docker management,
<x-highlighted text="Automation:" /> MapleDeploy handles server configuration, Docker management,
and
deployments automatically.
</p>
<p>
<x-highlighted text="Self-hosted:" /> All data and configurations live on your infrastructure.
<x-highlighted text="Your infrastructure:" /> All data and configurations live on your servers.
Works offline except for external integrations.
</p>
<p>
@ -132,7 +132,7 @@ class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-neutral-100 dark:b
<div>
<h3 class="text-xl font-bold mb-2">This Machine</h3>
<p class="text-sm dark:text-neutral-400">
Deploy on the server running Coolify. Best for testing and single-server setups.
Deploy on the server running MapleDeploy. Best for testing and single-server setups.
</p>
</div>
</div>
@ -163,38 +163,7 @@ class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:b
</div>
</div>
</button>
@can('viewAny', App\Models\CloudProviderToken::class)
@if ($currentState === 'select-server-type')
<x-modal-input title="Connect a Hetzner Server" isFullWidth>
<x-slot:content>
<div
class="group relative box-without-bg cursor-pointer hover:border-coollabs transition-all duration-200 p-6 h-full min-h-[210px]">
<div class="flex flex-col gap-4 text-left">
<div class="flex items-center justify-between">
<svg class="size-10" viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" fill="#D50C2D" rx="8" />
<path d="M40 40 H60 V90 H140 V40 H160 V160 H140 V110 H60 V160 H40 Z"
fill="white" />
</svg>
<span
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:bg-warning/20 text-coollabs dark:text-warning rounded">
Recommended
</span>
</div>
<div>
<h3 class="text-xl font-bold mb-2">Hetzner Cloud</h3>
<p class="text-sm dark:text-neutral-400">
Deploy servers directly from your Hetzner Cloud account.
</p>
</div>
</div>
</div>
</x-slot:content>
<livewire:server.new.by-hetzner :limit_reached="false" :from_onboarding="true" />
</x-modal-input>
@endif
@endcan
{{-- MapleDeploy branding: Hetzner Cloud provider removed --}}
</div>
@if (!$serverReachable)
@ -211,6 +180,7 @@ class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:b
wire:model="remoteServerUser" :value="$remoteServerUser" />
<p class="text-xs mt-1">
Non-root user is experimental:
{{-- MapleDeploy branding: link to upstream Coolify docs for technical reference --}}
<a class="font-bold underline" target="_blank"
href="https://coolify.io/docs/knowledge-base/server/non-root-user">docs</a>
</p>
@ -229,6 +199,7 @@ class="bg-red-200 dark:bg-red-900 px-1 rounded-sm">~/.ssh/authorized_keys</code>
</div>
<p class="mb-4">
{{-- MapleDeploy branding: link to upstream Coolify docs for technical reference --}}
For more help, check this <a target="_blank" class="underline font-semibold"
href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a>.
</p>
@ -248,12 +219,12 @@ class="bg-red-200 dark:bg-red-900 px-1 rounded-sm">~/.ssh/authorized_keys</code>
called resources). All CPU-intensive operations run on the target server.
</p>
<p>
<x-highlighted text="Localhost:" /> The machine running Coolify. Not recommended for production
<x-highlighted text="Localhost:" /> The machine running MapleDeploy. Not recommended for production
workloads due to resource contention.
</p>
<p>
<x-highlighted text="Remote Server:" /> Any SSH-accessible server—cloud providers (AWS, Hetzner,
DigitalOcean), bare metal, or self-hosted infrastructure.
<x-highlighted text="Remote Server:" /> Any SSH-accessible server—cloud providers,
bare metal, or self-hosted infrastructure.
</p>
</x-slot:explanation>
</x-boarding-step>
@ -336,7 +307,7 @@ class="text-xs bg-coolgray-300 dark:bg-coolgray-400 px-1 py-0.5 rounded">~/.ssh/
file.
</p>
<p>
<x-highlighted text="Key Generation:" /> Coolify generates ED25519 keys by default for optimal
<x-highlighted text="Key Generation:" /> MapleDeploy generates ED25519 keys by default for optimal
security and performance.
</p>
</x-slot:explanation>
@ -386,7 +357,7 @@ class="text-xs bg-coolgray-300 dark:bg-coolgray-400 px-1 py-0.5 rounded">~/.ssh/
</x-slot:actions>
<x-slot:explanation>
<p>
<x-highlighted text="Key Storage:" /> Private keys are encrypted at rest in Coolify's database.
<x-highlighted text="Key Storage:" /> Private keys are encrypted at rest in the database.
</p>
<p>
<x-highlighted text="Public Key Distribution:" /> Deploy the public key to
@ -443,6 +414,7 @@ class="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4 rounded-lg border border-neutra
wire:model="remoteServerUser" />
<p class="mt-1 text-xs dark:text-white text-black">
Non-root user support is experimental.
{{-- MapleDeploy branding: link to upstream Coolify docs for technical reference --}}
<a class="font-bold underline hover:text-coollabs" target="_blank"
href="https://coolify.io/docs/knowledge-base/server/non-root-user">Learn
more</a>
@ -473,7 +445,7 @@ class="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4 rounded-lg border border-neutra
<x-boarding-progress :currentStep="2" />
<x-boarding-step title="Server Validation">
<x-slot:question>
Coolify will automatically install Docker {{ $minDockerVersion }}+ if not present.
MapleDeploy will automatically install Docker {{ $minDockerVersion }}+ if not present.
</x-slot:question>
<x-slot:actions>
<div class="w-full space-y-6">
@ -567,7 +539,7 @@ class="p-6 bg-neutral-50 dark:bg-coolgray-200 rounded-lg border border-neutral-2
</x-slot:actions>
<x-slot:explanation>
<p>
<x-highlighted text="Automated Setup:" /> Coolify installs Docker Engine, Docker Compose, and
<x-highlighted text="Automated Setup:" /> MapleDeploy installs Docker Engine, Docker Compose, and
configures system requirements automatically.
</p>
<p>

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Dashboard | Coolify
Dashboard | MapleDeploy
</x-slot>
@if (session('error'))
<span x-data x-init="$wire.emit('error', '{{ session('error') }}')" />

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Destinations | Coolify
Destinations | MapleDeploy
</x-slot>
<div class="flex items-center gap-2">
<h1>Destinations</h1>

View file

@ -1,8 +1,9 @@
<section class="bg-gray-50 dark:bg-base">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a class="flex items-center mb-6 text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Coolify
</a>
<div class="flex items-center justify-center mb-6">
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
</div>
<div class="w-full bg-white shadow-sm md:mt-0 sm:max-w-md xl:p-0 dark:bg-base ">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<form class="flex flex-col gap-2" wire:submit='submit'>

View file

@ -1,5 +1,6 @@
<div class="flex flex-col w-full gap-2">
<div>Your feedback helps us to improve Coolify. Thank you! 💜</div>
{{-- MapleDeploy branding --}}
<div>Your feedback helps us improve MapleDeploy. Thank you!</div>
<form wire:submit="submit" class="flex flex-col gap-4 pt-4">
<x-forms.input minlength="3" required id="subject" label="Subject" placeholder="Help with..."></x-forms.input>
<x-forms.textarea minlength="10" maxlength="1000" required rows="10" id="description" label="Description"

View file

@ -24,8 +24,8 @@
if (checkNumber > 5) {
this.popups.realtime = true;
console.error(
'Coolify could not connect to its real-time service. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/knowledge-base/cloudflare/tunnels/overview) or get help on Discord (https://coollabs.io/discord).)'
);
'MapleDeploy could not connect to its real-time service. This will cause unusual problems on the UI if not fixed! Please contact support at support@mapledeploy.ca.'
); // MapleDeploy branding
}
}
@ -70,13 +70,11 @@
<x-slot:title>
<span class="font-bold text-left text-red-500">WARNING: </span> Cannot connect to real-time service
</x-slot:title>
{{-- MapleDeploy branding: support links updated --}}
<x-slot:description>
<div>This will cause unusual problems on the
UI! <br><br>
Please ensure that you have opened the
<a class="underline" href='https://coolify.io/docs/knowledge-base/server/firewall'
target='_blank'>required ports</a> or get
help on <a class="underline" href='https://coollabs.io/discord' target='_blank'>Discord</a>.
Please contact <a class="underline" href='mailto:support@mapledeploy.ca'>MapleDeploy support</a> for help.
</div>
</x-slot:description>
<x-slot:button-text @click="disableRealtime()">
@ -86,52 +84,7 @@
@endif
</span>
@endauth
@if (instanceSettings()->is_sponsorship_popup_enabled && !isCloud())
<span x-show="popups.sponsorship">
<x-popup>
<x-slot:customActions>
<div
class="flex md:flex-row flex-col max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 border-neutral-200 dark:bg-coolgray-100 lg:p-8 lg:pb-4 sm:rounded-sm gap-2">
<div class="md:block hidden">
<img src="{{ asset('heart.png') }}" class="w-20 h-20">
</div>
<div class="flex flex-col gap-2 lg:px-10 px-1">
<div class="lg:text-xl text-md dark:text-white font-bold">Love Coolify? Support our work.
</div>
<div class="lg:text-sm text-xs dark:text-white">
We are already profitable thanks to <span class="font-bold text-pink-500">YOU</span>
but...<br />We
would
like to
make
more cool features.
</div>
<div class="lg:text-sm text-xs dark:text-white pt-2 ">
For this we need your help to support our work financially.
</div>
</div>
<div class="flex flex-col gap-2 text-center md:mx-auto lg:py-0 pt-2">
<x-forms.button isHighlighted class="md:w-36 w-full"><a target="_blank"
href="https://github.com/sponsors/coollabsio"
class="font-bold dark:text-white">GitHub
Sponsors</a></x-forms.button>
<x-forms.button isHighlighted class="md:w-36 w-full"><a target="_blank"
href="https://opencollective.com/coollabsio/donate?interval=month&amount=10&name=&legalName=&email="
class="font-bold dark:text-white">Open
Collective</a></x-forms.button>
<x-forms.button isHighlighted class="md:w-36 w-full"><a
href="https://donate.stripe.com/8x2bJ104ifmB9kB45u38402" target="_blank"
class="font-bold dark:text-white">Stripe</a></x-forms.button>
<div class="pt-4 dark:text-white hover:underline cursor-pointer lg:text-base text-xs"
@click="bannerVisible=false;disableSponsorship()">
Maybe next time
</div>
</div>
</div>
</x-slot:customActions>
</x-popup>
</span>
@endif
{{-- MapleDeploy branding: Coolify sponsorship popup removed --}}
@if (currentTeam()->subscriptionPastOverDue())
<x-banner :closable=false>
<div><span class="font-bold text-red-500">WARNING:</span> Your subscription is in over-due. If your

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Notifications | Coolify
Notifications | MapleDeploy
</x-slot>
<x-notification.navbar />
<form wire:submit='submit' class="flex flex-col gap-4 pb-4">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Notifications | Coolify
Notifications | MapleDeploy
</x-slot>
<x-notification.navbar />
<form wire:submit='submit' class="flex flex-col gap-4 pb-4">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Notifications | Coolify
Notifications | MapleDeploy
</x-slot>
<x-notification.navbar />
<form wire:submit='submit' class="flex flex-col gap-4 pb-4">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Notifications | Coolify
Notifications | MapleDeploy
</x-slot>
<x-notification.navbar />
<form wire:submit='submit' class="flex flex-col gap-4 pb-4">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Notifications | Coolify
Notifications | MapleDeploy
</x-slot>
<x-notification.navbar />
<form wire:submit='submit' class="flex flex-col gap-4 pb-4">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Notifications | Coolify
Notifications | MapleDeploy
</x-slot>
<x-notification.navbar />
<form wire:submit='submit' class="flex flex-col gap-4 pb-4">
@ -29,7 +29,7 @@ class="normal-case dark:text-white btn btn-xs no-animation btn-primary">
<div class="flex items-end gap-2">
<x-forms.input canGate="update" :canResource="$settings" type="password"
helper="Enter a valid HTTP or HTTPS URL. Coolify will send POST requests to this endpoint when events occur."
helper="Enter a valid HTTP or HTTPS URL. MapleDeploy will send POST requests to this endpoint when events occur."
required id="webhookUrl" label="Webhook URL (POST)" />
</div>
</form>

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Profile | Coolify
Profile | MapleDeploy
</x-slot>
<h1>Profile</h1>
<div class="subtitle -mt-2">Your user profile settings.</div>

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($application, 'name')->limit(10) }} > Configuration | Coolify
{{ data_get_str($application, 'name')->limit(10) }} > Configuration | MapleDeploy
</x-slot>
<h1>Configuration</h1>
<livewire:project.shared.configuration-checker :resource="$application" />

View file

@ -1,5 +1,5 @@
<div>
<x-slot:title>{{ data_get_str($application, 'name')->limit(10) }} > Deployments | Coolify</x-slot>
<x-slot:title>{{ data_get_str($application, 'name')->limit(10) }} > Deployments | MapleDeploy</x-slot>
<h1>Deployments</h1>
<livewire:project.shared.configuration-checker :resource="$application" />
<livewire:project.application.heading :application="$application" />

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($application, 'name')->limit(10) }} > Deployment | Coolify
{{ data_get_str($application, 'name')->limit(10) }} > Deployment | MapleDeploy
</x-slot>
<h1 class="py-0">Deployment</h1>
<livewire:project.shared.configuration-checker :resource="$application" />

View file

@ -1,6 +1,6 @@
<form>
<x-slot:title>
{{ data_get_str($project, 'name')->limit(10) }} > Clone | Coolify
{{ data_get_str($project, 'name')->limit(10) }} > Clone | MapleDeploy
</x-slot>
<div class="flex flex-col">
<h1>Clone</h1>

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($database, 'name')->limit(10) }} > Backup | Coolify
{{ data_get_str($database, 'name')->limit(10) }} > Backup | MapleDeploy
</x-slot>
<h1>Backups</h1>
<livewire:project.shared.configuration-checker :resource="$database" />

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($database, 'name')->limit(10) }} > Backups | Coolify
{{ data_get_str($database, 'name')->limit(10) }} > Backups | MapleDeploy
</x-slot>
<h1>Backups</h1>
<livewire:project.shared.configuration-checker :resource="$database" />

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($database, 'name')->limit(10) }} > Configuration | Coolify
{{ data_get_str($database, 'name')->limit(10) }} > Configuration | MapleDeploy
</x-slot>
<h1>Configuration</h1>
<livewire:project.shared.configuration-checker :resource="$database" />

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($project, 'name')->limit(10) }} > Edit | Coolify
{{ data_get_str($project, 'name')->limit(10) }} > Edit | MapleDeploy
</x-slot>
<form wire:submit='submit' class="flex flex-col pb-10">
<div class="flex gap-2">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($project, 'name')->limit(10) }} > Edit | Coolify
{{ data_get_str($project, 'name')->limit(10) }} > Edit | MapleDeploy
</x-slot>
<form wire:submit='submit' class="flex flex-col">
<div class="flex items-end gap-2">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Projects | Coolify
Projects | MapleDeploy
</x-slot>
<div class="flex gap-2">
<h1>Projects</h1>
@ -14,7 +14,7 @@
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2 -mt-1">
@foreach ($projects as $project)
<div class="relative gap-2 cursor-pointer coolbox group">
<a href="{{ $project->navigateTo() }}" class="absolute inset-0"></a>
<a href="{{ $project->navigateTo() }}" {{ wireNavigate() }} class="absolute inset-0"></a>
<div class="flex flex-1 mx-6">
<div class="flex flex-col justify-center flex-1">
<div class="box-title">{{ $project->name }}</div>

View file

@ -13,9 +13,10 @@
</x-forms.button>
</div>
<div>
{{-- MapleDeploy branding: link to upstream examples (still useful) --}}
For example application deployments, checkout <a class="underline dark:text-white"
href="https://github.com/coollabsio/coolify-examples/" target="_blank">Coolify
Examples</a>.
href="https://github.com/coollabsio/coolify-examples/" target="_blank">example
repositories</a>.
</div>
</div>
</form>

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($project, 'name')->limit(10) }} > New | Coolify
{{ data_get_str($project, 'name')->limit(10) }} > New | MapleDeploy
</x-slot>
@if ($type === 'public')
<livewire:project.new.public-git-repository :type="$type" />

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($project, 'name')->limit(10) }} > Resources | Coolify
{{ data_get_str($project, 'name')->limit(10) }} > Resources | MapleDeploy
</x-slot>
<div class="flex flex-col">
<div class="flex items-center gap-2">
@ -29,9 +29,6 @@
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
@endcan
</div>
@php
$projects = auth()->user()->currentTeam()->projects()->get();
@endphp
<nav class="flex pt-2 pb-6">
<ol class="flex items-center">
<li class="inline-flex items-center" x-data="{ projectOpen: false, toggle() { this.projectOpen = !this.projectOpen }, open() { this.projectOpen = true }, close() { this.projectOpen = false } }">
@ -53,7 +50,7 @@
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
class="absolute z-20 top-full mt-1 w-56 -ml-2 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
@foreach ($projects as $proj)
@foreach ($allProjects as $proj)
<a href="{{ route('project.show', ['project_uuid' => $proj->uuid]) }}"
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $proj->uuid === $project->uuid ? 'dark:text-warning font-semibold' : '' }}"
title="{{ $proj->name }}">
@ -63,12 +60,6 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
</div>
</div>
</li>
@php
$allEnvironments = $project
->environments()
->with(['applications', 'services'])
->get();
@endphp
<li class="inline-flex items-center" x-data="{ envOpen: false, activeEnv: null, envPositions: {}, activeRes: null, resPositions: {}, activeMenuEnv: null, menuPositions: {}, closeTimeout: null, envTimeout: null, resTimeout: null, menuTimeout: null, toggle() { this.envOpen = !this.envOpen; if (!this.envOpen) { this.activeEnv = null;
this.activeRes = null;
this.activeMenuEnv = null; } }, open() { clearTimeout(this.closeTimeout);
@ -162,6 +153,16 @@ class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover
<!-- Resources Sub-dropdown (2nd level) -->
@foreach ($allEnvironments as $env)
@php
// Use pre-loaded relations instead of databases() method to avoid N+1 queries
$envDatabases = collect()
->merge($env->postgresqls ?? collect())
->merge($env->redis ?? collect())
->merge($env->mongodbs ?? collect())
->merge($env->mysqls ?? collect())
->merge($env->mariadbs ?? collect())
->merge($env->keydbs ?? collect())
->merge($env->dragonflies ?? collect())
->merge($env->clickhouses ?? collect());
$envResources = collect()
->merge(
$env->applications->map(
@ -169,9 +170,7 @@ class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover
),
)
->merge(
$env
->databases()
->map(fn($db) => ['type' => 'database', 'resource' => $db]),
$envDatabases->map(fn($db) => ['type' => 'database', 'resource' => $db]),
)
->merge(
$env->services->map(fn($svc) => ['type' => 'service', 'resource' => $svc]),
@ -208,10 +207,11 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
'database_uuid' => $res->uuid,
]),
};
// Use loaded relation to check additional_servers (avoids N+1 query)
$resHasMultipleServers =
$resType === 'application' &&
method_exists($res, 'additional_servers') &&
$res->additional_servers()->count() > 0;
($res->relationLoaded('additional_servers') ? $res->additional_servers->count() > 0 : false);
$resServerName = $resHasMultipleServers
? null
: data_get($res, 'destination.server.name');

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($service, 'name')->limit(10) }} > Configuration | Coolify
{{ data_get_str($service, 'name')->limit(10) }} > Configuration | MapleDeploy
</x-slot>
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />

View file

@ -5,7 +5,7 @@
<div class="w-full">
<x-slot:title>
{{ data_get_str($service, 'name')->limit(10) }} >
{{ data_get_str($serviceDatabase, 'name')->limit(10) }} > Backups | Coolify
{{ data_get_str($serviceDatabase, 'name')->limit(10) }} > Backups | MapleDeploy
</x-slot>
<div class="flex gap-2">
<h2 class="pb-4">Scheduled Backups</h2>

View file

@ -21,7 +21,7 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
@if ($resourceType === 'application')
<x-slot:title>
{{ data_get_str($service, 'name')->limit(10) }} >
{{ data_get_str($serviceApplication, 'name')->limit(10) }} | Coolify
{{ data_get_str($serviceApplication, 'name')->limit(10) }} | MapleDeploy
</x-slot>
<form wire:submit='submitApplication'>
<div class="flex items-center gap-2 pb-4">
@ -174,7 +174,7 @@ class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
@elseif ($resourceType === 'database')
<x-slot:title>
{{ data_get_str($service, 'name')->limit(10) }} >
{{ data_get_str($serviceDatabase, 'name')->limit(10) }} | Coolify
{{ data_get_str($serviceDatabase, 'name')->limit(10) }} | MapleDeploy
</x-slot>
@if ($currentRoute === 'project.service.database.import')
<livewire:project.database.import :resource="$serviceDatabase" :key="'import-' . $serviceDatabase->uuid" />

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($resource, 'name')->limit(10) }} > Commands | Coolify
{{ data_get_str($resource, 'name')->limit(10) }} > Commands | MapleDeploy
</x-slot>
@if ($type === 'application')
<livewire:project.shared.configuration-checker :resource="$resource" />

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($resource, 'name')->limit(10) }} > Logs | Coolify
{{ data_get_str($resource, 'name')->limit(10) }} > Logs | MapleDeploy
</x-slot>
<livewire:project.shared.configuration-checker :resource="$resource" />
@if ($type === 'application')

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($resource, 'name')->limit(10) }} > Scheduled Tasks | Coolify
{{ data_get_str($resource, 'name')->limit(10) }} > Scheduled Tasks | MapleDeploy
</x-slot>
@if ($type === 'application')
<h1>Scheduled Task</h1>

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($project, 'name')->limit(10) }} > Environments | Coolify
{{ data_get_str($project, 'name')->limit(10) }} > Environments | MapleDeploy
</x-slot>
<div class="flex items-center gap-2">
<h1>Environments</h1>

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
API Tokens | Coolify
API Tokens | MapleDeploy
</x-slot>
<x-security.navbar />
<div class="pb-4">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Cloud Tokens | Coolify
Cloud Tokens | MapleDeploy
</x-slot>
<x-security.navbar />
<livewire:security.cloud-provider-tokens />

View file

@ -1,6 +1,6 @@
<div x-init="$wire.loadPublicKey()">
<x-slot:title>
Private Key | Coolify
Private Key | MapleDeploy
</x-slot>
<x-security.navbar />
<div x-data="{ showPrivateKey: false }">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Advanced | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Advanced | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > CA Certificate | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > CA Certificate | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Metrics | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Metrics | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Hetzner Token | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Hetzner Token | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Cloudflare Tunnel | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Cloudflare Tunnel | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,31 +1,6 @@
{{-- MapleDeploy branding: Hetzner provider removed --}}
<div class="w-full">
<div class="flex flex-col gap-4">
@can('viewAny', App\Models\CloudProviderToken::class)
<div>
<x-modal-input title="Connect a Hetzner Server">
<x-slot:content>
<div class="relative gap-2 cursor-pointer coolbox group">
<div class="flex items-center gap-4 mx-6">
<svg class="w-10 h-10 flex-shrink-0" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" fill="#D50C2D" rx="8" />
<path d="M40 40 H60 V90 H140 V40 H160 V160 H140 V110 H60 V160 H40 Z" fill="white" />
</svg>
<div class="flex flex-col justify-center flex-1">
<div class="box-title">Connect a Hetzner Server</div>
<div class="box-description">
Deploy servers directly from your Hetzner Cloud account
</div>
</div>
</div>
</div>
</x-slot:content>
<livewire:server.new.by-hetzner :private_keys="$private_keys" :limit_reached="$limit_reached" />
</x-modal-input>
</div>
<div class="border-t dark:border-coolgray-300 my-4"></div>
@endcan
<div>
<h3 class="pb-2">Add Server by IP Address</h3>
<livewire:server.new.by-ip :private_keys="$private_keys" :limit_reached="$limit_reached" />

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Delete Server | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Delete Server | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Destinations | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Destinations | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Docker Cleanup | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Docker Cleanup | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Servers | Coolify
Servers | MapleDeploy
</x-slot>
<div class="flex items-center gap-2">
<h1>Servers</h1>

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Log Drains | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Log Drains | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Private Key | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Private Key | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Proxy Dynamic Configuration | Coolify
Proxy Dynamic Configuration | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Proxy Logs | Coolify
Proxy Logs | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
Proxy Configuration | Coolify
Proxy Configuration | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
@if ($server->isFunctional())

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Resources | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Server Resources | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div x-data="{ activeTab: 'managed' }" class="flex flex-col h-full gap-8 md:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Security | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Security | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<x-slide-over closeWithX fullScreen @startupdate.window="slideOverOpen = true">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Terminal Access | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Terminal Access | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">

View file

@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Sentinel | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Sentinel | MapleDeploy
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

Some files were not shown because too many files have changed in this diff Show more