feat(ui): toggle Server Timing HUD and link missing env badge

Add a local-only command palette action to show/hide the Server Timing
HUD via localStorage and a visibility event. Make the service heading
"Required variables missing" badge link to environment variables, with
tests covering both behaviors.
This commit is contained in:
Andras Bacsai committed 2026-08-07 12:35:49 +02:00
1 parent fd6dbd5863
commit d68b198d53
6 files changed
+93 -2

No files matched your search

@@ -101,6 +101,7 @@
window.__coolifyServerTimingHud = true;
const STORAGE_KEY = 'coolify.serverTimingHud.open';
const ENABLED_STORAGE_KEY = 'coolify.serverTimingHud.enabled';
const MAX_ENTRIES = 50;
const MS_KEYS = new Set(['app', 'db', 'php', 'dbslow']);
@@ -117,6 +118,25 @@
return document.getElementById('server-timing-hud');
}
function isHudEnabled() {
return localStorage.getItem(ENABLED_STORAGE_KEY) !== '0';
}
function applyVisibilityPreference() {
const root = rootEl();
if (!root) {
return;
}
const enabled = isHudEnabled();
root.style.display = enabled ? 'block' : 'none';
const slot = root.parentElement?.matches('[data-server-timing-hud-slot]') ? root.parentElement : null;
if (slot) {
slot.style.display = enabled ? 'flex' : 'none';
}
}
function pickSlot() {
const desktop = document.getElementById('server-timing-hud-slot');
const mobile = document.getElementById('server-timing-hud-slot-mobile');
@@ -306,6 +326,7 @@
applyFloatStyles(keep);
}
applyOpenState();
applyVisibilityPreference();
return keep;
}
@@ -960,6 +981,7 @@
}
});
window.addEventListener('resize', redock);
window.addEventListener('server-timing-hud-visibility-changed', redock);
// App shell uses x-cloak; re-dock once Alpine reveals the top bar.
document.addEventListener('alpine:initialized', redock);
// Safety: retry a few times after load in case cloak clears late.
@@ -14,6 +14,21 @@
const ua = navigator.userAgent || '';
return /Mac|iPhone|iPad|iPod/i.test(platform) || /Mac OS X|Macintosh/i.test(ua) ? '⌘' : 'Ctrl+';
})(),
serverTimingHudEnabled: localStorage.getItem('coolify.serverTimingHud.enabled') !== '0',
developerCommandsEnabled: @js(app()->environment('local')),
get showServerTimingCommand() {
if (!this.developerCommandsEnabled) return false;
const query = this.searchQuery.toLowerCase().trim();
return query.length > 0 && ['server timing', 'timing hud', 'debug hud'].some(term => term.includes(query) || query.includes(term));
},
toggleServerTimingHud() {
this.serverTimingHudEnabled = !this.serverTimingHudEnabled;
localStorage.setItem('coolify.serverTimingHud.enabled', this.serverTimingHudEnabled ? '1' : '0');
window.dispatchEvent(new CustomEvent('server-timing-hud-visibility-changed'));
this.closeModal();
},
// Client-side search function
get searchResults() {
@@ -327,6 +342,24 @@
<!-- Search results -->
<div x-show="searchQuery.length >= 1" x-cloak class="command-palette-body relative">
@if (app()->environment('local'))
<div x-show="showServerTimingCommand && !$wire.isSelectingResource"
class="command-palette-section">
<div class="command-palette-group-label">Developer tools</div>
<button type="button" @click="toggleServerTimingHud()"
class="search-result-item command-palette-item">
<div class="command-palette-item-main">
<div class="command-palette-item-title">
<span class="command-palette-item-name">Toggle Server Timing HUD</span>
<span class="command-palette-type-badge" x-text="serverTimingHudEnabled ? 'Enabled' : 'Disabled'"></span>
</div>
<div class="command-palette-item-meta"
x-text="serverTimingHudEnabled ? 'Hide the local request timing overlay' : 'Show the local request timing overlay'"></div>
</div>
<x-reicon name="time-back" class="command-palette-item-chevron" />
</button>
</div>
@endif
<div x-show="isPaletteTransitioning" x-cloak
class="absolute inset-0 z-30 flex items-center justify-center bg-white/50 backdrop-blur-[2px] dark:bg-black/40">
<x-loading text="Loading…" />
@@ -639,7 +672,7 @@
</template>
<template
x-if="searchQuery.length >= 2 && searchResults.length === 0 && filteredCreatableItems.length === 0 && !$wire.isSelectingResource && !$wire.autoOpenResource && !isLoadingInitialData">
x-if="searchQuery.length >= 2 && searchResults.length === 0 && filteredCreatableItems.length === 0 && !showServerTimingCommand && !$wire.isSelectingResource && !$wire.autoOpenResource && !isLoadingInitialData">
<div class="command-palette-empty">
<p class="command-palette-empty-title">No results found</p>
<p class="command-palette-empty-desc">
@@ -42,6 +42,11 @@
));
$serviceStatus = str($service->status ?? 'exited');
$environmentVariablesUrl = route('project.service.environment-variables', [
'project_uuid' => $service->environment->project->uuid,
'environment_uuid' => $service->environment->uuid,
'service_uuid' => $service->uuid,
]);
@endphp
<livewire:project.shared.configuration-checker :resource="$service" />
@@ -158,6 +163,11 @@
@endif
</div>
</div>
@else
<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
class="mb-3 inline-flex" aria-label="Open required environment variables">
<x-status-badge status="Required variables missing" type="error" />
</a>
@endif
<div class="resource-heading-menus w-full">
@@ -209,7 +219,10 @@
</x-forms.button>
@endif
@else
<x-status-badge status="Required variables missing" type="error" />
<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
aria-label="Open required environment variables">
<x-status-badge status="Required variables missing" type="error" />
</a>
@endif
</div>
</div>
@@ -214,3 +214,16 @@ test('SERVER_TIMING_ENABLED can force server_timing on or off regardless of APP_
expect($resolve('0', 'local'))->toBeFalse();
expect($resolve('off', 'local'))->toBeFalse();
});
test('server timing HUD can be toggled from the development command center', function () {
$hud = file_get_contents(resource_path('views/components/server-timing-hud.blade.php'));
$commandCenter = file_get_contents(resource_path('views/livewire/global-search.blade.php'));
expect($hud)
->toContain('coolify.serverTimingHud.enabled')
->toContain('server-timing-hud-visibility-changed')
->and($commandCenter)
->toContain("app()->environment('local')")
->toContain('Toggle Server Timing HUD')
->toContain("localStorage.setItem('coolify.serverTimingHud.enabled'")
->toContain("window.dispatchEvent(new CustomEvent('server-timing-hud-visibility-changed'))");
});
@@ -111,6 +111,7 @@ it('warns when a service has missing required environment variables', function (
->assertSee('Required environment variable missing')
->assertSee('PLUNK_API_KEY')
->assertSee('Open environment variables');
});
it('marks the service environment variables menu when required values are missing', function () {
@@ -82,6 +82,15 @@ it('docks desktop resource actions in the top bar instead of floating over conte
}
});
it('links the service header missing variables warning to environment variables', function () {
$heading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
expect($heading)
->toContain("route('project.service.environment-variables'")
->toContain('Required variables missing')
->toContain('href="{{ $environmentVariablesUrl }}"');
});
it('places the account menu beside the desktop sidebar toggle while retaining it on mobile', function () {
$layout = file_get_contents(resource_path('views/layouts/app.blade.php'));
$navbar = file_get_contents(resource_path('views/components/navbar.blade.php'));