Merge remote-tracking branch 'origin/main' into release/v4.5

This commit is contained in:
github-actions[bot]
2026-10-08 11:49:14 +00:00
5 changed files with 86 additions and 8 deletions
+13 -1
View File
@@ -7,20 +7,32 @@ use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue
class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldBeUniqueUntilProcessing, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Only one sync per resource can wait in the queue. The compose parser dispatches
* this job once per volume on every parse, so without this the queue fills with copies.
*/
public int $uniqueFor = 600;
public function __construct(public ServiceApplication|ServiceDatabase|Application $resource)
{
$this->onQueue('high');
}
public function uniqueId(): string
{
return $this->resource::class.':'.$this->resource->id;
}
public function handle()
{
$this->resource->getFilesFromServer(isInit: true);
+12 -6
View File
@@ -178,11 +178,11 @@ class Domains extends Component
$this->loadDomainState();
}
public function refreshDomains(): void
public function refreshDomains(bool $parseCompose = true): void
{
$editingRow = $this->editingIndex !== null ? ($this->domainRows[$this->editingIndex] ?? null) : null;
$this->loadDomainState();
$this->loadDomainState($parseCompose);
if ($editingRow !== null) {
$index = collect($this->domainRows)->search(fn (array $row): bool => $row['url'] === $editingRow['url']
@@ -199,7 +199,9 @@ class Domains extends Component
->where('dns_status', 'checking')
->values();
$this->refreshDomains();
// The poll only refreshes DNS results. Parsing the compose file here would
// queue file sync jobs on every tick and starve the DNS check jobs.
$this->refreshDomains(parseCompose: false);
foreach ($checkingRows as $checkingRow) {
$row = collect($this->domainRows)->first(fn (array $row): bool => $row['url'] === $checkingRow['url']
@@ -258,7 +260,7 @@ class Domains extends Component
$this->dispatch('success', 'HTTP to HTTPS redirect updated.');
}
public function loadDomainState(): void
public function loadDomainState(bool $parseCompose = true): void
{
$this->application->refresh();
$this->application->loadMissing(['destination.server', 'settings', 'additional_servers']);
@@ -293,23 +295,27 @@ class Domains extends Component
$this->serverIpConfigured = null;
}
$this->composeServices = [];
$pendingRedirect = $this->pendingRedirectService !== null
? ($this->serviceRedirects[$this->serviceRedirectWireKey($this->pendingRedirectService)] ?? null)
: null;
$this->serviceRedirects = [];
if ($this->isCompose) {
if (! $this->isCompose) {
$this->composeServices = [];
} elseif ($parseCompose) {
try {
$parsed = $this->application->parse() ?? [];
} catch (\Throwable) {
$parsed = [];
}
$this->composeServices = [];
$services = data_get($parsed, 'services', []);
foreach ($services as $serviceName => $service) {
if (! isDatabaseImage(data_get($service, 'image'))) {
$this->composeServices[] = $serviceName;
}
}
}
if ($this->isCompose) {
if ($this->newDomainService === null && count($this->composeServices) > 0) {
$this->newDomainService = $this->composeServices[0];
}
+18 -1
View File
@@ -83,6 +83,23 @@ export function isFindShortcut(event) {
return Boolean(event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey && event.key?.toLowerCase() === 'f';
}
const DEFAULT_LOG_FILTERS = { error: true, warning: true, debug: true, info: true };
/** Read saved level filters. Invalid saved values fall back to the defaults. */
export function readLogFilters(saved) {
const filters = { ...DEFAULT_LOG_FILTERS };
let parsed;
try {
parsed = JSON.parse(saved);
} catch {
return filters;
}
for (const level of Object.keys(filters)) {
if (typeof parsed?.[level] === 'boolean') filters[level] = parsed[level];
}
return filters;
}
export function filterLogLines(lines, filters, query) {
const needle = query.trim().toLowerCase();
return lines.filter((line) => filters[line.level] !== false && (!needle || line.lower.includes(needle)));
@@ -170,7 +187,7 @@ export function initializeRuntimeLogsComponent() {
scrollDebounce: null,
destroyed: false,
colorLogs: localStorage.getItem('coolify-color-logs') === 'true',
logFilters: JSON.parse(localStorage.getItem('coolify-log-filters')) || { error: true, warning: true, debug: true, info: true },
logFilters: readLogFilters(localStorage.getItem('coolify-log-filters')),
searchQuery: '',
appliedQuery: '',
matchCount: 0,
+23
View File
@@ -2,6 +2,7 @@
use App\Jobs\CheckDomainDnsJob;
use App\Jobs\ConfigureDnsRecordJob;
use App\Jobs\ServerFilesFromServerJob;
use App\Livewire\Project\Application\Domains;
use App\Livewire\Project\Application\PreviewDomains;
use App\Livewire\Project\Application\Previews;
@@ -3656,3 +3657,25 @@ it('offers dns provider record creation for a public server ip', function () {
->toContain('Add with Cloudflare')
->not->toContain('The server has no public IP address; add the DNS record manually.');
});
it('does not queue file sync jobs when polling dns checks for compose applications', function () {
Queue::fake();
$this->application->update([
'build_pack' => 'dockercompose',
'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n volumes:\n - web-data:/data\n api:\n image: nginx:alpine\n volumes:\n - api-data:/data\n - api-conf:/etc/api\nvolumes:\n web-data:\n api-data:\n api-conf:\n",
'docker_compose_domains' => json_encode([
'web' => ['domain' => 'https://compose-poll-test.invalid'],
]),
'fqdn' => null,
]);
$component = Livewire::test(Domains::class, ['application' => $this->application->fresh()]);
$queuedOnMount = Queue::pushed(ServerFilesFromServerJob::class)->count();
$component->call('pollDnsChecks')->call('pollDnsChecks')->call('pollDnsChecks');
expect($queuedOnMount)->toBe(1)
->and(Queue::pushed(ServerFilesFromServerJob::class))->toHaveCount(1)
->and($component->get('composeServices'))->toBe(['web', 'api']);
});
@@ -124,3 +124,23 @@ it('streams only new lines and appends them to the loaded log', function () {
$page->screenshot(filename: 'runtime-logs-virtualized-stream');
});
it('falls back to the default level filters when the saved filters are invalid', function () {
$page = visit('/login')
->fill('email', 'test@example.com')
->fill('password', 'password')
->click('Login')
->assertSee('Dashboard');
$page->script('localStorage.setItem("coolify-log-filters", "{")');
$page->navigate("/server/{$this->stack['server']->uuid}/sentinel/logs")
->assertSee('log line 00001');
// A valid saved preference is still applied.
$page->script('localStorage.setItem("coolify-log-filters", JSON.stringify({ info: false }))');
$page->navigate("/server/{$this->stack['server']->uuid}/sentinel/logs")
->wait(1)
->assertDontSee('log line 00001');
$page->screenshot(filename: 'runtime-logs-invalid-saved-filters');
});