refactor(proxy-status): refactored how the proxy status is handled on the UI and on the backend

feat(cloudflare): improved cloudflare tunnel automated installation
This commit is contained in:
Andras Bacsai
2025-06-06 14:47:54 +02:00
parent 8e8400f595
commit ddcb14500d
51 changed files with 1277 additions and 829 deletions

View File

@@ -104,14 +104,11 @@ class RunRemoteProcess
$this->activity->save();
if ($this->call_event_on_finish) {
try {
if ($this->call_event_data) {
event(resolve("App\\Events\\$this->call_event_on_finish", [
'data' => $this->call_event_data,
]));
$eventClass = "App\\Events\\$this->call_event_on_finish";
if (! is_null($this->call_event_data)) {
event(new $eventClass($this->call_event_data));
} else {
event(resolve("App\\Events\\$this->call_event_on_finish", [
'userId' => $this->activity->causer_id,
]));
event(new $eventClass($this->activity->causer_id));
}
} catch (\Throwable $e) {
Log::error('Error calling event: '.$e->getMessage());

View File

@@ -3,6 +3,7 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -12,12 +13,38 @@ class CheckProxy
{
use AsAction;
// It should return if the proxy should be started (true) or not (false)
public function handle(Server $server, $fromUI = false): bool
/**
* Determine if the proxy should be started
*
* @param bool $fromUI Whether this check is initiated from the UI
* @return bool True if proxy should be started, false otherwise
*/
public function handle(Server $server, bool $fromUI = false): bool
{
// Early validation checks
if (! $this->shouldProxyRun($server, $fromUI)) {
return false;
}
// Handle swarm vs standalone differently
if ($server->isSwarm()) {
return $this->checkSwarmProxy($server);
}
return $this->checkStandaloneProxy($server, $fromUI);
}
/**
* Basic validation to determine if proxy can run at all
*/
private function shouldProxyRun(Server $server, bool $fromUI): bool
{
// Server must be functional
if (! $server->isFunctional()) {
return false;
}
// Build servers don't need proxy
if ($server->isBuildServer()) {
if ($server->proxy) {
$server->proxy = null;
@@ -26,86 +53,308 @@ class CheckProxy
return false;
}
$proxyType = $server->proxyType();
// Check if proxy is disabled or force stopped
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) {
return false;
}
// Validate proxy configuration
if (! $server->isProxyShouldRun()) {
if ($fromUI) {
throw new \Exception('Proxy should not run. You selected the Custom Proxy.');
} else {
return false;
}
return false;
}
// Determine proxy container name based on environment
$proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
return true;
}
if ($server->isSwarm()) {
$status = getContainerStatus($server, $proxyContainerName);
/**
* Check proxy status for swarm mode
*/
private function checkSwarmProxy(Server $server): bool
{
$proxyContainerName = 'coolify-proxy_traefik';
$status = getContainerStatus($server, $proxyContainerName);
// Update status if changed
if ($server->proxy->status !== $status) {
$server->proxy->set('status', $status);
$server->save();
if ($status === 'running') {
return false;
}
return true;
} else {
$status = getContainerStatus($server, $proxyContainerName);
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
return false;
}
if ($server->settings->is_cloudflare_tunnel) {
return false;
}
$ip = $server->ip;
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$portsToCheck = ['80', '443'];
foreach ($portsToCheck as $port) {
// Use the smart port checker that handles dual-stack properly
if ($this->isPortConflict($server, $port, $proxyContainerName)) {
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>");
} else {
return false;
}
}
}
try {
if ($server->proxyType() !== ProxyTypes::NONE->value) {
$proxyCompose = CheckConfiguration::run($server);
if (isset($proxyCompose)) {
$yaml = Yaml::parse($proxyCompose);
$portsToCheck = [];
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
$ports = data_get($yaml, 'services.traefik.ports');
} elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
$ports = data_get($yaml, 'services.caddy.ports');
}
if (isset($ports)) {
foreach ($ports as $port) {
$portsToCheck[] = str($port)->before(':')->value();
}
}
}
} else {
$portsToCheck = [];
}
} catch (\Exception $e) {
Log::error('Error checking proxy: '.$e->getMessage());
}
if (count($portsToCheck) === 0) {
return false;
}
return true;
}
// If running, no need to start
return $status !== 'running';
}
/**
* Check proxy status for standalone mode
*/
private function checkStandaloneProxy(Server $server, bool $fromUI): bool
{
$proxyContainerName = 'coolify-proxy';
$status = getContainerStatus($server, $proxyContainerName);
if ($server->proxy->status !== $status) {
$server->proxy->set('status', $status);
$server->save();
ProxyStatusChanged::dispatch($server->id);
}
// If already running, no need to start
if ($status === 'running') {
return false;
}
// Cloudflare tunnel doesn't need proxy
if ($server->settings->is_cloudflare_tunnel) {
return false;
}
// Check for port conflicts
$portsToCheck = $this->getPortsToCheck($server);
if (empty($portsToCheck)) {
return false;
}
$this->validatePortsAvailable($server, $portsToCheck, $proxyContainerName, $fromUI);
return true;
}
/**
* Get list of ports that need to be available for the proxy
*/
private function getPortsToCheck(Server $server): array
{
$defaultPorts = ['80', '443'];
try {
if ($server->proxyType() === ProxyTypes::NONE->value) {
return [];
}
$proxyCompose = CheckConfiguration::run($server);
if (! $proxyCompose) {
return $defaultPorts;
}
$yaml = Yaml::parse($proxyCompose);
$ports = $this->extractPortsFromCompose($yaml, $server->proxyType());
return ! empty($ports) ? $ports : $defaultPorts;
} catch (\Exception $e) {
Log::error('Error checking proxy configuration: '.$e->getMessage());
return $defaultPorts;
}
}
/**
* Extract ports from docker-compose configuration
*/
private function extractPortsFromCompose(array $yaml, string $proxyType): array
{
$ports = [];
$servicePorts = null;
if ($proxyType === ProxyTypes::TRAEFIK->value) {
$servicePorts = data_get($yaml, 'services.traefik.ports');
} elseif ($proxyType === ProxyTypes::CADDY->value) {
$servicePorts = data_get($yaml, 'services.caddy.ports');
}
if ($servicePorts) {
foreach ($servicePorts as $port) {
$ports[] = str($port)->before(':')->value();
}
}
return array_unique($ports);
}
/**
* Validate that required ports are available (checked in parallel)
*/
private function validatePortsAvailable(Server $server, array $ports, string $proxyContainerName, bool $fromUI): void
{
if (empty($ports)) {
return;
}
\Log::info('Checking ports in parallel: '.implode(', ', $ports));
// Check all ports in parallel
$conflicts = $this->checkPortsInParallel($server, $ports, $proxyContainerName);
// Handle any conflicts found
foreach ($conflicts as $port) {
$message = "Port $port is in use";
if ($fromUI) {
$message = "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>";
}
throw new \Exception($message);
}
}
/**
* Check multiple ports for conflicts in parallel using concurrent processes
*/
private function checkPortsInParallel(Server $server, array $ports, string $proxyContainerName): array
{
$conflicts = [];
// First, quickly filter out ports used by our own proxy
$ourProxyPorts = $this->getOurProxyPorts($server, $proxyContainerName);
$portsToCheck = array_diff($ports, $ourProxyPorts);
if (empty($portsToCheck)) {
\Log::info('All ports are used by our proxy, no conflicts to check');
return [];
}
// Build a single optimized command to check all ports at once
$command = $this->buildBatchPortCheckCommand($portsToCheck, $server);
if (! $command) {
\Log::warning('No suitable port checking method available, falling back to sequential');
return $this->checkPortsSequentially($server, $portsToCheck, $proxyContainerName);
}
try {
$output = instant_remote_process([$command], $server, true);
$results = $this->parseBatchPortCheckOutput($output, $portsToCheck);
foreach ($results as $port => $isConflict) {
if ($isConflict) {
$conflicts[] = $port;
}
}
} catch (\Throwable $e) {
\Log::warning('Batch port check failed, falling back to sequential: '.$e->getMessage());
return $this->checkPortsSequentially($server, $portsToCheck, $proxyContainerName);
}
return $conflicts;
}
/**
* Get ports currently used by our proxy container
*/
private function getOurProxyPorts(Server $server, string $proxyContainerName): array
{
try {
$getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
$containerId = trim(instant_remote_process([$getProxyContainerId], $server));
if (empty($containerId)) {
return [];
}
$getPortsCommand = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' 2>/dev/null || echo '{}'";
$portsJson = instant_remote_process([$getPortsCommand], $server, true);
$portsData = json_decode($portsJson, true);
$ports = [];
if (is_array($portsData)) {
foreach (array_keys($portsData) as $portSpec) {
if (preg_match('/^(\d+)\/tcp$/', $portSpec, $matches)) {
$ports[] = $matches[1];
}
}
}
return $ports;
} catch (\Throwable $e) {
\Log::warning('Failed to get proxy ports: '.$e->getMessage());
return [];
}
}
/**
* Build a single command to check multiple ports efficiently
*/
private function buildBatchPortCheckCommand(array $ports, Server $server): ?string
{
$portList = implode('|', $ports);
// Try different commands in order of preference
$commands = [
// ss - fastest and most reliable
[
'check' => 'command -v ss >/dev/null 2>&1',
'exec' => "ss -Htuln state listening | grep -E ':($portList) ' | awk '{print \$5}' | cut -d: -f2 | sort -u",
],
// netstat - widely available
[
'check' => 'command -v netstat >/dev/null 2>&1',
'exec' => "netstat -tuln | grep -E ':($portList) ' | grep LISTEN | awk '{print \$4}' | cut -d: -f2 | sort -u",
],
// lsof - last resort
[
'check' => 'command -v lsof >/dev/null 2>&1',
'exec' => "lsof -iTCP -sTCP:LISTEN -P -n | grep -E ':($portList) ' | awk '{print \$9}' | cut -d: -f2 | sort -u",
],
];
foreach ($commands as $cmd) {
try {
instant_remote_process([$cmd['check']], $server);
return $cmd['exec'];
} catch (\Throwable $e) {
continue;
}
}
return null;
}
/**
* Parse output from batch port check command
*/
private function parseBatchPortCheckOutput(string $output, array $portsToCheck): array
{
$results = [];
$usedPorts = array_filter(explode("\n", trim($output)));
foreach ($portsToCheck as $port) {
$results[$port] = in_array($port, $usedPorts);
}
return $results;
}
/**
* Fallback to sequential checking if parallel fails
*/
private function checkPortsSequentially(Server $server, array $ports, string $proxyContainerName): array
{
$conflicts = [];
foreach ($ports as $port) {
try {
if ($this->isPortConflict($server, $port, $proxyContainerName)) {
$conflicts[] = $port;
}
} catch (\Throwable $e) {
\Log::warning("Sequential port check failed for port $port: ".$e->getMessage());
// Continue checking other ports
}
}
return $conflicts;
}
/**
@@ -114,137 +363,168 @@ class CheckProxy
*/
private function isPortConflict(Server $server, string $port, string $proxyContainerName): bool
{
// First check if our own proxy is using this port (which is fine)
// Check if our own proxy is using this port (which is fine)
if ($this->isOurProxyUsingPort($server, $port, $proxyContainerName)) {
return false;
}
// Try different methods to detect port conflicts
$portCheckMethods = [
'checkWithSs',
'checkWithNetstat',
'checkWithLsof',
'checkWithNetcat',
];
foreach ($portCheckMethods as $method) {
try {
$result = $this->$method($server, $port);
if ($result !== null) {
return $result;
}
} catch (\Throwable $e) {
continue; // Try next method
}
}
// If all methods fail, assume port is free to avoid false positives
return false;
}
/**
* Check if our own proxy container is using the port
*/
private function isOurProxyUsingPort(Server $server, string $port, string $proxyContainerName): bool
{
try {
$getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
$containerId = trim(instant_remote_process([$getProxyContainerId], $server));
if (! empty($containerId)) {
$checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'";
try {
instant_remote_process([$checkProxyPort], $server);
// Our proxy is using the port, which is fine
return false;
} catch (\Throwable $e) {
// Our container exists but not using this port
}
if (empty($containerId)) {
return false;
}
$checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'";
instant_remote_process([$checkProxyPort], $server);
return true; // Our proxy is using the port
} catch (\Throwable $e) {
// Container not found or error checking, continue with regular checks
}
// Command sets for different ways to check ports, ordered by preference
$commandSets = [
// Set 1: Use ss to check listener counts by protocol stack
[
'available' => 'command -v ss >/dev/null 2>&1',
'check' => [
// Get listening process details
"ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"",
// Count IPv4 listeners
"echo \"\$ss_output\" | grep -c ':$port '",
],
],
// Set 2: Use netstat as alternative to ss
[
'available' => 'command -v netstat >/dev/null 2>&1',
'check' => [
// Get listening process details
"netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '",
// Count listeners
"echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'",
],
],
// Set 3: Use lsof as last resort
[
'available' => 'command -v lsof >/dev/null 2>&1',
'check' => [
// Get process using the port
"lsof -i :$port -P -n | grep 'LISTEN'",
// Count listeners
"lsof -i :$port -P -n | grep 'LISTEN' | wc -l",
],
],
];
// Try each command set until we find one available
foreach ($commandSets as $set) {
try {
// Check if the command is available
instant_remote_process([$set['available']], $server);
// Run the actual check commands
$output = instant_remote_process($set['check'], $server, true);
// Parse the output lines
$lines = explode("\n", trim($output));
// Get the detailed output and listener count
$details = trim($lines[0] ?? '');
$count = intval(trim($lines[1] ?? '0'));
// If no listeners or empty result, port is free
if ($count == 0 || empty($details)) {
return false;
}
// Try to detect if this is our coolify-proxy
if (strpos($details, 'docker') !== false || strpos($details, $proxyContainerName) !== false) {
// It's likely our docker or proxy, which is fine
return false;
}
// Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6)
// If exactly 2 listeners and both have same port, likely dual-stack
if ($count <= 2) {
// Check if it looks like a standard dual-stack setup
$isDualStack = false;
// Look for IPv4 and IPv6 in the listing (ss output format)
if (preg_match('/LISTEN.*:'.$port.'\s/', $details) &&
(preg_match('/\*:'.$port.'\s/', $details) ||
preg_match('/:::'.$port.'\s/', $details))) {
$isDualStack = true;
}
// For netstat format
if (strpos($details, '0.0.0.0:'.$port) !== false &&
strpos($details, ':::'.$port) !== false) {
$isDualStack = true;
}
// For lsof format (IPv4 and IPv6)
if (strpos($details, '*:'.$port) !== false &&
preg_match('/\*:'.$port.'.*IPv4/', $details) &&
preg_match('/\*:'.$port.'.*IPv6/', $details)) {
$isDualStack = true;
}
if ($isDualStack) {
return false; // This is just a normal dual-stack setup
}
}
// If we get here, it's likely a real port conflict
return true;
} catch (\Throwable $e) {
// This command set failed, try the next one
continue;
}
}
// Fallback to simpler check if all above methods fail
try {
// Just try to bind to the port directly to see if it's available
$checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'";
$result = instant_remote_process([$checkCommand], $server, true);
return trim($result) === 'in-use';
} catch (\Throwable $e) {
// If everything fails, assume the port is free to avoid false positives
return false;
}
}
/**
* Check port usage with ss command
*/
private function checkWithSs(Server $server, string $port): ?bool
{
$commands = [
'command -v ss >/dev/null 2>&1',
"ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"",
"echo \"\$ss_output\" | grep -c ':$port '",
];
$output = instant_remote_process($commands, $server, true);
return $this->parsePortCheckOutput($output, $port);
}
/**
* Check port usage with netstat command
*/
private function checkWithNetstat(Server $server, string $port): ?bool
{
$commands = [
'command -v netstat >/dev/null 2>&1',
"netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '",
"echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'",
];
$output = instant_remote_process($commands, $server, true);
return $this->parsePortCheckOutput($output, $port);
}
/**
* Check port usage with lsof command
*/
private function checkWithLsof(Server $server, string $port): ?bool
{
$commands = [
'command -v lsof >/dev/null 2>&1',
"lsof -i :$port -P -n | grep 'LISTEN'",
"lsof -i :$port -P -n | grep 'LISTEN' | wc -l",
];
$output = instant_remote_process($commands, $server, true);
return $this->parsePortCheckOutput($output, $port);
}
/**
* Check port usage with netcat as fallback
*/
private function checkWithNetcat(Server $server, string $port): ?bool
{
$checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'";
$result = instant_remote_process([$checkCommand], $server, true);
return trim($result) === 'in-use';
}
/**
* Parse output from port checking commands
*/
private function parsePortCheckOutput(string $output, string $port): ?bool
{
$lines = explode("\n", trim($output));
$details = trim($lines[0] ?? '');
$count = intval(trim($lines[1] ?? '0'));
// No listeners found
if ($count === 0 || empty($details)) {
return false;
}
// Check if it's likely our docker/proxy process
if (strpos($details, 'docker') !== false || strpos($details, 'coolify-proxy') !== false) {
return false;
}
// Handle dual-stack scenarios (1-2 listeners for IPv4+IPv6)
if ($count <= 2 && $this->isDualStackSetup($details, $port)) {
return false;
}
// Real port conflict detected
return true;
}
/**
* Detect if this is a normal dual-stack setup (IPv4 + IPv6)
*/
private function isDualStackSetup(string $details, string $port): bool
{
$patterns = [
// ss output format
'/LISTEN.*:'.$port.'\s/',
'/\*:'.$port.'\s/',
'/:::'.$port.'\s/',
// netstat format
'/0\.0\.0\.0:'.$port.'/',
'/:::'.$port.'/',
// lsof format
'/\*:'.$port.'.*IPv[46]/',
];
$matches = 0;
foreach ($patterns as $pattern) {
if (preg_match($pattern, $details)) {
$matches++;
}
}
// If we see patterns indicating both IPv4 and IPv6, it's likely dual-stack
return $matches >= 2;
}
}

View File

@@ -3,7 +3,7 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Events\ProxyStarted;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Models\Activity;
@@ -57,20 +57,19 @@ class StartProxy
" echo 'Successfully stopped and removed existing coolify-proxy.'",
'fi',
"echo 'Starting coolify-proxy.'",
'docker compose up -d',
'docker compose up -d --wait',
"echo 'Successfully started coolify-proxy.'",
]);
$commands = $commands->merge(connectProxyToNetworks($server));
}
if ($async) {
return remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server);
return remote_process($commands, $server, callEventOnFinish: 'ProxyStatusChanged', callEventData: $server->id);
} else {
instant_remote_process($commands, $server);
$server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType);
$server->save();
ProxyStarted::dispatch($server);
ProxyStatusChanged::dispatch($server->id);
return 'OK';
}

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Proxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -24,6 +25,8 @@ class StopProxy
$server->save();
} catch (\Throwable $e) {
return handleError($e);
} finally {
ProxyStatusChanged::dispatch($server->id);
}
}
}

View File

@@ -2,16 +2,16 @@
namespace App\Actions\Server;
use App\Events\CloudflareTunnelConfigured;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Models\Activity;
use Symfony\Component\Yaml\Yaml;
class ConfigureCloudflared
{
use AsAction;
public function handle(Server $server, string $cloudflare_token)
public function handle(Server $server, string $cloudflare_token, string $ssh_domain): Activity
{
try {
$config = [
@@ -24,6 +24,13 @@ class ConfigureCloudflared
'command' => 'tunnel run',
'environment' => [
"TUNNEL_TOKEN={$cloudflare_token}",
'TUNNEL_METRICS=127.0.0.1:60123',
],
'healthcheck' => [
'test' => ['CMD', 'cloudflared', 'tunnel', '--metrics', '127.0.0.1:60123', 'ready'],
'interval' => '5s',
'timeout' => '30s',
'retries' => 5,
],
],
],
@@ -34,22 +41,20 @@ class ConfigureCloudflared
'mkdir -p /tmp/cloudflared',
'cd /tmp/cloudflared',
"echo '$docker_compose_yml_base64' | base64 -d | tee docker-compose.yml > /dev/null",
'echo Pulling latest Cloudflare Tunnel image.',
'docker compose pull',
'docker compose down -v --remove-orphans > /dev/null 2>&1',
'docker compose up -d --remove-orphans',
'echo Stopping existing Cloudflare Tunnel container.',
'docker rm -f coolify-cloudflared || true',
'echo Starting new Cloudflare Tunnel container.',
'docker compose up --wait --wait-timeout 15 --remove-orphans || docker logs coolify-cloudflared',
]);
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
$server->settings->is_cloudflare_tunnel = false;
$server->settings->save();
throw $e;
} finally {
CloudflareTunnelConfigured::dispatch($server->team_id);
$commands = collect([
'rm -fr /tmp/cloudflared',
return remote_process($commands, $server, callEventOnFinish: 'CloudflareTunnelChanged', callEventData: [
'server_id' => $server->id,
'ssh_domain' => $ssh_domain,
]);
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
throw $e;
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CloudflareTunnelChanged
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public $data) {}
}

View File

@@ -3,33 +3,12 @@
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProxyStatusChanged implements ShouldBroadcast
class ProxyStatusChanged
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
public function __construct(public $data) {}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProxyStatusChangedUI implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?int $teamId = null;
public function __construct(int $teamId)
{
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@@ -213,8 +213,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
if (! $application) {
return;
}
$application->status = $containerStatus;
$application->save();
if ($application->status !== $containerStatus) {
$application->status = $containerStatus;
$application->save();
}
}
private function updateApplicationPreviewStatus(string $applicationId, string $pullRequestId, string $containerStatus)
@@ -249,8 +251,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
return;
}
$application->status = 'exited';
$application->save();
if ($application->status !== 'exited') {
$application->status = 'exited';
$application->save();
}
}
});
}
@@ -286,8 +290,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
return;
}
$applicationPreview->status = 'exited';
$applicationPreview->save();
if ($applicationPreview->status !== 'exited') {
$applicationPreview->status = 'exited';
$applicationPreview->save();
}
}
});
}
@@ -318,8 +324,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
if (! $database) {
return;
}
$database->status = $containerStatus;
$database->save();
if ($database->status !== $containerStatus) {
$database->status = $containerStatus;
$database->save();
}
if ($this->isRunning($containerStatus) && $tcpProxy) {
$tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) {
return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running';
@@ -339,8 +347,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
$notFoundDatabaseUuids->each(function ($databaseUuid) {
$database = $this->databases->where('uuid', $databaseUuid)->first();
if ($database) {
$database->status = 'exited';
$database->save();
if ($database->status !== 'exited') {
$database->status = 'exited';
$database->save();
}
if ($database->is_public) {
StopDatabaseProxy::dispatch($database);
}
@@ -358,14 +368,18 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
if ($subType === 'application') {
$application = $service->applications()->where('id', $subId)->first();
if ($application) {
$application->status = $containerStatus;
$application->save();
if ($application->status !== $containerStatus) {
$application->status = $containerStatus;
$application->save();
}
}
} elseif ($subType === 'database') {
$database = $service->databases()->where('id', $subId)->first();
if ($database) {
$database->status = $containerStatus;
$database->save();
if ($database->status !== $containerStatus) {
$database->status = $containerStatus;
$database->save();
}
}
}
}
@@ -378,8 +392,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
$notFoundServiceApplicationIds->each(function ($serviceApplicationId) {
$application = ServiceApplication::find($serviceApplicationId);
if ($application) {
$application->status = 'exited';
$application->save();
if ($application->status !== 'exited') {
$application->status = 'exited';
$application->save();
}
}
});
}
@@ -387,8 +403,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
$notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) {
$database = ServiceDatabase::find($serviceDatabaseId);
if ($database) {
$database->status = 'exited';
$database->save();
if ($database->status !== 'exited') {
$database->status = 'exited';
$database->save();
}
}
});
}

View File

@@ -36,9 +36,10 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
$this->server->proxy->force_stop = false;
$this->server->save();
StartProxy::run($this->server, force: true);
CheckProxy::run($this->server, true);
// CheckProxy::run($this->server, true);
} catch (\Throwable $e) {
return handleError($e);
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Listeners;
use App\Events\CloudflareTunnelChanged;
use App\Events\CloudflareTunnelConfigured;
use App\Models\Server;
use Illuminate\Support\Sleep;
class CloudflareTunnelChangedNotification
{
public Server $server;
public function __construct() {}
public function handle(CloudflareTunnelChanged $event): void
{
$server_id = data_get($event, 'data.server_id');
$ssh_domain = data_get($event, 'data.ssh_domain');
$this->server = Server::find($server_id)->firstOrFail();
// Check if cloudflare tunnel is running (container is healthy) - try 3 times with 5 second intervals
$cloudflareHealthy = false;
$attempts = 3;
for ($i = 1; $i <= $attempts; $i++) {
\Log::debug("Cloudflare health check attempt {$i}/{$attempts}", ['server_id' => $server_id]);
$result = instant_remote_process_with_timeout(['docker inspect coolify-cloudflared | jq -e ".[0].State.Health.Status == \"healthy\""'], $this->server, false, 10);
if (blank($result)) {
\Log::debug("Cloudflare Tunnels container not found on attempt {$i}", ['server_id' => $server_id]);
} elseif ($result === 'true') {
\Log::debug("Cloudflare Tunnels container healthy on attempt {$i}", ['server_id' => $server_id]);
$cloudflareHealthy = true;
break;
} else {
\Log::debug("Cloudflare Tunnels container not healthy on attempt {$i}", ['server_id' => $server_id, 'result' => $result]);
}
// Sleep between attempts (except after the last attempt)
if ($i < $attempts) {
Sleep::for(5)->seconds();
}
}
if (! $cloudflareHealthy) {
\Log::error('Cloudflare Tunnels container failed all health checks.', ['server_id' => $server_id, 'attempts' => $attempts]);
return;
}
$this->server->settings->update([
'is_cloudflare_tunnel' => true,
]);
// Only update IP if it's not already set to the ssh_domain or if it's empty
if ($this->server->ip !== $ssh_domain && ! empty($ssh_domain)) {
\Log::debug('Cloudflare Tunnels configuration updated - updating IP address.', ['old_ip' => $this->server->ip, 'new_ip' => $ssh_domain]);
$this->server->update(['ip' => $ssh_domain]);
} else {
\Log::debug('Cloudflare Tunnels configuration updated - IP address unchanged.', ['current_ip' => $this->server->ip]);
}
$teamId = $this->server->team_id;
CloudflareTunnelConfigured::dispatch($teamId);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Listeners;
use App\Events\ProxyStarted;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
class ProxyStartedNotification
@@ -18,5 +19,6 @@ class ProxyStartedNotification
$this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false;
$this->server->save();
// ProxyStatusChanged::dispatch( $this->server->id);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Listeners;
use App\Events\ProxyStatusChanged;
use App\Events\ProxyStatusChangedUI;
use App\Models\Server;
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
class ProxyStatusChangedNotification implements ShouldQueueAfterCommit
{
public function __construct() {}
public function handle(ProxyStatusChanged $event)
{
$serverId = $event->data;
$server = Server::find($serverId);
if (is_null($server)) {
return;
}
$proxyContainerName = 'coolify-proxy';
$status = getContainerStatus($server, $proxyContainerName);
$server->proxy->set('status', $status);
$server->save();
if ($status === 'running') {
$server->setupDefaultRedirect();
$server->setupDynamicProxyConfiguration();
$server->proxy->force_stop = false;
$server->save();
}
ProxyStatusChangedUI::dispatch($server->team_id);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Server\ConfigureCloudflared;
use App\Models\Server;
use Livewire\Attributes\Validate;
use Livewire\Component;
class CloudflareTunnel extends Component
{
public Server $server;
#[Validate(['required', 'string'])]
public string $cloudflare_token;
#[Validate(['required', 'string'])]
public string $ssh_domain;
#[Validate(['required', 'boolean'])]
public bool $isCloudflareTunnelsEnabled;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh',
];
}
public function refresh()
{
$this->mount($this->server->uuid);
}
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
if ($this->server->isLocalhost()) {
return redirect()->route('server.show', ['server_uuid' => $server_uuid]);
}
$this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function toggleCloudflareTunnels()
{
try {
remote_process(['docker rm -f coolify-cloudflared'], $this->server, false, 10);
$this->isCloudflareTunnelsEnabled = false;
$this->server->settings->is_cloudflare_tunnel = false;
$this->server->settings->save();
if ($this->server->ip_previous) {
$this->server->update(['ip' => $this->server->ip_previous]);
$this->dispatch('success', 'Cloudflare Tunnel disabled.<br><br>Manually updated the server IP address to its previous IP address.');
} else {
$this->dispatch('success', 'Cloudflare Tunnel disabled. Please update the server IP address to its real IP address in the server settings.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCloudflareConfig()
{
$this->isCloudflareTunnelsEnabled = true;
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnel enabled.');
}
public function automatedCloudflareConfig()
{
try {
if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
}
$activity = ConfigureCloudflared::run($this->server, $this->cloudflare_token, $this->ssh_domain);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.cloudflare-tunnel');
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Livewire\Attributes\Validate;
use Livewire\Component;
class CloudflareTunnels extends Component
{
public Server $server;
#[Validate(['required', 'boolean'])]
public bool $isCloudflareTunnelsEnabled;
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
if ($this->server->isLocalhost()) {
return redirect()->route('server.show', ['server_uuid' => $server_uuid]);
}
$this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
$this->validate();
$this->server->settings->is_cloudflare_tunnel = $this->isCloudflareTunnelsEnabled;
$this->server->settings->save();
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCloudflareConfig()
{
$this->isCloudflareTunnelsEnabled = true;
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
public function render()
{
return view('livewire.server.cloudflare-tunnels');
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Server\ConfigureCloudflared;
use App\Models\Server;
use Livewire\Component;
class ConfigureCloudflareTunnels extends Component
{
public $server_id;
public string $cloudflare_token;
public string $ssh_domain;
public function alreadyConfigured()
{
try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
$server->settings->is_cloudflare_tunnel = true;
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
// remove / from the end
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
}
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain;
$server->save();
$server->settings->save();
$this->dispatch('info', 'Cloudflare Tunnels configuration started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.configure-cloudflare-tunnels');
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Jobs\RestartProxyJob;
use App\Models\Server;
use Livewire\Component;
class Navbar extends Component
{
public Server $server;
public bool $isChecking = false;
public ?string $currentRoute = null;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChangedUI" => 'showNotification',
];
}
public function mount(Server $server)
{
$this->server = $server;
$this->currentRoute = request()->route()->getName();
}
public function restart()
{
try {
RestartProxyJob::dispatch($this->server);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkProxy()
{
try {
CheckProxy::run($this->server, true);
$this->dispatch('startProxy')->self();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startProxy()
{
try {
$activity = StartProxy::run($this->server, force: true);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function stop(bool $forceStop = true)
{
try {
StopProxy::dispatch($this->server, $forceStop);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkProxyStatus()
{
if ($this->isChecking) {
return;
}
try {
$this->isChecking = true;
CheckProxy::run($this->server, true);
$this->showNotification();
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->isChecking = false;
}
}
public function showNotification()
{
$status = $this->server->proxy->status ?? 'unknown';
$forceStop = $this->server->proxy->force_stop ?? false;
switch ($status) {
case 'running':
$this->dispatch('success', 'Proxy is running.');
break;
case 'restarting':
$this->dispatch('info', 'Initiating proxy restart.');
break;
case 'exited':
if ($forceStop) {
$this->dispatch('info', 'Proxy is stopped manually.');
} else {
$this->dispatch('info', 'Proxy is stopped manually. Starting in a moment.');
}
break;
default:
$this->dispatch('warning', 'Proxy is not running.');
break;
}
}
public function render()
{
return view('livewire.server.navbar');
}
}

View File

@@ -19,7 +19,7 @@ class Proxy extends Component
public ?string $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
protected $listeners = ['saveConfiguration' => 'submit'];
protected $rules = [
'server.settings.generate_exact_labels' => 'required|boolean',
@@ -32,10 +32,10 @@ class Proxy extends Component
$this->redirect_url = data_get($this->server, 'proxy.redirect_url');
}
public function proxyStatusUpdated()
{
$this->dispatch('refresh')->self();
}
// public function proxyStatusUpdated()
// {
// $this->dispatch('refresh')->self();
// }
public function changeProxy()
{

View File

@@ -1,106 +0,0 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Events\ProxyStatusChanged;
use App\Jobs\RestartProxyJob;
use App\Models\Server;
use Livewire\Component;
class Deploy extends Component
{
public Server $server;
public bool $traefikDashboardAvailable = false;
public ?string $currentRoute = null;
public ?string $serverIp = null;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChanged" => 'proxyStarted',
'proxyStatusUpdated',
'traefikDashboardAvailable',
'serverRefresh' => 'proxyStatusUpdated',
'checkProxy',
'startProxy',
'proxyChanged' => 'proxyStatusUpdated',
];
}
public function mount()
{
if ($this->server->id === 0) {
$this->serverIp = base_ip();
} else {
$this->serverIp = $this->server->ip;
}
$this->currentRoute = request()->route()->getName();
}
public function traefikDashboardAvailable(bool $data)
{
$this->traefikDashboardAvailable = $data;
}
public function proxyStarted()
{
CheckProxy::run($this->server, true);
$this->dispatch('proxyStatusUpdated');
}
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function restart()
{
try {
RestartProxyJob::dispatch($this->server);
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkProxy()
{
try {
CheckProxy::run($this->server, true);
$this->dispatch('startProxyPolling');
$this->dispatch('proxyChecked');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startProxy()
{
try {
$this->server->proxy->force_stop = false;
$this->server->save();
$activity = StartProxy::run($this->server, force: true);
$this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function stop(bool $forceStop = true)
{
try {
StopProxy::run($this->server, $forceStop);
$this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -19,7 +19,7 @@ class DynamicConfigurations extends Component
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations',
"echo-private:team.{$teamId},ProxyStatusChangedUI" => 'loadDynamicConfigurations',
'loadDynamicConfigurations',
];
}

View File

@@ -11,12 +11,12 @@ class Show extends Component
public $parameters = [];
protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
// protected $listeners = ['proxyStatusUpdated'];
public function proxyStatusUpdated()
{
$this->server->refresh();
}
// public function proxyStatusUpdated()
// {
// $this->server->refresh();
// }
public function mount()
{

View File

@@ -1,77 +0,0 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Models\Server;
use Livewire\Component;
class Status extends Component
{
public Server $server;
public bool $polling = false;
public int $numberOfPolls = 0;
protected $listeners = [
'proxyStatusUpdated',
'startProxyPolling',
];
public function startProxyPolling()
{
$this->checkProxy();
}
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function checkProxy(bool $notification = false)
{
try {
if ($this->polling) {
if ($this->numberOfPolls >= 10) {
$this->polling = false;
$this->numberOfPolls = 0;
$notification && $this->dispatch('error', 'Proxy is not running.');
return;
}
$this->numberOfPolls++;
}
$shouldStart = CheckProxy::run($this->server, true);
if ($shouldStart) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$notification && $this->dispatch('success', 'Proxy is running.');
} elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) {
$notification && $this->dispatch('error', 'Proxy has exited.');
} elseif ($this->server->proxy->force_stop) {
$notification && $this->dispatch('error', 'Proxy is stopped manually.');
} else {
$notification && $this->dispatch('error', 'Proxy is not running.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function getProxyStatus()
{
try {
GetContainersStatus::run($this->server);
// dispatch_sync(new ContainerStatusJob($this->server));
$this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -86,10 +86,7 @@ class Show extends Component
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh',
'refreshServerShow' => 'refresh',
];
}
@@ -211,7 +208,6 @@ class Show extends Component
$this->server->settings->is_usable = $this->isUsable = true;
$this->server->settings->save();
ServerReachabilityChanged::dispatch($this->server);
$this->dispatch('proxyStatusUpdated');
} else {
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);

View File

@@ -69,6 +69,11 @@ class Server extends BaseModel
}
if ($server->ip) {
$payload['ip'] = str($server->ip)->trim();
// Update ip_previous when ip is being changed
if ($server->isDirty('ip') && $server->getOriginal('ip')) {
$payload['ip_previous'] = $server->getOriginal('ip');
}
}
$server->forceFill($payload);
});

View File

@@ -2,10 +2,8 @@
namespace App\Providers;
use App\Events\ProxyStarted;
use App\Listeners\MaintenanceModeDisabledNotification;
use App\Listeners\MaintenanceModeEnabledNotification;
use App\Listeners\ProxyStartedNotification;
use Illuminate\Foundation\Events\MaintenanceModeDisabled;
use Illuminate\Foundation\Events\MaintenanceModeEnabled;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@@ -30,9 +28,6 @@ class EventServiceProvider extends ServiceProvider
GoogleExtendSocialite::class.'@handle',
InfomaniakExtendSocialite::class.'@handle',
],
ProxyStarted::class => [
ProxyStartedNotification::class,
],
];
public function boot(): void
@@ -42,6 +37,6 @@ class EventServiceProvider extends ServiceProvider
public function shouldDiscoverEvents(): bool
{
return false;
return true;
}
}