diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index 6350a5f37..cec90288e 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -23,13 +23,10 @@ class StartSentinel $refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds'); $pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds'); $token = $server->settings->ensureValidSentinelToken(); - $endpoint = data_get($server, 'settings.sentinel_custom_url'); + $endpoint = $server->settings->ensureSentinelUrl(); $debug = data_get($server, 'settings.is_sentinel_debug_enabled'); $mountDir = '/data/coolify/sentinel'; $image = coolifyRegistryUrl().'/coollabsio/sentinel:'.$version; - if (! $endpoint) { - throw new \RuntimeException('You should set FQDN in Instance Settings.'); - } $environments = [ 'TOKEN' => $token, 'DEBUG' => $debug ? 'true' : 'false', diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php index 0f5c739b1..46a8ecdc8 100644 --- a/app/Livewire/Project/Service/EditCompose.php +++ b/app/Livewire/Project/Service/EditCompose.php @@ -77,7 +77,6 @@ class EditCompose extends Component { try { $this->authorize('update', $this->service); - $this->dispatch('info', 'Saving new docker compose...'); $this->dispatch('saveCompose', $this->dockerComposeRaw); $this->dispatch('refreshStorages'); } catch (\Throwable $e) { diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 86d5a57c1..92829edd6 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -140,6 +140,7 @@ class StackForm extends Component { $this->dockerComposeRaw = $raw; $this->submit(notify: true); + $this->dispatch('compose-save-finished'); } public function instantSave() diff --git a/app/Livewire/Server/Sentinel.php b/app/Livewire/Server/Sentinel.php index 909ed54f9..cd05002aa 100644 --- a/app/Livewire/Server/Sentinel.php +++ b/app/Livewire/Server/Sentinel.php @@ -114,9 +114,10 @@ class Sentinel extends Component return; } - $this->isSentinelEnabled = true; $customImage = isDev() ? $this->sentinelCustomDockerImage : null; StartSentinel::run($this->server, true, null, $customImage); + $this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url; + $this->isSentinelEnabled = true; } else { $this->isSentinelEnabled = false; $this->isMetricsEnabled = false; diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index c7181ebcf..9e6108de0 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -39,6 +39,8 @@ class ValidateAndInstall extends Component public bool $ask = false; + public bool $isInstalling = false; + protected $listeners = [ 'init', 'validateConnection', @@ -51,6 +53,7 @@ class ValidateAndInstall extends Component public function init(int $data = 0) { + $this->isInstalling = false; $this->uptime = null; $this->supported_os_type = null; $this->prerequisites_installed = null; @@ -172,6 +175,7 @@ class ValidateAndInstall extends Component if ($this->number_of_tries <= $this->max_tries) { $this->installationStep = 'Prerequisites'; $activity = $this->server->installPrerequisites(); + $this->isInstalling = true; $this->number_of_tries++; $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs"); } @@ -208,6 +212,7 @@ class ValidateAndInstall extends Component if ($this->number_of_tries <= $this->max_tries) { $this->installationStep = 'Docker'; $activity = $this->server->installDocker(); + $this->isInstalling = true; $this->number_of_tries++; $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs"); } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 0453dc793..93f040e4a 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -219,7 +219,22 @@ class ServerSetting extends Model return $token; } - public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false) + public function ensureSentinelUrl(): string + { + $url = $this->sentinel_custom_url; + + if (blank($url)) { + $url = $this->generateSentinelUrl(ignoreEvent: true); + } + + if (blank($url)) { + throw new \RuntimeException('Set an instance FQDN, public IP, or reachable Coolify URL before enabling Sentinel.'); + } + + return $url; + } + + public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false): ?string { $domain = null; $settings = InstanceSettings::get(); @@ -231,6 +246,8 @@ class ServerSetting extends Model $domain = 'http://'.$settings->public_ipv4.':8000'; } elseif ($settings->public_ipv6) { $domain = 'http://'.$settings->public_ipv6.':8000'; + } else { + $domain = $this->sentinelUrlFromCurrentRequest(); } $this->sentinel_custom_url = $domain; if ($save) { @@ -244,6 +261,29 @@ class ServerSetting extends Model return $domain; } + private function sentinelUrlFromCurrentRequest(): ?string + { + if (! app()->bound('request')) { + return null; + } + + $request = request(); + $host = strtolower($request->getHost()); + + if ( + $host === 'localhost' || + str_ends_with($host, '.localhost') || + $host === '::1' || + $host === '::' || + $host === '0.0.0.0' || + str_starts_with($host, '127.') + ) { + return null; + } + + return $request->getSchemeAndHttpHost(); + } + public function server() { return $this->belongsTo(Server::class); diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 84522a5e1..fee3f376f 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -253,7 +253,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d } catch (Exception) { $timestamp->setTimezone('UTC'); } - data_set($i, 'timestamp', $timestamp->format('Y-M-d H:i:s.u')); + data_set($i, 'timestamp', $timestamp->format('Y-M-d H:i:s')); return $i; }) diff --git a/resources/css/app.css b/resources/css/app.css index 400b4a9a0..98e7d3899 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -872,7 +872,7 @@ html[data-theme="custom"] { --theme-bright-color: color-mix(in srgb, var(--theme-base-color) 85%, white); --theme-scrollbar-thumb: color-mix(in srgb, var(--theme-bright-color) 70%, var(--theme-accent-foreground)); --theme-border-color: color-mix(in oklab, var(--theme-base-color) 42%, #52525b); - --theme-placeholder-color: color-mix(in srgb, white 82%, var(--theme-base-color)); + --theme-placeholder-color: color-mix(in srgb, white 20%, var(--theme-base-color)); --color-accent: var(--theme-bright-color); --color-coollabs: var(--theme-bright-color); --color-coollabs-100: color-mix(in oklab, var(--theme-bright-color) 88%, white); @@ -966,7 +966,7 @@ html[data-theme="custom"] [class~="dark:bg-white/[0.025]"] { html[data-theme="custom"] input::placeholder, html[data-theme="custom"] textarea::placeholder { color: var(--theme-placeholder-color) !important; - opacity: 1; + opacity: 0.7; } html[data-theme="custom"] input:read-only, @@ -1430,8 +1430,10 @@ html[data-theme="custom"] textarea:disabled { /* Inputs & selects: recessed fill + line border (surface hierarchy) */ .application-settings-workspace .input, .application-settings-workspace .select, +.application-settings-workspace .listbox-trigger, .application-settings-form .input, -.application-settings-form .select { +.application-settings-form .select, +.application-settings-form .listbox-trigger { height: 2rem; border-radius: 8px; border-color: var(--coollabs-line); @@ -1741,7 +1743,7 @@ html[data-theme="custom"] textarea:disabled { gap: 0.5rem; width: 100%; min-width: 0; - height: 2rem; + height: 2.25rem; padding: 0 0.625rem 0 0.75rem; overflow: hidden; border-radius: 8px; @@ -2441,7 +2443,9 @@ input[type="search"]::-webkit-search-results-decoration { } .backup-executions-table-grid { - grid-template-columns: 7.5rem minmax(9rem, 1fr) 8rem 6rem 5rem minmax(9rem, 1fr) minmax(9rem, auto); + grid-template-columns: 6.5rem minmax(7rem, 1fr) 7rem 5rem 4rem minmax(8rem, 1fr) 5rem; + gap: 0.75rem; + min-width: 49rem; } .volume-backup-executions-grid { diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 25867e500..bdcc3824d 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -8,7 +8,14 @@ import { } from './terminal-session-timer.js'; import { FitAddon } from '@xterm/addon-fit'; -const terminalDebugEnabled = import.meta.env.DEV; +const terminalDebugParameter = new URLSearchParams(window.location.search).get('terminal-debug'); + +if (terminalDebugParameter === '1' || terminalDebugParameter === '0') { + localStorage.setItem('coolify-terminal-debug', terminalDebugParameter); +} + +const terminalDebugEnabled = import.meta.env.DEV + || localStorage.getItem('coolify-terminal-debug') === '1'; const baseApplicationTerminalTheme = { black: '#675f70', @@ -393,9 +400,15 @@ export function initializeTerminalComponent() { setTerminalTheme(themeName) { if (!applicationTerminalThemes[themeName]) { + logTerminal('warn', '[Terminal Theme] Unknown theme', { + requestedTheme: themeName, + availableThemes: Object.keys(applicationTerminalThemes), + }); return; } + logTerminal('log', '[Terminal Theme] Applying theme', this.terminalThemeDebugSnapshot(themeName)); + if (themeName === 'system') { applicationTerminalThemes.system = createSystemTerminalTheme(); } @@ -417,10 +430,36 @@ export function initializeTerminalComponent() { this.term.options.cursorBlink = cursorBlink; this.term.refresh(0, Math.max(0, this.term.rows - 1)); this.term.focus(); + logTerminal('log', '[Terminal Theme] Theme applied', this.terminalThemeDebugSnapshot(themeName)); }); } }, + terminalThemeDebugSnapshot(themeName) { + const shell = this.$el.closest('.application-console-shell'); + const viewport = this.term?.element?.querySelector('.xterm-viewport'); + const screen = this.term?.element?.querySelector('.xterm-screen'); + + return { + requestedTheme: themeName, + selectedTheme: this.selectedTheme, + terminalExists: Boolean(this.term), + terminalOpened: Boolean(this.term?.element), + terminalActive: this.terminalActive, + connectionState: this.connectionState, + shellTheme: shell?.dataset.consoleTheme, + shellBackground: shell ? getComputedStyle(shell).background : null, + shellThemeBackground: shell ? getComputedStyle(shell).getPropertyValue('--console-theme-background') : null, + shellThemeOpacity: shell ? getComputedStyle(shell).getPropertyValue('--console-theme-opacity') : null, + shellPseudoBackground: shell ? getComputedStyle(shell, '::before').background : null, + viewportBackground: viewport ? getComputedStyle(viewport).background : null, + screenBackground: screen ? getComputedStyle(screen).background : null, + xtermBackground: this.term?.options.theme?.background, + xtermForeground: this.term?.options.theme?.foreground, + xtermCursor: this.term?.options.theme?.cursor, + }; + }, + resetTerminal() { if (this.term) { this.$wire.dispatch('error', 'Terminal websocket connection lost. Reconnecting...'); diff --git a/resources/views/components/database/configuration-sidebar.blade.php b/resources/views/components/database/configuration-sidebar.blade.php index 9091ffcb0..a2f301230 100644 --- a/resources/views/components/database/configuration-sidebar.blade.php +++ b/resources/views/components/database/configuration-sidebar.blade.php @@ -12,10 +12,10 @@ ['label' => 'Environment Variables', 'route' => 'project.database.environment-variables', 'icon' => 'variables'], ['label' => 'Persistent Storage', 'route' => 'project.database.persistent-storage', 'icon' => 'storages'], ['label' => 'Backups', 'route' => 'project.database.backup.index', 'icon' => 'database', 'visible' => $database->isBackupSolutionAvailable()], + ['label' => 'Import Backup', 'route' => 'project.database.import-backup', 'icon' => 'upload', 'navigate' => false, 'visible' => auth()->user()?->can('update', $database)], ['label' => 'Servers', 'route' => 'project.database.servers', 'icon' => 'servers'], ['label' => 'Runtime', 'route' => 'project.database.logs', 'icon' => 'unordered-list', 'navigate' => false], ['label' => 'Terminal', 'route' => 'project.database.command', 'icon' => 'browser-terminal', 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')], - ['label' => 'Import Backup', 'route' => 'project.database.import-backup', 'icon' => 'upload', 'visible' => auth()->user()?->can('update', $database)], ['label' => 'Webhooks', 'route' => 'project.database.webhooks', 'icon' => 'notifications'], ['label' => 'Healthcheck', 'route' => 'project.database.healthcheck', 'icon' => 'feedback'], ['label' => 'Resource Limits', 'route' => 'project.database.resource-limits', 'icon' => 'cpu'], @@ -32,7 +32,7 @@ ]); $menuGroups = [ - 'Settings' => ['General', 'Environment Variables', 'Persistent Storage', 'Backups', 'Servers', 'Import Backup'], + 'Settings' => ['General', 'Environment Variables', 'Persistent Storage', 'Backups', 'Import Backup', 'Servers'], 'Automation' => ['Webhooks', 'Healthcheck'], 'Logs' => ['Runtime'], 'Operations' => ['Terminal', 'Resource Limits', 'Resource Operations', 'Metrics', 'Tags', 'Danger Zone'], @@ -41,6 +41,19 @@ $groupedItems = collect($menuGroups) ->map(fn (array $labels) => $configurationItems->whereIn('label', $labels)->values()) ->filter(fn ($items) => $items->isNotEmpty()); + + $pageSections = $database->type() === 'standalone-postgresql' + ? [ + ['id' => 'database-details-section', 'label' => 'Database details'], + ['id' => 'credentials-section', 'label' => 'Credentials'], + ['id' => 'initialization-section', 'label' => 'Initialization'], + ['id' => 'runtime-network-section', 'label' => 'Runtime and network'], + ['id' => 'public-access-section', 'label' => 'Public access'], + ['id' => 'configuration-section', 'label' => 'Configuration'], + ['id' => 'log-delivery-section', 'label' => 'Log delivery'], + ['id' => 'initialization-scripts-section', 'label' => 'Initialization scripts'], + ] + : []; @endphp