- Add minimal blocking script immediately after <html> tag to apply dark class before any rendering - Move theme detection from body to run before <head> parsing - Add color-scheme meta tag for browser-level dark mode support - Update theme-color meta tag dynamically based on theme - Improve queryTheme() logic in settings dropdown for consistent behavior - Remove duplicate theme detection code from body script This eliminates the white "flashbang" effect that occurs during Livewire page navigation, especially noticeable for users with high latency connections. The solution uses an ultra-minimal (~100 bytes) script that runs before <head> parsing, preventing FOUC while maintaining optimal performance (~0.1ms impact). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
327 lines
14 KiB
PHP
327 lines
14 KiB
PHP
<!DOCTYPE html>
|
|
<html data-theme="dark" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
|
<script>
|
|
// Immediate theme application - runs before any rendering
|
|
(function() {
|
|
const t = localStorage.theme || 'dark';
|
|
const d = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
|
|
document.documentElement.classList[d ? 'add' : 'remove']('dark');
|
|
})();
|
|
</script>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="robots" content="noindex">
|
|
<meta name="theme-color" content="#101010" id="theme-color-meta" />
|
|
<meta name="color-scheme" content="dark light" />
|
|
<meta name="Description" content="Coolify: An open-source & self-hostable Heroku / Netlify / Vercel alternative" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta name="twitter:site" content="@coolifyio" />
|
|
<meta name="twitter:title" content="Coolify" />
|
|
<meta name="twitter:description" content="An open-source & self-hostable Heroku / Netlify / Vercel alternative." />
|
|
<meta name="twitter:image" content="https://cdn.coollabs.io/assets/coolify/og-image.png" />
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:url" content="https://coolify.io" />
|
|
<meta property="og:title" content="Coolify" />
|
|
<meta property="og:description" content="An open-source & self-hostable Heroku / Netlify / Vercel alternative." />
|
|
<meta property="og:site_name" content="Coolify" />
|
|
<meta property="og:image" content="https://cdn.coollabs.io/assets/coolify/og-image.png" />
|
|
@use('App\Models\InstanceSettings')
|
|
@php
|
|
|
|
$instanceSettings = instanceSettings();
|
|
$name = null;
|
|
|
|
if ($instanceSettings) {
|
|
$displayName = $instanceSettings->getTitleDisplayName();
|
|
|
|
if (strlen($displayName) > 0) {
|
|
$name = $displayName . ' ';
|
|
}
|
|
}
|
|
@endphp
|
|
<title>{{ $name }}{{ $title ?? 'Coolify' }}</title>
|
|
@env('local')
|
|
<link rel="icon" href="{{ asset('coolify-logo-dev-transparent.png') }}" type="image/png" />
|
|
@else
|
|
<link rel="icon" href="{{ asset('coolify-logo.svg') }}" type="image/svg+xml" />
|
|
@endenv
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
|
<script>
|
|
// Update theme-color meta tag (non-critical, can run async)
|
|
const t = localStorage.theme || 'dark';
|
|
const isDark = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
|
|
document.getElementById('theme-color-meta')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
|
|
</script>
|
|
<style>
|
|
[x-cloak] {
|
|
display: none !important;
|
|
}
|
|
</style>
|
|
@if (config('app.name') == 'Coolify Cloud')
|
|
<script defer data-domain="app.coolify.io" src="https://analytics.coollabs.io/js/plausible.js"></script>
|
|
<script src="https://js.sentry-cdn.com/0f8593910512b5cdd48c6da78d4093be.min.js" crossorigin="anonymous"></script>
|
|
@endif
|
|
@auth
|
|
<script type="text/javascript" src="{{ URL::asset('js/echo.js') }}"></script>
|
|
<script type="text/javascript" src="{{ URL::asset('js/pusher.js') }}"></script>
|
|
<script type="text/javascript" src="{{ URL::asset('js/apexcharts.js') }}"></script>
|
|
<script type="text/javascript" src="{{ URL::asset('js/purify.min.js') }}"></script>
|
|
@endauth
|
|
</head>
|
|
@section('body')
|
|
|
|
<body>
|
|
<x-toast />
|
|
<script data-navigate-once>
|
|
// Global HTML sanitization function using DOMPurify
|
|
window.sanitizeHTML = function(html) {
|
|
if (!html) return '';
|
|
const URL_RE = /^(https?:|mailto:)/i;
|
|
const config = {
|
|
ALLOWED_TAGS: ['a', 'b', 'br', 'code', 'del', 'div', 'em', 'i', 'p', 'pre', 's', 'span', 'strong',
|
|
'u'
|
|
],
|
|
ALLOWED_ATTR: ['class', 'href', 'target', 'title', 'rel'],
|
|
ALLOW_DATA_ATTR: false,
|
|
FORBID_TAGS: ['script', 'object', 'embed', 'applet', 'iframe', 'form', 'input', 'button', 'select',
|
|
'textarea', 'details', 'summary', 'dialog', 'style'
|
|
],
|
|
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange',
|
|
'onsubmit', 'ontoggle', 'style'
|
|
],
|
|
KEEP_CONTENT: true,
|
|
RETURN_DOM: false,
|
|
RETURN_DOM_FRAGMENT: false,
|
|
SANITIZE_DOM: true,
|
|
SANITIZE_NAMED_PROPS: true,
|
|
SAFE_FOR_TEMPLATES: true,
|
|
ALLOWED_URI_REGEXP: URL_RE
|
|
};
|
|
|
|
// One-time hook registration (idempotent pattern)
|
|
if (!window.__dpLinkHook) {
|
|
DOMPurify.addHook('afterSanitizeAttributes', node => {
|
|
// Remove Alpine.js directives to prevent XSS
|
|
if (node.hasAttributes && node.hasAttributes()) {
|
|
const attrs = Array.from(node.attributes);
|
|
attrs.forEach(attr => {
|
|
// Remove x-* attributes (Alpine directives)
|
|
if (attr.name.startsWith('x-')) {
|
|
node.removeAttribute(attr.name);
|
|
}
|
|
// Remove @* attributes (Alpine event shorthand)
|
|
if (attr.name.startsWith('@')) {
|
|
node.removeAttribute(attr.name);
|
|
}
|
|
// Remove :* attributes (Alpine binding shorthand)
|
|
if (attr.name.startsWith(':')) {
|
|
node.removeAttribute(attr.name);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Existing link sanitization
|
|
if (node.nodeName === 'A' && node.hasAttribute('href')) {
|
|
const href = node.getAttribute('href') || '';
|
|
if (!URL_RE.test(href)) node.removeAttribute('href');
|
|
if (node.getAttribute('target') === '_blank') {
|
|
node.setAttribute('rel', 'noopener noreferrer');
|
|
}
|
|
}
|
|
});
|
|
window.__dpLinkHook = true;
|
|
}
|
|
return DOMPurify.sanitize(html, config);
|
|
};
|
|
|
|
// Initialize theme if not set
|
|
if (!('theme' in localStorage)) {
|
|
localStorage.theme = 'dark';
|
|
}
|
|
|
|
let theme = localStorage.theme
|
|
let cpuColor = '#1e90ff'
|
|
let ramColor = '#00ced1'
|
|
let textColor = '#ffffff'
|
|
let editorBackground = '#181818'
|
|
let editorTheme = 'blackboard'
|
|
|
|
function checkTheme() {
|
|
theme = localStorage.theme
|
|
if (theme == 'system') {
|
|
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
}
|
|
if (theme == 'dark') {
|
|
cpuColor = '#1e90ff'
|
|
ramColor = '#00ced1'
|
|
textColor = '#ffffff'
|
|
editorBackground = '#181818'
|
|
editorTheme = 'blackboard'
|
|
} else {
|
|
cpuColor = '#1e90ff'
|
|
ramColor = '#00ced1'
|
|
textColor = '#000000'
|
|
editorBackground = '#ffffff'
|
|
editorTheme = null
|
|
}
|
|
}
|
|
@auth
|
|
window.Pusher = Pusher;
|
|
window.Echo = new Echo({
|
|
broadcaster: 'pusher',
|
|
cluster: "{{ config('constants.pusher.host') }}" || window.location.hostname,
|
|
key: "{{ config('constants.pusher.app_key') }}" || 'coolify',
|
|
wsHost: "{{ config('constants.pusher.host') }}" || window.location.hostname,
|
|
wsPort: "{{ getRealtime() }}",
|
|
wssPort: "{{ getRealtime() }}",
|
|
forceTLS: false,
|
|
encrypted: true,
|
|
enableStats: false,
|
|
enableLogging: true,
|
|
enabledTransports: ['ws', 'wss'],
|
|
disableStats: true,
|
|
// Add auto reconnection settings
|
|
enabledTransports: ['ws', 'wss'],
|
|
disabledTransports: ['sockjs', 'xhr_streaming', 'xhr_polling'],
|
|
// Attempt to reconnect on connection lost
|
|
autoReconnect: true,
|
|
// Wait 1 second before first reconnect attempt
|
|
reconnectionDelay: 1000,
|
|
// Maximum delay between reconnection attempts
|
|
maxReconnectionDelay: 1000,
|
|
// Multiply delay by this number for each reconnection attempt
|
|
reconnectionDelayGrowth: 1,
|
|
// Maximum number of reconnection attempts
|
|
maxAttempts: 15
|
|
});
|
|
@endauth
|
|
let checkHealthInterval = null;
|
|
let checkIfIamDeadInterval = null;
|
|
|
|
function changePasswordFieldType(event) {
|
|
let element = event.target
|
|
for (let i = 0; i < 10; i++) {
|
|
if (element.className === "relative") {
|
|
break;
|
|
}
|
|
element = element.parentElement;
|
|
}
|
|
element = element.children[1];
|
|
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
|
|
if (element.type === 'password') {
|
|
element.type = 'text';
|
|
if (element.disabled) return;
|
|
element.classList.add('truncate');
|
|
this.type = 'text';
|
|
} else {
|
|
element.type = 'password';
|
|
if (element.disabled) return;
|
|
element.classList.remove('truncate');
|
|
this.type = 'password';
|
|
}
|
|
}
|
|
}
|
|
|
|
function copyToClipboard(text) {
|
|
navigator?.clipboard?.writeText(text) && window.Livewire.dispatch('success', 'Copied to clipboard.');
|
|
}
|
|
document.addEventListener('livewire:init', () => {
|
|
window.Livewire.on('reloadWindow', (timeout) => {
|
|
if (timeout) {
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, timeout);
|
|
return;
|
|
} else {
|
|
window.location.reload();
|
|
}
|
|
})
|
|
window.Livewire.on('info', (message) => {
|
|
if (typeof message === 'string') {
|
|
window.toast('Info', {
|
|
type: 'info',
|
|
description: message,
|
|
})
|
|
return;
|
|
}
|
|
if (message.length == 1) {
|
|
window.toast('Info', {
|
|
type: 'info',
|
|
description: message[0],
|
|
})
|
|
} else if (message.length == 2) {
|
|
window.toast(message[0], {
|
|
type: 'info',
|
|
description: message[1],
|
|
})
|
|
}
|
|
})
|
|
window.Livewire.on('error', (message) => {
|
|
if (typeof message === 'string') {
|
|
window.toast('Error', {
|
|
type: 'danger',
|
|
description: message,
|
|
})
|
|
return;
|
|
}
|
|
if (message.length == 1) {
|
|
window.toast('Error', {
|
|
type: 'danger',
|
|
description: message[0],
|
|
})
|
|
} else if (message.length == 2) {
|
|
window.toast(message[0], {
|
|
type: 'danger',
|
|
description: message[1],
|
|
})
|
|
}
|
|
})
|
|
window.Livewire.on('warning', (message) => {
|
|
if (typeof message === 'string') {
|
|
window.toast('Warning', {
|
|
type: 'warning',
|
|
description: message,
|
|
})
|
|
return;
|
|
}
|
|
if (message.length == 1) {
|
|
window.toast('Warning', {
|
|
type: 'warning',
|
|
description: message[0],
|
|
})
|
|
} else if (message.length == 2) {
|
|
window.toast(message[0], {
|
|
type: 'warning',
|
|
description: message[1],
|
|
})
|
|
}
|
|
})
|
|
window.Livewire.on('success', (message) => {
|
|
if (typeof message === 'string') {
|
|
window.toast('Success', {
|
|
type: 'success',
|
|
description: message,
|
|
})
|
|
return;
|
|
}
|
|
if (message.length == 1) {
|
|
window.toast('Success', {
|
|
type: 'success',
|
|
description: message[0],
|
|
})
|
|
} else if (message.length == 2) {
|
|
window.toast(message[0], {
|
|
type: 'success',
|
|
description: message[1],
|
|
})
|
|
}
|
|
})
|
|
});
|
|
</script>
|
|
</body>
|
|
@show
|
|
|
|
</html>
|