Merge branch 'next' into feat/healthcheck-cmd

This commit is contained in:
Aditya Tripathi authored and GitHub committed 2026-02-21 06:54:29 +05:30
commit 04283a03a0
192 files changed
+16780 -10634

No files matched your search

+12 -2
View File
@@ -96,7 +96,17 @@ body {
}
body {
@apply min-h-screen text-sm antialiased scrollbar;
@apply min-h-screen text-sm antialiased scrollbar overflow-x-hidden;
}
.coolify-monaco-editor {
@apply min-w-0 w-full;
overflow-x: hidden;
}
.coolify-monaco-editor .monaco-editor,
.coolify-monaco-editor .overflow-guard {
max-width: 100%;
}
option {
@@ -196,4 +206,4 @@ section {
.dark .log-highlight {
background-color: rgba(234, 179, 8, 0.3);
}
}
+1 -1
View File
@@ -281,7 +281,7 @@
}
@utility info-helper-popup {
@apply hidden absolute z-40 text-xs rounded-sm text-neutral-700 group-hover:block dark:border-coolgray-500 border-neutral-900 dark:bg-coolgray-400 bg-neutral-200 dark:text-neutral-300 max-w-xs whitespace-normal break-words;
@apply hidden absolute z-40 text-xs rounded-sm text-neutral-700 group-hover:block dark:border-coolgray-500 border-neutral-900 dark:bg-coolgray-400 bg-neutral-200 dark:text-neutral-300 max-w-sm whitespace-normal break-words;
}
@utility buyme {
+5 -3
View File
@@ -1,7 +1,9 @@
<?php
function getOldOrLocal($key, $localValue)
{
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
if (! function_exists('getOldOrLocal')) {
function getOldOrLocal($key, $localValue)
{
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
}
}
$name = getOldOrLocal('name', 'test3 normal user');
@@ -99,8 +99,14 @@
{{-- Unified Input Container with Tags Inside --}}
<div @click="$refs.searchInput.focus()" x-data="{ focused: false }" @focusin="focused = true" @focusout="focused = false"
class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 px-2 w-full text-sm rounded-sm border-0 bg-white dark:bg-coolgray-100 cursor-text px-1 text-black dark:text-white"
:style="focused ? 'box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;' : 'box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px #e5e5e5;'"
x-init="$watch('focused', () => { if ($root.classList.contains('dark') || document.documentElement.classList.contains('dark')) { $el.style.boxShadow = focused ? 'inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424' : 'inset 4px 0 0 transparent, inset 0 0 0 2px #242424'; } })"
:style="(() => {
const isDark = document.documentElement.classList.contains('dark');
const accent = isDark ? '#fcd452' : '#6b16ed';
const border = isDark ? '#242424' : '#e5e5e5';
return focused
? 'box-shadow: inset 4px 0 0 ' + accent + ', inset 0 0 0 2px ' + border + ';'
: 'box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px ' + border + ';';
})()"
:class="{
'opacity-50': {{ $disabled ? 'true' : 'false' }}
}" wire:loading.class="opacity-50"
@@ -225,8 +231,14 @@
{{-- Input Container --}}
<div @click="openDropdown()" x-data="{ focused: false }" @focusin="focused = true" @focusout="focused = false"
class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 bg-white dark:bg-coolgray-100 cursor-text text-black dark:text-white"
:style="focused ? 'box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;' : 'box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px #e5e5e5;'"
x-init="$watch('focused', () => { if ($root.classList.contains('dark') || document.documentElement.classList.contains('dark')) { $el.style.boxShadow = focused ? 'inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424' : 'inset 4px 0 0 transparent, inset 0 0 0 2px #242424'; } })"
:style="(() => {
const isDark = document.documentElement.classList.contains('dark');
const accent = isDark ? '#fcd452' : '#6b16ed';
const border = isDark ? '#242424' : '#e5e5e5';
return focused
? 'box-shadow: inset 4px 0 0 ' + accent + ', inset 0 0 0 2px ' + border + ';'
: 'box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px ' + border + ';';
})()"
:class="{
'opacity-50': {{ $disabled ? 'true' : 'false' }}
}" wire:loading.class="opacity-50" wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]">
@@ -7,8 +7,6 @@
'action' => 'delete',
'content' => null,
'closeOutside' => true,
'minWidth' => '36rem',
'maxWidth' => '48rem',
'isFullWidth' => false,
])
@@ -16,7 +14,6 @@
$modalId = 'modal-' . uniqid();
@endphp
<div x-data="{ modalOpen: false }"
x-init="$watch('modalOpen', value => { if (!value) { $wire.dispatch('modalClosed') } })"
:class="{ 'z-40': modalOpen }" @keydown.window.escape="modalOpen=false"
@@ -52,7 +49,7 @@
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full min-w-full lg:min-w-[{{ $minWidth }}] max-w-[{{ $maxWidth }}] max-h-[calc(100vh-2rem)] border rounded-sm drop-shadow-sm bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">
class="relative w-full lg:w-auto lg:min-w-2xl lg:max-w-4xl border rounded-sm drop-shadow-sm bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">
<div class="flex items-center justify-between py-6 px-6 shrink-0">
<h3 class="text-2xl font-bold">{{ $title }}</h3>
<button @click="modalOpen=false"
@@ -63,7 +60,7 @@
</svg>
</button>
</div>
<div class="relative flex items-center justify-center w-auto overflow-y-auto px-6 pb-6" style="-webkit-overflow-scrolling: touch;">
<div class="relative flex items-center justify-center w-auto px-6 pb-6">
{{ $slot }}
</div>
</div>
@@ -2,7 +2,7 @@
<h1>Notifications</h1>
<div class="subtitle">Get notified about your infrastructure.</div>
<div class="navbar-main">
<nav class="flex items-center gap-6 min-h-10">
<nav class="flex items-center gap-3.5 min-h-10">
<a class="{{ request()->routeIs('notifications.email') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('notifications.email') }}">
<button>Email</button>
@@ -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="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 @@
),
)
->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 @@
]),
};
$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 @@
$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,
@@ -19,6 +19,10 @@
href="{{ route('settings.oauth') }}">
OAuth
</a>
<a class="{{ request()->routeIs('settings.scheduled-jobs') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('settings.scheduled-jobs') }}">
Scheduled Jobs
</a>
<div class="flex-1"></div>
</nav>
</div>
@@ -699,15 +699,21 @@
class="search-result-item w-full text-left block px-4 py-3 hover:bg-warning-50 dark:hover:bg-warning-900/20 transition-colors focus:outline-none focus:bg-warning-100 dark:focus:bg-warning-900/30 border-transparent hover:border-warning-500 focus:border-warning-500">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3 flex-1 min-w-0">
<div
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-warning-600 dark:text-warning-400" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" />
</svg>
</div>
@if (! empty($item['logo']))
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center overflow-hidden">
<img src="{{ asset($item['logo']) }}" alt="{{ $item['name'] }}" class="w-7 h-7 object-contain">
</div>
@else
<div
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-warning-600 dark:text-warning-400" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" />
</svg>
</div>
@endif
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<div class="font-medium text-neutral-900 dark:text-white truncate">
@@ -808,15 +814,22 @@
class="search-result-item w-full text-left block px-4 py-3 hover:bg-warning-50 dark:hover:bg-warning-900/20 transition-colors focus:outline-none focus:bg-warning-100 dark:focus:bg-warning-900/30 border-transparent hover:border-warning-500 focus:border-warning-500">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3 flex-1 min-w-0">
<div
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-warning-600 dark:text-warning-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</div>
<template x-if="item.logo">
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center overflow-hidden">
<img :src="'/' + item.logo" :alt="item.name" class="w-7 h-7 object-contain">
</div>
</template>
<template x-if="!item.logo">
<div
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-warning-600 dark:text-warning-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</div>
</template>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<div class="font-medium text-neutral-900 dark:text-white truncate"
@@ -1,7 +1,7 @@
<nav wire:poll.10000ms="checkStatus" class="pb-6">
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
<div class="navbar-main">
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
<nav class="flex shrink-0 gap-4 items-center whitespace-nowrap scrollbar min-h-10">
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.application.configuration', $parameters) }}">
Configuration
@@ -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>
@@ -448,14 +448,56 @@
<h2>Select a Postgresql type</h2>
<div>If you need extra extensions, you can select Supabase PostgreSQL (or others), otherwise select
PostgreSQL
17 (default).</div>
<div class="flex flex-col gap-6 pt-8">
18 (default).</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-8">
<div class="gap-2 coolbox group flex relative"
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('postgres:18-alpine'))"
:disabled="selecting">
<div class="flex flex-col">
<div class="box-title">PostgreSQL 18 (default)</div>
<div class="box-description">
PostgreSQL is a powerful, open-source object-relational database system (no extensions).
</div>
</div>
<a href="https://hub.docker.com/_/postgres/" target="_blank"
@click.stop
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
title="View documentation">
<svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</a>
</div>
<div class="gap-2 coolbox group flex relative"
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('postgres:17-alpine'))"
:disabled="selecting">
<div class="flex flex-col">
<div class="box-title">PostgreSQL 17 (default)</div>
<div class="box-title">PostgreSQL 17</div>
<div class="box-description">
PostgreSQL is a powerful, open-source object-relational database system (no extensions).
</div>
</div>
<a href="https://hub.docker.com/_/postgres/" target="_blank"
@click.stop
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
title="View documentation">
<svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</a>
</div>
<div class="gap-2 coolbox group flex relative"
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('postgres:16-alpine'))"
:disabled="selecting">
<div class="flex flex-col">
<div class="box-title">PostgreSQL 16</div>
<div class="box-description">
PostgreSQL is a powerful, open-source object-relational database system (no extensions).
</div>
@@ -513,6 +555,27 @@
</svg>
</a>
</div>
<div class="gap-2 coolbox group flex relative"
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('pgvector/pgvector:pg18'))"
:disabled="selecting">
<div class="flex flex-col">
<div class="box-title">PGVector (18)</div>
<div class="box-description">
PGVector is a PostgreSQL extension for vector data types.
</div>
</div>
<a href="https://github.com/pgvector/pgvector" target="_blank"
@click.stop
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
title="View documentation">
<svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</a>
</div>
<div class="gap-2 coolbox group flex relative"
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('pgvector/pgvector:pg17'))"
@@ -3,7 +3,7 @@
{{ data_get_str($project, 'name')->limit(10) }} > Resources | Coolify
</x-slot>
<div class="flex flex-col">
<div class="flex items-center gap-2">
<div class="flex min-w-0 flex-nowrap items-center gap-1">
<h1>Resources</h1>
@if ($environment->isEmpty())
@can('createAnyResource')
@@ -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 @@
</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 @@
<!-- 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 @@
),
)
->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 @@
'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');
@@ -71,8 +71,7 @@
@if ($application->fqdn)
<span class="flex gap-1 text-xs">{{ Str::limit($application->fqdn, 60) }}
@can('update', $service)
<x-modal-input title="Edit Domains" :closeOutside="false" minWidth="32rem"
maxWidth="40rem">
<x-modal-input title="Edit Domains" :closeOutside="false">
<x-slot:content>
<span class="cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg"
@@ -9,7 +9,7 @@
<h1>{{ $title }}</h1>
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
<div class="navbar-main" x-data">
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
<nav class="flex shrink-0 gap-4 items-center whitespace-nowrap scrollbar min-h-10">
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button>
@@ -62,7 +62,7 @@
<div class="subtitle">{{ data_get($server, 'name') }}</div>
<div class="navbar-main">
<nav
class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2">
class="flex items-center gap-4 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2">
<a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}" href="{{ route('server.show', [
'server_uuid' => data_get($server, 'uuid'),
]) }}" {{ wireNavigate() }}>
@@ -197,4 +197,4 @@
</div>
</div>
</div>
</div>
</div>
@@ -18,8 +18,10 @@
<div class="flex flex-col gap-2">
<div class="flex flex-wrap items-end gap-2">
<div class="flex gap-2 md:flex-row flex-col w-full">
<x-forms.input canGate="update" :canResource="$settings" id="fqdn" label="Domain"
helper="Enter the full domain name (FQDN) of the instance, including 'https://' if you want to secure the dashboard with HTTPS. Setting this will make the dashboard accessible via this domain, secured by HTTPS, instead of just the IP address."
<x-forms.input canGate="update" :canResource="$settings" id="fqdn" label="URL"
helper="Enter the full URL of the instance (for example, https://dashboard.example.com).<br><br>
<span class='dark:text-warning text-coollabs'>Important: </span>
If you want the dashboard to be accessible over HTTPS, you must include <b>https://</b> at the start of the URL. Without it, the dashboard will use HTTP and won’t be secured."
placeholder="https://coolify.yourdomain.com" />
<x-forms.input canGate="update" :canResource="$settings" id="instance_name" label="Name" placeholder="Coolify"
helper="Custom name for your Coolify instance, shown in the URL." />
@@ -0,0 +1,260 @@
<div>
<x-slot:title>
Scheduled Job Issues | Coolify
</x-slot>
<x-settings.navbar />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'executions' }"
class="flex flex-col gap-8">
<div>
<div class="flex items-center gap-2">
<h2>Scheduled Job Issues</h2>
<x-forms.button wire:click="refresh">Refresh</x-forms.button>
</div>
<div class="pb-4">Shows failed executions, skipped jobs, and scheduler health.</div>
</div>
{{-- Tab Buttons --}}
<div class="flex flex-row gap-4">
<div @class([
'box-without-bg cursor-pointer dark:bg-coolgray-100 dark:text-white w-full text-center items-center justify-center',
])
:class="activeTab === 'executions' && 'dark:bg-coollabs bg-coollabs text-white'"
@click="activeTab = 'executions'; window.location.hash = 'executions'">
Failures ({{ $executions->count() }})
</div>
<div @class([
'box-without-bg cursor-pointer dark:bg-coolgray-100 dark:text-white w-full text-center items-center justify-center',
])
:class="activeTab === 'scheduler-runs' && 'dark:bg-coollabs bg-coollabs text-white'"
@click="activeTab = 'scheduler-runs'; window.location.hash = 'scheduler-runs'">
Scheduler Runs ({{ $managerRuns->count() }})
</div>
<div @class([
'box-without-bg cursor-pointer dark:bg-coolgray-100 dark:text-white w-full text-center items-center justify-center',
])
:class="activeTab === 'skipped-jobs' && 'dark:bg-coollabs bg-coollabs text-white'"
@click="activeTab = 'skipped-jobs'; window.location.hash = 'skipped-jobs'">
Skipped Jobs ({{ $skipLogs->count() }})
</div>
</div>
{{-- Executions Tab --}}
<div x-show="activeTab === 'executions'" x-cloak>
{{-- Filters --}}
<div class="flex gap-4 flex-wrap mb-4">
<div class="flex flex-col gap-1">
<label class="text-sm font-medium">Type</label>
<select wire:model.live="filterType"
class="w-40 border bg-white dark:bg-coolgray-100 border-gray-300 dark:border-coolgray-400 rounded-md text-sm">
<option value="all">All Types</option>
<option value="backup">Backups</option>
<option value="task">Tasks</option>
<option value="cleanup">Docker Cleanup</option>
</select>
</div>
<div class="flex flex-col gap-1">
<label class="text-sm font-medium">Time Range</label>
<select wire:model.live="filterDate"
class="w-40 border bg-white dark:bg-coolgray-100 border-gray-300 dark:border-coolgray-400 rounded-md text-sm">
<option value="last_24h">Last 24 Hours</option>
<option value="last_7d">Last 7 Days</option>
<option value="last_30d">Last 30 Days</option>
<option value="all">All Time</option>
</select>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="text-xs uppercase bg-gray-50 dark:bg-coolgray-200">
<tr>
<th class="px-4 py-3">Type</th>
<th class="px-4 py-3">Resource</th>
<th class="px-4 py-3">Server</th>
<th class="px-4 py-3">Started</th>
<th class="px-4 py-3">Duration</th>
<th class="px-4 py-3">Message</th>
</tr>
</thead>
<tbody>
@forelse($executions as $execution)
<tr wire:key="exec-{{ $execution['type'] }}-{{ $execution['id'] }}"
class="border-b border-gray-200 dark:border-coolgray-400 hover:bg-gray-50 dark:hover:bg-coolgray-200">
<td class="px-4 py-3">
@php
$typeLabel = match($execution['type']) {
'backup' => 'Backup',
'task' => 'Task',
'cleanup' => 'Cleanup',
default => ucfirst($execution['type']),
};
$typeBg = match($execution['type']) {
'backup' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
'task' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
'cleanup' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
default => 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300',
};
@endphp
<span class="px-2 py-1 rounded-md text-xs font-medium {{ $typeBg }}">
{{ $typeLabel }}
</span>
</td>
<td class="px-4 py-3">
{{ $execution['resource_name'] }}
@if($execution['resource_type'])
<span class="text-xs text-gray-500">({{ $execution['resource_type'] }})</span>
@endif
</td>
<td class="px-4 py-3">{{ $execution['server_name'] }}</td>
<td class="px-4 py-3 whitespace-nowrap">
{{ $execution['created_at']->diffForHumans() }}
<span class="block text-xs text-gray-500">{{ $execution['created_at']->format('M d H:i') }}</span>
</td>
<td class="px-4 py-3 whitespace-nowrap">
@if($execution['finished_at'] && $execution['created_at'])
{{ \Carbon\Carbon::parse($execution['created_at'])->diffInSeconds(\Carbon\Carbon::parse($execution['finished_at'])) }}s
@elseif($execution['status'] === 'running')
<x-loading class="w-4 h-4" />
@else
-
@endif
</td>
<td class="px-4 py-3 max-w-xs truncate" title="{{ $execution['message'] }}">
{{ \Illuminate\Support\Str::limit($execution['message'], 80) }}
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-8 text-center text-gray-500">
No failures found for the selected filters.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
{{-- Scheduler Runs Tab --}}
<div x-show="activeTab === 'scheduler-runs'" x-cloak>
<div class="pb-4 text-sm text-gray-500">Shows when the ScheduledJobManager executed. Gaps indicate lock conflicts or missed runs.</div>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="text-xs uppercase bg-gray-50 dark:bg-coolgray-200">
<tr>
<th class="px-4 py-3">Time</th>
<th class="px-4 py-3">Event</th>
<th class="px-4 py-3">Duration</th>
<th class="px-4 py-3">Dispatched</th>
<th class="px-4 py-3">Skipped</th>
</tr>
</thead>
<tbody>
@forelse($managerRuns as $run)
<tr wire:key="run-{{ $loop->index }}"
class="border-b border-gray-200 dark:border-coolgray-400">
<td class="px-4 py-2 whitespace-nowrap text-xs">{{ $run['timestamp'] }}</td>
<td class="px-4 py-2">{{ $run['message'] }}</td>
<td class="px-4 py-2">
@if($run['duration_ms'] !== null)
{{ $run['duration_ms'] }}ms
@else
-
@endif
</td>
<td class="px-4 py-2">{{ $run['dispatched'] ?? '-' }}</td>
<td class="px-4 py-2">
@if(($run['skipped'] ?? 0) > 0)
<span class="text-warning">{{ $run['skipped'] }}</span>
@else
{{ $run['skipped'] ?? '-' }}
@endif
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-4 py-4 text-center text-gray-500">
No scheduler run logs found. Logs appear after the ScheduledJobManager runs.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
{{-- Skipped Jobs Tab --}}
<div x-show="activeTab === 'skipped-jobs'" x-cloak>
<div class="pb-4 text-sm text-gray-500">Jobs that were not dispatched because conditions were not met.</div>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="text-xs uppercase bg-gray-50 dark:bg-coolgray-200">
<tr>
<th class="px-4 py-3">Time</th>
<th class="px-4 py-3">Type</th>
<th class="px-4 py-3">Reason</th>
<th class="px-4 py-3">Details</th>
</tr>
</thead>
<tbody>
@forelse($skipLogs as $skip)
<tr wire:key="skip-{{ $loop->index }}"
class="border-b border-gray-200 dark:border-coolgray-400">
<td class="px-4 py-2 whitespace-nowrap text-xs">{{ $skip['timestamp'] }}</td>
<td class="px-4 py-2">
@php
$skipTypeBg = match($skip['type']) {
'backup' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
'task' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
'docker_cleanup' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
default => 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300',
};
@endphp
<span class="px-2 py-1 rounded-md text-xs font-medium {{ $skipTypeBg }}">
{{ ucfirst(str_replace('_', ' ', $skip['type'])) }}
</span>
</td>
<td class="px-4 py-2">
@php
$reasonLabel = match($skip['reason']) {
'server_not_functional' => 'Server not functional',
'subscription_unpaid' => 'Subscription unpaid',
'database_deleted' => 'Database deleted',
'server_deleted' => 'Server deleted',
'resource_deleted' => 'Resource deleted',
'application_not_running' => 'Application not running',
'service_not_running' => 'Service not running',
default => ucfirst(str_replace('_', ' ', $skip['reason'])),
};
$reasonBg = match($skip['reason']) {
'server_not_functional', 'database_deleted', 'server_deleted', 'resource_deleted' => 'text-red-600 dark:text-red-400',
'subscription_unpaid' => 'text-warning',
'application_not_running', 'service_not_running' => 'text-orange-600 dark:text-orange-400',
default => '',
};
@endphp
<span class="{{ $reasonBg }}">{{ $reasonLabel }}</span>
</td>
<td class="px-4 py-2 text-xs text-gray-500">
@php
$details = collect($skip['context'])
->except(['type', 'skip_reason', 'execution_time'])
->map(fn($v, $k) => str_replace('_', ' ', $k) . ': ' . $v)
->implode(', ');
@endphp
{{ $details }}
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-4 py-4 text-center text-gray-500">
No skipped jobs found. This means all scheduled jobs passed their conditions.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>