Merge branch 'next' into decouple-storage-from-sentinel
This commit is contained in:
commit
f75bc85bc1
11 changed files with 330 additions and 124 deletions
|
|
@ -461,9 +461,10 @@ private function aggregateApplicationStatus($application, Collection $containerS
|
|||
}
|
||||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// Use preserveRestarting: true so applications show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
|
||||
return $aggregator->aggregateFromStrings($relevantStatuses, $maxRestartCount);
|
||||
return $aggregator->aggregateFromStrings($relevantStatuses, $maxRestartCount, preserveRestarting: true);
|
||||
}
|
||||
|
||||
private function aggregateServiceContainerStatuses($services)
|
||||
|
|
@ -518,8 +519,9 @@ private function aggregateServiceContainerStatuses($services)
|
|||
}
|
||||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// Use preserveRestarting: true so individual sub-resources show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses);
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, preserveRestarting: true);
|
||||
|
||||
// Update service sub-resource status with aggregated result
|
||||
if ($aggregatedStatus) {
|
||||
|
|
|
|||
|
|
@ -1813,9 +1813,9 @@ private function health_check()
|
|||
$this->application->update(['status' => 'running']);
|
||||
$this->application_deployment_queue->addLogEntry('New container is healthy.');
|
||||
break;
|
||||
}
|
||||
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
|
||||
} elseif (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
|
||||
$this->newVersionIsHealthy = false;
|
||||
$this->application_deployment_queue->addLogEntry('New container is unhealthy.', type: 'error');
|
||||
$this->query_logs();
|
||||
break;
|
||||
}
|
||||
|
|
@ -3187,6 +3187,18 @@ private function stop_running_container(bool $force = false)
|
|||
$this->graceful_shutdown_container($this->container_name);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// If new version is healthy, this is just cleanup - don't fail the deployment
|
||||
if ($this->newVersionIsHealthy || $force) {
|
||||
$this->application_deployment_queue->addLogEntry(
|
||||
"Warning: Could not remove old container: {$e->getMessage()}",
|
||||
'stderr',
|
||||
hidden: true
|
||||
);
|
||||
|
||||
return; // Don't re-throw - cleanup failures shouldn't fail successful deployments
|
||||
}
|
||||
|
||||
// Only re-throw if deployment hasn't succeeded yet
|
||||
throw new DeploymentException("Failed to stop running container: {$e->getMessage()}", $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -300,8 +300,9 @@ private function aggregateMultiContainerStatuses()
|
|||
}
|
||||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// Use preserveRestarting: true so applications show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0);
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0, preserveRestarting: true);
|
||||
|
||||
// Update application status with aggregated result
|
||||
if ($aggregatedStatus && $application->status !== $aggregatedStatus) {
|
||||
|
|
@ -360,8 +361,9 @@ private function aggregateServiceContainerStatuses()
|
|||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// NOTE: Sentinel does NOT provide restart count data, so maxRestartCount is always 0
|
||||
// Use preserveRestarting: true so individual sub-resources show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0);
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0, preserveRestarting: true);
|
||||
|
||||
// Update service sub-resource status with aggregated result
|
||||
if ($aggregatedStatus && $subResource->status !== $aggregatedStatus) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class GetLogs extends Component
|
|||
|
||||
public ?bool $streamLogs = false;
|
||||
|
||||
public ?bool $showTimeStamps = false;
|
||||
public ?bool $showTimeStamps = true;
|
||||
|
||||
public ?int $numberOfLines = 100;
|
||||
|
||||
|
|
|
|||
|
|
@ -43,9 +43,19 @@ public function toMail($notifiable = null): MailMessage
|
|||
$mail = new MailMessage;
|
||||
$count = $this->servers->count();
|
||||
|
||||
// Transform servers to include URLs
|
||||
$serversWithUrls = $this->servers->map(function ($server) {
|
||||
return [
|
||||
'name' => $server->name,
|
||||
'uuid' => $server->uuid,
|
||||
'url' => base_url().'/server/'.$server->uuid.'/proxy',
|
||||
'outdatedInfo' => $server->outdatedInfo ?? [],
|
||||
];
|
||||
});
|
||||
|
||||
$mail->subject("Coolify: Traefik proxy outdated on {$count} server(s)");
|
||||
$mail->view('emails.traefik-version-outdated', [
|
||||
'servers' => $this->servers,
|
||||
'servers' => $serversWithUrls,
|
||||
'count' => $count,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,14 +16,23 @@
|
|||
* UI components transform this to human-readable format (e.g., "Running (Healthy)").
|
||||
*
|
||||
* State Priority (highest to lowest):
|
||||
* 1. Restarting → degraded:unhealthy
|
||||
* 2. Crash Loop (exited with restarts) → degraded:unhealthy
|
||||
* 3. Mixed (running + exited) → degraded:unhealthy
|
||||
* 4. Running → running:healthy/unhealthy/unknown
|
||||
* 5. Dead/Removing → degraded:unhealthy
|
||||
* 6. Paused → paused:unknown
|
||||
* 7. Starting/Created → starting:unknown
|
||||
* 8. Exited → exited
|
||||
* 1. Degraded (from sub-resources) → degraded:unhealthy
|
||||
* 2. Restarting → degraded:unhealthy (or restarting:unknown if preserveRestarting=true)
|
||||
* 3. Crash Loop (exited with restarts) → degraded:unhealthy
|
||||
* 4. Mixed (running + exited) → degraded:unhealthy
|
||||
* 5. Mixed (running + starting) → starting:unknown
|
||||
* 6. Running → running:healthy/unhealthy/unknown
|
||||
* 7. Dead/Removing → degraded:unhealthy
|
||||
* 8. Paused → paused:unknown
|
||||
* 9. Starting/Created → starting:unknown
|
||||
* 10. Exited → exited
|
||||
*
|
||||
* The $preserveRestarting parameter controls whether "restarting" containers should be
|
||||
* reported as "restarting:unknown" (true) or "degraded:unhealthy" (false, default).
|
||||
* - Use preserveRestarting=true for individual sub-resources (ServiceApplication/ServiceDatabase)
|
||||
* so they show "Restarting" in the UI.
|
||||
* - Use preserveRestarting=false for overall Service status aggregation where any restarting
|
||||
* container should mark the entire service as "Degraded".
|
||||
*/
|
||||
class ContainerStatusAggregator
|
||||
{
|
||||
|
|
@ -32,9 +41,10 @@ class ContainerStatusAggregator
|
|||
*
|
||||
* @param Collection $containerStatuses Collection of status strings (e.g., "running (healthy)", "running:healthy")
|
||||
* @param int $maxRestartCount Maximum restart count across containers (for crash loop detection)
|
||||
* @param bool $preserveRestarting If true, "restarting" containers return "restarting:unknown" instead of "degraded:unhealthy"
|
||||
* @return string Aggregated status in colon format (e.g., "running:healthy")
|
||||
*/
|
||||
public function aggregateFromStrings(Collection $containerStatuses, int $maxRestartCount = 0): string
|
||||
public function aggregateFromStrings(Collection $containerStatuses, int $maxRestartCount = 0, bool $preserveRestarting = false): string
|
||||
{
|
||||
// Validate maxRestartCount parameter
|
||||
if ($maxRestartCount < 0) {
|
||||
|
|
@ -64,10 +74,16 @@ public function aggregateFromStrings(Collection $containerStatuses, int $maxRest
|
|||
$hasStarting = false;
|
||||
$hasPaused = false;
|
||||
$hasDead = false;
|
||||
$hasDegraded = false;
|
||||
|
||||
// Parse each status string and set flags
|
||||
foreach ($containerStatuses as $status) {
|
||||
if (str($status)->contains('restarting')) {
|
||||
if (str($status)->contains('degraded')) {
|
||||
$hasDegraded = true;
|
||||
if (str($status)->contains('unhealthy')) {
|
||||
$hasUnhealthy = true;
|
||||
}
|
||||
} elseif (str($status)->contains('restarting')) {
|
||||
$hasRestarting = true;
|
||||
} elseif (str($status)->contains('running')) {
|
||||
$hasRunning = true;
|
||||
|
|
@ -98,7 +114,9 @@ public function aggregateFromStrings(Collection $containerStatuses, int $maxRest
|
|||
$hasStarting,
|
||||
$hasPaused,
|
||||
$hasDead,
|
||||
$maxRestartCount
|
||||
$hasDegraded,
|
||||
$maxRestartCount,
|
||||
$preserveRestarting
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -107,9 +125,10 @@ public function aggregateFromStrings(Collection $containerStatuses, int $maxRest
|
|||
*
|
||||
* @param Collection $containers Collection of Docker container objects with State property
|
||||
* @param int $maxRestartCount Maximum restart count across containers (for crash loop detection)
|
||||
* @param bool $preserveRestarting If true, "restarting" containers return "restarting:unknown" instead of "degraded:unhealthy"
|
||||
* @return string Aggregated status in colon format (e.g., "running:healthy")
|
||||
*/
|
||||
public function aggregateFromContainers(Collection $containers, int $maxRestartCount = 0): string
|
||||
public function aggregateFromContainers(Collection $containers, int $maxRestartCount = 0, bool $preserveRestarting = false): string
|
||||
{
|
||||
// Validate maxRestartCount parameter
|
||||
if ($maxRestartCount < 0) {
|
||||
|
|
@ -175,7 +194,9 @@ public function aggregateFromContainers(Collection $containers, int $maxRestartC
|
|||
$hasStarting,
|
||||
$hasPaused,
|
||||
$hasDead,
|
||||
$maxRestartCount
|
||||
false, // $hasDegraded - not applicable for container objects, only for status strings
|
||||
$maxRestartCount,
|
||||
$preserveRestarting
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +211,9 @@ public function aggregateFromContainers(Collection $containers, int $maxRestartC
|
|||
* @param bool $hasStarting Has at least one starting/created container
|
||||
* @param bool $hasPaused Has at least one paused container
|
||||
* @param bool $hasDead Has at least one dead/removing container
|
||||
* @param bool $hasDegraded Has at least one degraded container
|
||||
* @param int $maxRestartCount Maximum restart count (for crash loop detection)
|
||||
* @param bool $preserveRestarting If true, return "restarting:unknown" instead of "degraded:unhealthy" for restarting containers
|
||||
* @return string Status in colon format (e.g., "running:healthy")
|
||||
*/
|
||||
private function resolveStatus(
|
||||
|
|
@ -202,24 +225,40 @@ private function resolveStatus(
|
|||
bool $hasStarting,
|
||||
bool $hasPaused,
|
||||
bool $hasDead,
|
||||
int $maxRestartCount
|
||||
bool $hasDegraded,
|
||||
int $maxRestartCount,
|
||||
bool $preserveRestarting = false
|
||||
): string {
|
||||
// Priority 1: Restarting containers (degraded state)
|
||||
if ($hasRestarting) {
|
||||
// Priority 1: Degraded containers from sub-resources (highest priority)
|
||||
// If any service/application within a service stack is degraded, the entire stack is degraded
|
||||
if ($hasDegraded) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 2: Crash loop detection (exited with restart count > 0)
|
||||
// Priority 2: Restarting containers
|
||||
// When preserveRestarting is true (for individual sub-resources), keep as "restarting"
|
||||
// When false (for overall service status), mark as "degraded"
|
||||
if ($hasRestarting) {
|
||||
return $preserveRestarting ? 'restarting:unknown' : 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 3: Crash loop detection (exited with restart count > 0)
|
||||
if ($hasExited && $maxRestartCount > 0) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 3: Mixed state (some running, some exited = degraded)
|
||||
// Priority 4: Mixed state (some running, some exited = degraded)
|
||||
if ($hasRunning && $hasExited) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 4: Running containers (check health status)
|
||||
// Priority 5: Mixed state (some running, some starting = still starting)
|
||||
// If any component is still starting, the entire service stack is not fully ready
|
||||
if ($hasRunning && $hasStarting) {
|
||||
return 'starting:unknown';
|
||||
}
|
||||
|
||||
// Priority 6: Running containers (check health status)
|
||||
if ($hasRunning) {
|
||||
if ($hasUnhealthy) {
|
||||
return 'running:unhealthy';
|
||||
|
|
@ -230,22 +269,22 @@ private function resolveStatus(
|
|||
}
|
||||
}
|
||||
|
||||
// Priority 5: Dead or removing containers
|
||||
// Priority 7: Dead or removing containers
|
||||
if ($hasDead) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 6: Paused containers
|
||||
// Priority 8: Paused containers
|
||||
if ($hasPaused) {
|
||||
return 'paused:unknown';
|
||||
}
|
||||
|
||||
// Priority 7: Starting/created containers
|
||||
// Priority 9: Starting/created containers
|
||||
if ($hasStarting) {
|
||||
return 'starting:unknown';
|
||||
}
|
||||
|
||||
// Priority 8: All containers exited (no restart count = truly stopped)
|
||||
// Priority 10: All containers exited (no restart count = truly stopped)
|
||||
return 'exited';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@
|
|||
|
||||
@foreach ($servers as $server)
|
||||
@php
|
||||
$info = $server->outdatedInfo ?? [];
|
||||
$current = $info['current'] ?? 'unknown';
|
||||
$latest = $info['latest'] ?? 'unknown';
|
||||
$isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
|
||||
$serverName = data_get($server, 'name', 'Unknown Server');
|
||||
$serverUrl = data_get($server, 'url', '#');
|
||||
$info = data_get($server, 'outdatedInfo', []);
|
||||
$current = data_get($info, 'current', 'unknown');
|
||||
$latest = data_get($info, 'latest', 'unknown');
|
||||
$isPatch = (data_get($info, 'type', 'patch_update') === 'patch_update');
|
||||
$hasNewerBranch = isset($info['newer_branch_target']);
|
||||
$hasUpgrades = $hasUpgrades ?? false;
|
||||
if (!$isPatch || $hasNewerBranch) {
|
||||
|
|
@ -19,8 +21,9 @@
|
|||
$latest = str_starts_with($latest, 'v') ? $latest : "v{$latest}";
|
||||
|
||||
// For minor upgrades, use the upgrade_target (e.g., "v3.6")
|
||||
if (!$isPatch && isset($info['upgrade_target'])) {
|
||||
$upgradeTarget = str_starts_with($info['upgrade_target'], 'v') ? $info['upgrade_target'] : "v{$info['upgrade_target']}";
|
||||
if (!$isPatch && data_get($info, 'upgrade_target')) {
|
||||
$upgradeTarget = data_get($info, 'upgrade_target');
|
||||
$upgradeTarget = str_starts_with($upgradeTarget, 'v') ? $upgradeTarget : "v{$upgradeTarget}";
|
||||
} else {
|
||||
// For patch updates, show the full version
|
||||
$upgradeTarget = $latest;
|
||||
|
|
@ -28,22 +31,23 @@
|
|||
|
||||
// Get newer branch info if available
|
||||
if ($hasNewerBranch) {
|
||||
$newerBranchTarget = $info['newer_branch_target'];
|
||||
$newerBranchLatest = str_starts_with($info['newer_branch_latest'], 'v') ? $info['newer_branch_latest'] : "v{$info['newer_branch_latest']}";
|
||||
$newerBranchTarget = data_get($info, 'newer_branch_target', 'unknown');
|
||||
$newerBranchLatest = data_get($info, 'newer_branch_latest', 'unknown');
|
||||
$newerBranchLatest = str_starts_with($newerBranchLatest, 'v') ? $newerBranchLatest : "v{$newerBranchLatest}";
|
||||
}
|
||||
@endphp
|
||||
@if ($isPatch && $hasNewerBranch)
|
||||
- **{{ $server->name }}**: {{ $current }} → {{ $upgradeTarget }} (patch update available) | Also available: {{ $newerBranchTarget }} (latest patch: {{ $newerBranchLatest }}) - new minor version
|
||||
- [**{{ $serverName }}**]({{ $serverUrl }}): {{ $current }} → {{ $upgradeTarget }} (patch update available) | Also available: {{ $newerBranchTarget }} (latest patch: {{ $newerBranchLatest }}) - new minor version
|
||||
@elseif ($isPatch)
|
||||
- **{{ $server->name }}**: {{ $current }} → {{ $upgradeTarget }} (patch update available)
|
||||
- [**{{ $serverName }}**]({{ $serverUrl }}): {{ $current }} → {{ $upgradeTarget }} (patch update available)
|
||||
@else
|
||||
- **{{ $server->name }}**: {{ $current }} (latest patch: {{ $latest }}) → {{ $upgradeTarget }} (new minor version available)
|
||||
- [**{{ $serverName }}**]({{ $serverUrl }}): {{ $current }} (latest patch: {{ $latest }}) → {{ $upgradeTarget }} (new minor version available)
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
## Recommendation
|
||||
|
||||
It is recommended to test the new Traefik version before switching it in production environments. You can update your proxy configuration through your [Coolify Dashboard]({{ config('app.url') }}).
|
||||
It is recommended to test the new Traefik version before switching it in production environments. You can update your proxy configuration by clicking on any server name above.
|
||||
|
||||
@if ($hasUpgrades ?? false)
|
||||
**Important for minor version upgrades:** Before upgrading to a new minor version, please read the [Traefik changelog](https://github.com/traefik/traefik/releases) to understand breaking changes and new features.
|
||||
|
|
@ -58,5 +62,5 @@
|
|||
|
||||
---
|
||||
|
||||
You can manage your server proxy settings in your Coolify Dashboard.
|
||||
Click on any server name above to manage its proxy settings.
|
||||
</x-emails.layout>
|
||||
|
|
|
|||
|
|
@ -65,21 +65,6 @@
|
|||
<div :class="fullscreen ? 'fixed top-4 right-4' : 'absolute top-6 right-0'">
|
||||
<div class="flex justify-end gap-4" :class="fullscreen ? 'fixed' : ''"
|
||||
style="transform: translateX(-100%)">
|
||||
{{-- <button title="Go Top" x-show="fullscreen" x-on:click="goTop">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100" 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="M12 5v14m4-10l-4-4M8 9l4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Follow Logs" x-show="fullscreen" :class="alwaysScroll ? 'dark:text-warning' : ''"
|
||||
x-on:click="toggleScroll">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100" 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="M12 5v14m4-4l-4 4m-4-4l4 4" />
|
||||
</svg>
|
||||
</button> --}}
|
||||
<button title="Fullscreen" x-show="!fullscreen" x-on:click="makeFullscreen">
|
||||
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
@ -102,64 +87,29 @@
|
|||
</div>
|
||||
</div>
|
||||
@if ($outputs)
|
||||
<div id="logs" class="font-mono text-sm">
|
||||
@foreach (explode("\n", trim($outputs)) as $line)
|
||||
@if (!empty(trim($line)))
|
||||
@php
|
||||
$lowerLine = strtolower($line);
|
||||
$isError =
|
||||
str_contains($lowerLine, 'error') ||
|
||||
str_contains($lowerLine, 'err') ||
|
||||
str_contains($lowerLine, 'failed') ||
|
||||
str_contains($lowerLine, 'exception');
|
||||
$isWarning =
|
||||
str_contains($lowerLine, 'warn') ||
|
||||
str_contains($lowerLine, 'warning') ||
|
||||
str_contains($lowerLine, 'wrn');
|
||||
$isDebug =
|
||||
str_contains($lowerLine, 'debug') ||
|
||||
str_contains($lowerLine, 'dbg') ||
|
||||
str_contains($lowerLine, 'trace');
|
||||
$barColor = $isError
|
||||
? 'bg-red-500 dark:bg-red-400'
|
||||
: ($isWarning
|
||||
? 'bg-warning-500 dark:bg-warning-400'
|
||||
: ($isDebug
|
||||
? 'bg-purple-500 dark:bg-purple-400'
|
||||
: 'bg-blue-500 dark:bg-blue-400'));
|
||||
$bgColor = $isError
|
||||
? 'bg-red-50/50 dark:bg-red-900/20 hover:bg-red-100/50 dark:hover:bg-red-800/30'
|
||||
: ($isWarning
|
||||
? 'bg-warning-50/50 dark:bg-warning-900/20 hover:bg-warning-100/50 dark:hover:bg-warning-800/30'
|
||||
: ($isDebug
|
||||
? 'bg-purple-50/50 dark:bg-purple-900/20 hover:bg-purple-100/50 dark:hover:bg-purple-800/30'
|
||||
: 'bg-blue-50/50 dark:bg-blue-900/20 hover:bg-blue-100/50 dark:hover:bg-blue-800/30'));
|
||||
<div id="logs" class="font-mono max-w-full cursor-default">
|
||||
@foreach (explode("\n", $outputs) as $line)
|
||||
@php
|
||||
// Skip empty lines
|
||||
if (trim($line) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for timestamp at the beginning (ISO 8601 format)
|
||||
$timestampPattern = '/^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z?)\s+/';
|
||||
$hasTimestamp = preg_match($timestampPattern, $line, $matches);
|
||||
$timestamp = $hasTimestamp ? $matches[1] : null;
|
||||
$logContent = $hasTimestamp ? preg_replace($timestampPattern, '', $line) : $line;
|
||||
@endphp
|
||||
<div class="flex items-start gap-2 py-1 px-2 rounded-sm">
|
||||
<div class="w-1 {{ $barColor }} rounded-full flex-shrink-0 self-stretch"></div>
|
||||
<div class="flex-1 {{ $bgColor }} py-1 px-2 -mx-2 rounded-sm">
|
||||
@if ($hasTimestamp)
|
||||
<span
|
||||
class="text-xs text-gray-500 dark:text-gray-400 font-mono mr-2">{{ $timestamp }}</span>
|
||||
<span class="whitespace-pre-wrap break-all">{{ $logContent }}</span>
|
||||
@else
|
||||
<span class="whitespace-pre-wrap break-all">{{ $line }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
// Style timestamps by replacing them inline
|
||||
$styledLine = preg_replace(
|
||||
'/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)/',
|
||||
'<span class="text-gray-500 dark:text-gray-400">$1</span>',
|
||||
htmlspecialchars($line),
|
||||
);
|
||||
@endphp
|
||||
<div
|
||||
class="break-all py-0.5 transition-colors hover:bg-gray-100 dark:hover:bg-coolgray-200">
|
||||
{!! $styledLine !!}
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div id="logs" class="font-mono text-sm py-4 px-2 text-gray-500 dark:text-gray-400">
|
||||
Refresh to get the logs...
|
||||
</div>
|
||||
<pre id="logs" class="font-mono whitespace-pre-wrap break-all max-w-full">Refresh to get the logs...</pre>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,28 +18,28 @@ class="flex flex-col h-full gap-8 sm:flex-row">
|
|||
<div class="flex flex-col gap-1">
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_registration_enabled"
|
||||
helper="If enabled, users can register themselves. If disabled, only administrators can create new users."
|
||||
helper="Allow users to self-register. If disabled, only administrators can create accounts."
|
||||
label="Registration Allowed" />
|
||||
</div>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="do_not_track"
|
||||
helper="If enabled, Coolify will not track any data. This is useful if you are concerned about privacy."
|
||||
helper="Opt out of reporting this instance to coolify.io's installation count. No other data is collected."
|
||||
label="Do Not Track" />
|
||||
</div>
|
||||
<h4 class="pt-4">DNS Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_dns_validation_enabled"
|
||||
helper="If you set a custom domain, Coolify will validate the domain in your DNS provider."
|
||||
helper="Verify that custom domains are correctly configured in DNS before deployment. Prevents deployment failures from DNS misconfigurations."
|
||||
label="DNS Validation" />
|
||||
</div>
|
||||
|
||||
<x-forms.input id="custom_dns_servers" label="Custom DNS Servers"
|
||||
helper="DNS servers to validate domains against. A comma separated list of DNS servers."
|
||||
helper="Custom DNS servers for domain validation. Comma-separated list (e.g., 1.1.1.1,8.8.8.8). Leave empty to use system defaults."
|
||||
placeholder="1.1.1.1,8.8.8.8" />
|
||||
<h4 class="pt-4">API Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_api_enabled" label="API Access"
|
||||
helper="If enabled, the API will be enabled. If disabled, the API will be disabled." />
|
||||
helper="If enabled, authenticated requests to Coolify's REST API will be allowed. Configure API tokens in Security > API Tokens." />
|
||||
</div>
|
||||
<x-forms.input id="allowed_ips" label="Allowed IPs for API Access"
|
||||
helper="Allowed IP addresses or subnets for API access.<br>Supports single IPs (192.168.1.100) and CIDR notation (192.168.1.0/24).<br>Use comma to separate multiple entries.<br>Use 0.0.0.0 or leave empty to allow from anywhere."
|
||||
|
|
@ -53,7 +53,7 @@ class="flex flex-col h-full gap-8 sm:flex-row">
|
|||
<h4 class="pt-4">Confirmation Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id=" is_sponsorship_popup_enabled" label="Show Sponsorship Popup"
|
||||
helper="When enabled, sponsorship popups will be shown monthly to users. When disabled, the sponsorship popup will be permanently hidden for all users." />
|
||||
helper="Show monthly sponsorship reminders to support Coolify development. Disable to hide these messages permanently." />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
|
|
|
|||
|
|
@ -214,3 +214,90 @@
|
|||
expect($notification->servers)->toHaveCount(1);
|
||||
expect($notification->servers->first()->outdatedInfo['type'])->toBe('patch_update');
|
||||
});
|
||||
|
||||
it('notification generates correct server proxy URLs', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'name' => 'Test Server',
|
||||
'team_id' => $team->id,
|
||||
'uuid' => 'test-uuid-123',
|
||||
]);
|
||||
|
||||
$server->outdatedInfo = [
|
||||
'current' => '3.5.0',
|
||||
'latest' => '3.5.6',
|
||||
'type' => 'patch_update',
|
||||
];
|
||||
|
||||
$notification = new TraefikVersionOutdated(collect([$server]));
|
||||
$mail = $notification->toMail($team);
|
||||
|
||||
// Verify the mail has the transformed servers with URLs
|
||||
expect($mail->viewData['servers'])->toHaveCount(1);
|
||||
expect($mail->viewData['servers'][0]['name'])->toBe('Test Server');
|
||||
expect($mail->viewData['servers'][0]['uuid'])->toBe('test-uuid-123');
|
||||
expect($mail->viewData['servers'][0]['url'])->toBe(base_url().'/server/test-uuid-123/proxy');
|
||||
expect($mail->viewData['servers'][0]['outdatedInfo'])->toBeArray();
|
||||
});
|
||||
|
||||
it('notification transforms multiple servers with URLs correctly', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server1 = Server::factory()->create([
|
||||
'name' => 'Server 1',
|
||||
'team_id' => $team->id,
|
||||
'uuid' => 'uuid-1',
|
||||
]);
|
||||
$server1->outdatedInfo = [
|
||||
'current' => '3.5.0',
|
||||
'latest' => '3.5.6',
|
||||
'type' => 'patch_update',
|
||||
];
|
||||
|
||||
$server2 = Server::factory()->create([
|
||||
'name' => 'Server 2',
|
||||
'team_id' => $team->id,
|
||||
'uuid' => 'uuid-2',
|
||||
]);
|
||||
$server2->outdatedInfo = [
|
||||
'current' => '3.4.0',
|
||||
'latest' => '3.6.0',
|
||||
'type' => 'minor_upgrade',
|
||||
'upgrade_target' => 'v3.6',
|
||||
];
|
||||
|
||||
$servers = collect([$server1, $server2]);
|
||||
$notification = new TraefikVersionOutdated($servers);
|
||||
$mail = $notification->toMail($team);
|
||||
|
||||
// Verify both servers have URLs
|
||||
expect($mail->viewData['servers'])->toHaveCount(2);
|
||||
|
||||
expect($mail->viewData['servers'][0]['name'])->toBe('Server 1');
|
||||
expect($mail->viewData['servers'][0]['url'])->toBe(base_url().'/server/uuid-1/proxy');
|
||||
|
||||
expect($mail->viewData['servers'][1]['name'])->toBe('Server 2');
|
||||
expect($mail->viewData['servers'][1]['url'])->toBe(base_url().'/server/uuid-2/proxy');
|
||||
});
|
||||
|
||||
it('notification uses base_url helper not config app.url', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'name' => 'Test Server',
|
||||
'team_id' => $team->id,
|
||||
'uuid' => 'test-uuid',
|
||||
]);
|
||||
|
||||
$server->outdatedInfo = [
|
||||
'current' => '3.5.0',
|
||||
'latest' => '3.5.6',
|
||||
'type' => 'patch_update',
|
||||
];
|
||||
|
||||
$notification = new TraefikVersionOutdated(collect([$server]));
|
||||
$mail = $notification->toMail($team);
|
||||
|
||||
// Verify URL starts with base_url() not config('app.url')
|
||||
$generatedUrl = $mail->viewData['servers'][0]['url'];
|
||||
expect($generatedUrl)->toStartWith(base_url());
|
||||
expect($generatedUrl)->not->toContain('localhost');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -126,6 +126,70 @@
|
|||
expect($result)->toBe('starting:unknown');
|
||||
});
|
||||
|
||||
test('returns degraded:unhealthy for single degraded container', function () {
|
||||
$statuses = collect(['degraded:unhealthy']);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
test('returns degraded:unhealthy when mixing degraded with running healthy', function () {
|
||||
$statuses = collect(['degraded:unhealthy', 'running:healthy']);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
test('returns degraded:unhealthy when mixing running healthy with degraded', function () {
|
||||
$statuses = collect(['running:healthy', 'degraded:unhealthy']);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
test('returns degraded:unhealthy for multiple degraded containers', function () {
|
||||
$statuses = collect(['degraded:unhealthy', 'degraded:unhealthy']);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
test('degraded status overrides all other non-critical states', function () {
|
||||
$statuses = collect(['degraded:unhealthy', 'running:healthy', 'starting', 'paused']);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
test('returns starting:unknown when mixing starting with running healthy (service not fully ready)', function () {
|
||||
$statuses = collect(['starting:unknown', 'running:healthy']);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('starting:unknown');
|
||||
});
|
||||
|
||||
test('returns starting:unknown when mixing created with running healthy', function () {
|
||||
$statuses = collect(['created', 'running:healthy']);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('starting:unknown');
|
||||
});
|
||||
|
||||
test('returns starting:unknown for multiple starting containers with some running', function () {
|
||||
$statuses = collect(['starting:unknown', 'starting:unknown', 'running:healthy']);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('starting:unknown');
|
||||
});
|
||||
|
||||
test('handles parentheses format input (backward compatibility)', function () {
|
||||
$statuses = collect(['running (healthy)', 'running (unhealthy)']);
|
||||
|
||||
|
|
@ -166,8 +230,16 @@
|
|||
expect($result)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
test('prioritizes running over paused/starting/exited', function () {
|
||||
$statuses = collect(['running:healthy', 'starting', 'paused']);
|
||||
test('mixed running and starting returns starting', function () {
|
||||
$statuses = collect(['running:healthy', 'starting']);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('starting:unknown');
|
||||
});
|
||||
|
||||
test('prioritizes running over paused/exited when no starting', function () {
|
||||
$statuses = collect(['running:healthy', 'paused', 'exited']);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
|
|
@ -398,7 +470,23 @@
|
|||
});
|
||||
|
||||
describe('state priority enforcement', function () {
|
||||
test('restarting has highest priority', function () {
|
||||
test('degraded from sub-resources has highest priority', function () {
|
||||
$statuses = collect([
|
||||
'degraded:unhealthy',
|
||||
'restarting',
|
||||
'running:healthy',
|
||||
'dead',
|
||||
'paused',
|
||||
'starting',
|
||||
'exited',
|
||||
]);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
test('restarting has second highest priority', function () {
|
||||
$statuses = collect([
|
||||
'restarting',
|
||||
'running:healthy',
|
||||
|
|
@ -413,7 +501,7 @@
|
|||
expect($result)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
test('crash loop has second highest priority', function () {
|
||||
test('crash loop has third highest priority', function () {
|
||||
$statuses = collect([
|
||||
'exited',
|
||||
'running:healthy',
|
||||
|
|
@ -426,7 +514,7 @@
|
|||
expect($result)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
test('mixed state (running + exited) has third priority', function () {
|
||||
test('mixed state (running + exited) has fourth priority', function () {
|
||||
$statuses = collect([
|
||||
'running:healthy',
|
||||
'exited',
|
||||
|
|
@ -439,6 +527,18 @@
|
|||
expect($result)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
test('mixed state (running + starting) has fifth priority', function () {
|
||||
$statuses = collect([
|
||||
'running:healthy',
|
||||
'starting',
|
||||
'paused',
|
||||
]);
|
||||
|
||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||
|
||||
expect($result)->toBe('starting:unknown');
|
||||
});
|
||||
|
||||
test('running:unhealthy has priority over running:unknown', function () {
|
||||
$statuses = collect([
|
||||
'running:unknown',
|
||||
|
|
|
|||
Loading…
Reference in a new issue