Merge branch 'next' into service/lobe-ai-chat

This commit is contained in:
Romain ROCHAS 2025-10-08 21:46:23 +02:00 committed by GitHub
commit bdd078e53d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1755 additions and 270 deletions

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,11 @@ protected static function booted()
});
}
public static function ownedByCurrentTeam()
{
return Environment::whereRelation('project.team', 'id', currentTeam()->id)->orderBy('name');
}
public function isEmpty()
{
return $this->applications()->count() == 0 &&

View file

@ -114,7 +114,7 @@
}
}
}"
@keydown.escape.window="modalOpen = false; resetModal()" :class="{ 'z-40': modalOpen }"
@keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto">
@if ($customButton)
@if ($buttonFullWidth)

View file

@ -59,25 +59,25 @@
if (this.zoom === '90') {
const style = document.createElement('style');
style.textContent = `
html {
font-size: 93.75%;
}
:root {
--vh: 1vh;
}
@media (min-width: 1024px) {
html {
font-size: 87.5%;
}
}
`;
html {
font-size: 93.75%;
}
:root {
--vh: 1vh;
}
@media (min-width: 1024px) {
html {
font-size: 87.5%;
}
}
`;
document.head.appendChild(style);
}
}
}">
<div class="flex pt-6 pb-4 pl-2">
<div class="flex lg:pt-6 pt-4 pb-4 pl-2">
<div class="flex flex-col w-full">
<div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div>
<x-version />
@ -86,8 +86,8 @@
<!-- Search button that triggers global search modal -->
<button @click="$dispatch('open-global-search')" type="button" title="Search (Press / or ⌘K)"
class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>

View file

@ -20,9 +20,9 @@
}" x-cloak class="mx-auto" :class="pageWidth === 'full' ? '' : 'max-w-7xl'">
<div class="relative z-50 lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-black/80" x-on:click="open = false"></div>
<div class="fixed h-full flex">
<div class="fixed inset-y-0 right-0 h-full flex">
<div class="relative flex flex-1 w-full max-w-56 ">
<div class="absolute top-0 flex justify-center w-16 pt-5 left-full">
<div class="absolute top-0 flex justify-center w-16 pt-5 right-full">
<button type="button" class="-m-2.5 p-2.5" x-on:click="open = !open">
<span class="sr-only">Close sidebar</span>
<svg class="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
@ -45,8 +45,12 @@
</div>
</div>
<div class="sticky top-0 z-40 flex items-center px-4 py-4 gap-x-6 sm:px-6 lg:hidden">
<button type="button" class="-m-2.5 p-2.5 dark:text-warning lg:hidden" x-on:click="open = !open">
<div class="sticky top-0 z-40 flex items-center justify-between px-4 py-4 gap-x-6 sm:px-6 lg:hidden">
<div class="flex items-center gap-3 flex-shrink-0">
<div class="text-xl font-bold tracking-wide dark:text-white">Coolify</div>
<livewire:switch-team />
</div>
<button type="button" class="-m-2.5 p-2.5 dark:text-warning" x-on:click="open = !open">
<span class="sr-only">Open sidebar</span>
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"

View file

@ -21,7 +21,7 @@
<x-modal-input buttonTitle="Add" title="New Project">
<x-slot:content>
<button
class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300">
class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
@ -87,7 +87,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
<x-slot:content>
<button
class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300">
class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />

View file

@ -1,14 +1,82 @@
<div x-data="{
modalOpen: false,
selectedIndex: -1,
isSearching: false,
isLoadingInitialData: false,
allSearchableItems: [],
searchQuery: '',
creatableItems: [],
isCreateMode: false,
// Client-side search function
get searchResults() {
if (!this.searchQuery || this.searchQuery.length < 1) {
return [];
}
const query = this.searchQuery.toLowerCase().trim();
const results = this.allSearchableItems.filter(item => {
if (!item.search_text) return false;
return item.search_text.toLowerCase().includes(query);
}).slice(0, 20);
return results;
},
get filteredCreatableItems() {
if (!this.searchQuery || this.searchQuery.length < 1) {
return [];
}
const query = this.searchQuery.toLowerCase().trim();
if (query === 'new') {
return this.creatableItems;
}
return this.creatableItems.filter(item => {
const searchText = `${item.name} ${item.description} ${item.type} ${item.category}`.toLowerCase();
if (query.startsWith('new ')) {
const queryWithoutNew = query.substring(4);
return searchText.includes(queryWithoutNew) || searchText.includes(query);
}
return searchText.includes(query);
});
},
get groupedCreatableItems() {
const grouped = {};
this.filteredCreatableItems.forEach(item => {
const category = item.category || 'Other';
if (!grouped[category]) {
grouped[category] = [];
}
grouped[category].push(item);
});
return grouped;
},
openModal() {
this.modalOpen = true;
this.selectedIndex = -1;
@this.openSearchModal();
this.isLoadingInitialData = true;
this.searchQuery = '';
$wire.openSearchModal().then(() => {
this.allSearchableItems = $wire.allSearchableItems || [];
this.creatableItems = $wire.creatableItems || [];
this.isLoadingInitialData = false;
setTimeout(() => this.$refs.searchInput?.focus(), 50);
});
},
closeModal() {
this.modalOpen = false;
this.selectedIndex = -1;
this.isSearching = false;
this.isLoadingInitialData = false;
this.searchQuery = '';
this.allSearchableItems = [];
// Ensure scroll is restored
document.body.style.overflow = '';
@this.closeSearchModal();
@ -31,19 +99,68 @@
}
},
init() {
// Listen for reset index event from Livewire
Livewire.on('reset-selected-index', () => {
this.selectedIndex = -1;
});
this.$watch('searchQuery', (value) => {
this.selectedIndex = -1;
const trimmed = value.trim().toLowerCase();
if (trimmed === '') {
if ($wire.isSelectingResource) {
$wire.cancelResourceSelection();
}
return;
}
const exactMatchCommands = [
'new project', 'new server', 'new team', 'new storage', 'new s3',
'new private key', 'new privatekey', 'new key',
'new github app', 'new github', 'new source',
'new public', 'new public git', 'new public repo', 'new public repository',
'new private github', 'new private gh', 'new private deploy', 'new deploy key',
'new dockerfile', 'new docker compose', 'new compose', 'new docker image', 'new image',
'new postgresql', 'new postgres', 'new mysql', 'new mariadb',
'new redis', 'new keydb', 'new dragonfly', 'new mongodb', 'new mongo', 'new clickhouse'
];
if (exactMatchCommands.includes(trimmed)) {
const matchingItem = this.creatableItems.find(item => {
const itemSearchText = `new ${item.name}`.toLowerCase();
const itemType = `new ${item.type}`.toLowerCase();
return itemSearchText === trimmed || itemType === trimmed ||
(item.type && trimmed.includes(item.type.replace(/-/g, ' ')));
});
if (matchingItem) {
$wire.navigateToResource(matchingItem.type);
}
}
});
// Create named handlers for proper cleanup
const openGlobalSearchHandler = () => this.openModal();
const slashKeyHandler = (e) => {
if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName) && !this.modalOpen) {
if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName)) {
e.preventDefault();
this.openModal();
if (!this.modalOpen) {
this.openModal();
} else {
// If modal is open, focus the input
this.$refs.searchInput?.focus();
this.selectedIndex = -1;
}
}
};
const cmdKHandler = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
if (this.modalOpen) {
this.closeModal();
// If modal is open, focus the input instead of closing
this.$refs.searchInput?.focus();
this.selectedIndex = -1;
} else {
this.openModal();
}
@ -51,7 +168,21 @@
};
const escapeKeyHandler = (e) => {
if (e.key === 'Escape' && this.modalOpen) {
this.closeModal();
// If search query is empty, close the modal
if (!this.searchQuery || this.searchQuery === '') {
// Check if we're in a selection state using Alpine-accessible Livewire state
if ($wire.isSelectingResource) {
$wire.cancelResourceSelection();
setTimeout(() => this.$refs.searchInput?.focus(), 100);
} else {
// Close the modal if in main menu
this.closeModal();
}
} else {
// If search query has text, just clear it
this.searchQuery = '';
setTimeout(() => this.$refs.searchInput?.focus(), 100);
}
}
};
const arrowKeyHandler = (e) => {
@ -94,13 +225,18 @@
}, 150);
}
});
// Listen for closeSearchModal event from backend
window.addEventListener('closeSearchModal', () => {
this.closeModal();
});
}
}">
<!-- Modal overlay -->
<template x-teleport="body">
<div x-show="modalOpen" x-cloak
class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen pt-[20vh]">
class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen pt-[10vh]">
<div @click="closeModal()" class="absolute inset-0 w-full h-full bg-black/50 backdrop-blur-sm">
</div>
<div x-show="modalOpen" x-trap.inert="modalOpen" x-init="$watch('modalOpen', value => { document.body.style.overflow = value ? 'hidden' : '' })"
@ -113,73 +249,456 @@ class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen
<!-- Search input (always visible) -->
<div class="relative">
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-neutral-400" xmlns="http://www.w3.org/2000/svg" fill="none"
<svg x-show="!isLoadingInitialData" class="w-5 h-5 text-neutral-400" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<svg x-show="isLoadingInitialData" x-cloak class="animate-spin h-5 w-5 text-warning"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
</div>
<input type="text" x-model="searchQuery"
placeholder="Search resources (type new for create things)..." x-ref="searchInput"
x-init="$watch('modalOpen', value => { if (value) setTimeout(() => $refs.searchInput.focus(), 100) })" :disabled="isLoadingInitialData"
class="w-full pl-12 pr-32 py-4 text-base bg-white dark:bg-coolgray-100 border-none rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 disabled:opacity-50 disabled:cursor-not-allowed" />
<div class="absolute inset-y-0 right-2 flex items-center gap-2 pointer-events-none">
<span class="text-xs font-medium text-neutral-400 dark:text-neutral-500">
/ or ⌘K to focus
</span>
<button @click="closeModal()"
class="pointer-events-auto px-2 py-1 text-xs font-medium text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 rounded">
ESC
</button>
</div>
<input type="text" wire:model.live.debounce.500ms="searchQuery"
placeholder="Search for resources... (Type 'new' to create, or 'new project' to add directly)"
x-ref="searchInput" x-init="$watch('modalOpen', value => { if (value) setTimeout(() => $refs.searchInput.focus(), 100) })"
class="w-full pl-12 pr-12 py-4 text-base bg-white dark:bg-coolgray-100 border-none rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500" />
<button @click="closeModal()"
class="absolute inset-y-0 right-2 flex items-center justify-center px-2 text-xs font-medium text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 rounded">
ESC
</button>
</div>
<!-- Search results (with background) -->
@if (strlen($searchQuery) >= 1)
<div
class="mt-2 bg-white dark:bg-coolgray-100 rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 overflow-hidden">
<!-- Loading indicator -->
<div wire:loading.flex wire:target="searchQuery"
class="min-h-[200px] items-center justify-center p-8">
<div class="text-center">
<svg class="animate-spin mx-auto h-8 w-8 text-neutral-400"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
Searching...
</p>
</div>
</div>
<!-- Debug: Show data loaded (temporary) -->
{{-- <div x-show="!isLoadingInitialData && searchQuery === '' && allSearchableItems.length > 0" x-cloak
class="mt-2 bg-white dark:bg-coolgray-100 rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 overflow-hidden p-6">
<div class="text-center">
<p class="text-sm font-semibold text-green-600 dark:text-green-400">
Data loaded successfully!
</p>
<p class="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
<span x-text="allSearchableItems.length"></span> searchable items available
</p>
<p class="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
Start typing to search...
</p>
</div>
</div> --}}
<!-- Results content - hidden while loading -->
<div wire:loading.remove wire:target="searchQuery"
class="max-h-[60vh] overflow-y-auto scrollbar">
@if ($isCreateMode && count($creatableItems) > 0 && !$autoOpenResource)
<!-- Create new resources section -->
<div class="py-2" x-data="{
openModal(type) {
// Close the parent search modal properly
const parentModal = this.$root.closest('[x-data]');
if (parentModal && parentModal.__x) {
parentModal.__x.$data.closeModal();
}
// Dispatch event to open creation modal after a short delay
setTimeout(() => {
this.$dispatch('open-create-modal-' + type);
}, 150);
}
}">
<div
class="px-4 py-2 bg-yellow-50 dark:bg-yellow-900/20 border-b border-yellow-100 dark:border-yellow-800">
<h3 class="text-sm font-semibold text-yellow-900 dark:text-yellow-100">
Create New Resources
</h3>
<p class="text-xs text-yellow-700 dark:text-yellow-300 mt-0.5">
Click on any item below to create a new resource
</p>
<!-- Search results (with background) -->
<div x-show="searchQuery.length >= 1" x-cloak
class="mt-2 bg-white dark:bg-coolgray-100 rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 overflow-hidden">
<!-- Results content -->
<div class="max-h-[60vh] overflow-y-auto scrollbar">
@if ($isSelectingResource)
<!-- Resource Selection Flow -->
<div class="p-6">
<!-- Server Selection -->
@if ($selectedServerId === null)
<div class="mb-4" x-init="selectedIndex = -1">
<div class="flex items-center gap-3 mb-3">
<button type="button"
@click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div>
<h2 class="text-base font-semibold text-neutral-900 dark:text-white">
Select Server
</h2>
@if ($this->selectedResourceName)
<div class="text-xs text-neutral-500 dark:text-neutral-400">
for {{ $this->selectedResourceName }}
</div>
@endif
</div>
</div>
@if ($loadingServers)
<div
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
<svg class="animate-spin h-5 w-5 text-yellow-500"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<span class="text-sm text-neutral-600 dark:text-neutral-400">Loading
servers...</span>
</div>
@elseif (count($availableServers) > 0)
@foreach ($availableServers as $index => $server)
<button type="button"
wire:click="selectServer({{ $server['id'] }}, true)"
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
<div class="flex-1 min-w-0">
<div class="font-medium text-neutral-900 dark:text-white">
{{ $server['name'] }}
</div>
@if (!empty($server['description']))
<div
class="text-xs text-neutral-500 dark:text-neutral-400">
{{ $server['description'] }}
</div>
@else
<div class="text-xs text-transparent select-none">
&nbsp;
</div>
@endif
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
@endforeach
@else
<div
class="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200">No servers
available</p>
</div>
@endif
</div>
@foreach ($creatableItems as $item)
<button type="button" @click="openModal('{{ $item['type'] }}')"
@endif
<!-- Destination Selection -->
@if ($selectedServerId !== null && $selectedDestinationUuid === null)
<div class="mb-4" x-init="selectedIndex = -1">
<div class="flex items-center gap-3 mb-3">
<button type="button"
@click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div>
<h2 class="text-base font-semibold text-neutral-900 dark:text-white">
Select Destination
</h2>
@if ($this->selectedResourceName)
<div class="text-xs text-neutral-500 dark:text-neutral-400">
for {{ $this->selectedResourceName }}
</div>
@endif
</div>
</div>
@if ($loadingDestinations)
<div
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
<svg class="animate-spin h-5 w-5 text-yellow-500"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<span class="text-sm text-neutral-600 dark:text-neutral-400">Loading
destinations...</span>
</div>
@elseif (count($availableDestinations) > 0)
@foreach ($availableDestinations as $index => $destination)
<button type="button"
wire:click="selectDestination('{{ $destination['uuid'] }}', true)"
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
<div class="flex-1 min-w-0">
<div class="font-medium text-neutral-900 dark:text-white">
{{ $destination['name'] }}
</div>
<div
class="text-xs text-neutral-500 dark:text-neutral-400">
Network: {{ $destination['network'] }}
</div>
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
@endforeach
@else
<div
class="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200">No destinations
available</p>
</div>
@endif
</div>
@endif
<!-- Project Selection -->
@if ($selectedDestinationUuid !== null && $selectedProjectUuid === null)
<div class="mb-4" x-init="selectedIndex = -1">
<div class="flex items-center gap-3 mb-3">
<button type="button"
@click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div>
<h2 class="text-base font-semibold text-neutral-900 dark:text-white">
Select Project
</h2>
@if ($this->selectedResourceName)
<div class="text-xs text-neutral-500 dark:text-neutral-400">
for {{ $this->selectedResourceName }}
</div>
@endif
</div>
</div>
@if ($loadingProjects)
<div
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
<svg class="animate-spin h-5 w-5 text-yellow-500"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<span class="text-sm text-neutral-600 dark:text-neutral-400">Loading
projects...</span>
</div>
@elseif (count($availableProjects) > 0)
@foreach ($availableProjects as $index => $project)
<button type="button"
wire:click="selectProject('{{ $project['uuid'] }}', true)"
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
<div class="flex-1 min-w-0">
<div class="font-medium text-neutral-900 dark:text-white">
{{ $project['name'] }}
</div>
@if (!empty($project['description']))
<div
class="text-xs text-neutral-500 dark:text-neutral-400">
{{ $project['description'] }}
</div>
@else
<div class="text-xs text-transparent select-none">
&nbsp;
</div>
@endif
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
@endforeach
@else
<div
class="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200">No projects
available</p>
</div>
@endif
</div>
@endif
<!-- Environment Selection -->
@if ($selectedProjectUuid !== null && $selectedEnvironmentUuid === null)
<div class="mb-4" x-init="selectedIndex = -1">
<div class="flex items-center gap-3 mb-3">
<button type="button"
@click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div>
<h2 class="text-base font-semibold text-neutral-900 dark:text-white">
Select Environment
</h2>
@if ($this->selectedResourceName)
<div class="text-xs text-neutral-500 dark:text-neutral-400">
for {{ $this->selectedResourceName }}
</div>
@endif
</div>
</div>
@if ($loadingEnvironments)
<div
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
<svg class="animate-spin h-5 w-5 text-yellow-500"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<span class="text-sm text-neutral-600 dark:text-neutral-400">Loading
environments...</span>
</div>
@elseif (count($availableEnvironments) > 0)
@foreach ($availableEnvironments as $index => $environment)
<button type="button"
wire:click="selectEnvironment('{{ $environment['uuid'] }}', true)"
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
<div class="flex-1 min-w-0">
<div class="font-medium text-neutral-900 dark:text-white">
{{ $environment['name'] }}
</div>
@if (!empty($environment['description']))
<div
class="text-xs text-neutral-500 dark:text-neutral-400">
{{ $environment['description'] }}
</div>
@else
<div class="text-xs text-transparent select-none">
&nbsp;
</div>
@endif
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
@endforeach
@else
<div
class="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200">No environments
available</p>
</div>
@endif
</div>
@endif
</div>
@elseif ($isCreateMode && count($this->filteredCreatableItems) > 0 && !$autoOpenResource)
<!-- Create new resources section -->
<div class="py-2">
{{-- Show existing resources first if any match --}}
@php
$existingResources = collect($searchResults)
->filter(fn($r) => !isset($r['is_creatable_suggestion']))
->count();
@endphp
@if ($existingResources > 0)
<!-- Existing Resources Section -->
<div class="px-4 pt-3 pb-1">
<h4
class="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
Existing Resources
</h4>
</div>
@foreach ($searchResults as $result)
@if (!isset($result['is_creatable_suggestion']))
<a href="{{ $result['link'] ?? '#' }}"
class="search-result-item block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-yellow-50 dark:focus:bg-yellow-900/20 border-transparent hover:border-coollabs focus:border-yellow-500 dark:focus:border-yellow-400">
<div class="flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span
class="font-medium text-neutral-900 dark:text-white truncate">
{{ $result['name'] }}
</span>
<span
class="px-2 py-0.5 text-xs rounded-full bg-neutral-100 dark:bg-coolgray-300 text-neutral-700 dark:text-neutral-300 shrink-0">
@if ($result['type'] === 'application')
Application
@elseif ($result['type'] === 'service')
Service
@elseif ($result['type'] === 'database')
{{ ucfirst($result['subtype'] ?? 'Database') }}
@elseif ($result['type'] === 'server')
Server
@elseif ($result['type'] === 'project')
Project
@elseif ($result['type'] === 'environment')
Environment
@endif
</span>
</div>
@if (!empty($result['project']) && !empty($result['environment']))
<div
class="text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{{ $result['project'] }} /
{{ $result['environment'] }}
</div>
@endif
@if (!empty($result['description']))
<div
class="text-sm text-neutral-600 dark:text-neutral-400">
{{ Str::limit($result['description'], 80) }}
</div>
@endif
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-neutral-300 dark:text-neutral-600 self-center"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</a>
@endif
@endforeach
@endif
@php
$grouped = collect($this->filteredCreatableItems)->groupBy('category');
@endphp
@foreach ($grouped as $category => $items)
<!-- Category Header -->
<div class="px-4 pt-3 pb-1">
<h4
class="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
{{ $category }}
</h4>
</div>
<!-- Category Items -->
@foreach ($items as $item)
<button type="button" wire:click="navigateToResource('{{ $item['type'] }}')"
class="search-result-item w-full text-left block px-4 py-3 hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-transparent hover:border-yellow-500 focus:border-yellow-500">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3 flex-1 min-w-0">
@ -194,16 +713,17 @@ class="h-5 w-5 text-yellow-600 dark:text-yellow-400"
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span
<div
class="font-medium text-neutral-900 dark:text-white truncate">
{{ $item['name'] }}
</span>
<span
class="px-2 py-0.5 text-xs rounded-full bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300 shrink-0">
New
</span>
</div>
@if (isset($item['quickcommand']))
<span
class="text-xs text-neutral-500 dark:text-neutral-400 shrink-0">{{ $item['quickcommand'] }}</span>
@endif
</div>
<div class="text-sm text-neutral-600 dark:text-neutral-400">
<div
class="text-sm text-neutral-600 dark:text-neutral-400 truncate">
{{ $item['description'] }}
</div>
</div>
@ -217,81 +737,140 @@ class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400 self-center"
</div>
</button>
@endforeach
</div>
@elseif (strlen($searchQuery) >= 2 && count($searchResults) > 0)
<div class="py-2">
@foreach ($searchResults as $index => $result)
<a href="{{ $result['link'] ?? '#' }}"
class="search-result-item block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-yellow-50 dark:focus:bg-yellow-900/20 border-transparent hover:border-coollabs focus:border-yellow-500 dark:focus:border-yellow-400">
<div class="flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span
class="font-medium text-neutral-900 dark:text-white truncate">
{{ $result['name'] }}
</span>
<span
class="px-2 py-0.5 text-xs rounded-full bg-neutral-100 dark:bg-coolgray-300 text-neutral-700 dark:text-neutral-300 shrink-0">
@if ($result['type'] === 'application')
Application
@elseif ($result['type'] === 'service')
Service
@elseif ($result['type'] === 'database')
{{ ucfirst($result['subtype'] ?? 'Database') }}
@elseif ($result['type'] === 'server')
Server
@elseif ($result['type'] === 'project')
Project
@elseif ($result['type'] === 'environment')
Environment
@endif
</span>
</div>
@if (!empty($result['project']) && !empty($result['environment']))
<div
class="text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{{ $result['project'] }} / {{ $result['environment'] }}
</div>
@endif
@if (!empty($result['description']))
<div class="text-sm text-neutral-600 dark:text-neutral-400">
{{ Str::limit($result['description'], 80) }}
</div>
@endif
@endforeach
</div>
@endif
<template
x-if="searchQuery.length >= 1 && searchResults.length > 0 && !$wire.isSelectingResource">
<div class="py-2">
<template x-if="filteredCreatableItems.length > 0">
<div class="px-4 pt-3 pb-1">
<h4
class="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
Existing Resources
</h4>
</div>
</template>
<template x-for="(result, index) in searchResults" :key="index">
<a :href="result.link || '#'"
class="search-result-item block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-yellow-50 dark:focus:bg-yellow-900/20 border-transparent hover:border-coollabs focus:border-yellow-500 dark:focus:border-yellow-400">
<div class="flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-neutral-900 dark:text-white truncate"
x-text="result.name">
</span>
<span
class="px-2 py-0.5 text-xs rounded-full bg-neutral-100 dark:bg-coolgray-300 text-neutral-700 dark:text-neutral-300 shrink-0">
<span x-show="result.type === 'navigation'">Navigation</span>
<span x-show="result.type === 'application'">Application</span>
<span x-show="result.type === 'service'">Service</span>
<span x-show="result.type === 'database'"
x-text="result.subtype ? result.subtype.charAt(0).toUpperCase() + result.subtype.slice(1) : 'Database'"></span>
<span x-show="result.type === 'server'">Server</span>
<span x-show="result.type === 'project'">Project</span>
<span x-show="result.type === 'environment'">Environment</span>
</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-neutral-300 dark:text-neutral-600 self-center"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<template x-if="result.project && result.environment">
<div class="text-xs text-neutral-500 dark:text-neutral-400 mb-1">
<span x-text="result.project"></span> / <span
x-text="result.environment"></span>
</div>
</template>
<template x-if="result.description">
<div class="text-sm text-neutral-600 dark:text-neutral-400"
x-text="result.description.length > 80 ? result.description.substring(0, 80) + '...' : result.description">
</div>
</template>
</div>
</a>
@endforeach
</div>
@elseif (strlen($searchQuery) >= 2 && count($searchResults) === 0 && !$autoOpenResource)
<div class="flex items-center justify-center py-12 px-4">
<div class="text-center">
<p class="mt-4 text-sm font-medium text-neutral-900 dark:text-white">
No results found
</p>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
Try different keywords or check the spelling
</p>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-neutral-300 dark:text-neutral-600 self-center"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5l7 7-7 7" />
</svg>
</div>
</a>
</template>
</div>
</template>
<template x-if="filteredCreatableItems.length > 0 && !$wire.isSelectingResource">
<div class="py-2">
<template x-for="[categoryName, items] in Object.entries(groupedCreatableItems)"
:key="categoryName">
<div>
<div class="px-4 pt-3 pb-1">
<h4 class="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider"
x-text="categoryName">
</h4>
</div>
<template x-for="item in items" :key="item.type">
<button type="button" @click="$wire.navigateToResource(item.type)"
class="search-result-item w-full text-left block px-4 py-3 hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-transparent hover:border-yellow-500 focus:border-yellow-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-yellow-100 dark:bg-yellow-900/40 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-yellow-600 dark:text-yellow-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>
<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"
x-text="item.name">
</div>
<span
class="text-xs text-neutral-500 dark:text-neutral-400 shrink-0"
x-text="item.quickcommand"
x-show="item.quickcommand">
</span>
</div>
<div class="text-sm text-neutral-600 dark:text-neutral-400 truncate"
x-text="item.description">
</div>
</div>
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400 self-center"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
</template>
</div>
</template>
</div>
</template>
<template
x-if="searchQuery.length >= 2 && searchResults.length === 0 && filteredCreatableItems.length === 0 && !$wire.isSelectingResource && !$wire.autoOpenResource">
<div class="flex items-center justify-center py-12 px-4">
<div class="text-center">
<p class="mt-4 text-sm font-medium text-neutral-900 dark:text-white">
No results found
</p>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
Try different keywords or check the spelling
</p>
<p class="mt-2 text-xs text-neutral-400 dark:text-neutral-500">
💡 Tip: Search for service names like "wordpress", "postgres", or "redis"
</p>
</div>
@elseif (strlen($searchQuery) > 0 && strlen($searchQuery) < 2)
<div class="flex items-center justify-center py-12 px-4">
<div class="text-center">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
Type at least 2 characters to search
</p>
</div>
</div>
@endif
</div>
</div>
</template>
</div>
@endif
</div>
</div>
</div>
</template>
@ -548,4 +1127,5 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
</div>
</template>
</div>
</div>

View file

@ -1,4 +1,4 @@
<x-forms.select wire:model.live="selectedTeamId" label="Current Team">
<x-forms.select wire:model.live="selectedTeamId">
<option value="default" disabled selected>Switch team</option>
@foreach (auth()->user()->teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option>

View file

@ -7,107 +7,123 @@
services:
museum:
image: ghcr.io/ente-io/server:latest
image: 'ghcr.io/ente-io/server:613c6a96390d7a624cf30b946955705d632423cc' # Released at 2025-09-14T22:16:37-07:00
environment:
- SERVICE_URL_MUSEUM_8080
- ENTE_HTTP_USE_TLS=${ENTE_HTTP_USE_TLS:-false}
- ENTE_APPS_PUBLIC_ALBUMS=${SERVICE_URL_WEB_3002}
- ENTE_APPS_CAST=${SERVICE_URL_WEB_3004}
- ENTE_APPS_ACCOUNTS=${SERVICE_URL_WEB_3001}
- ENTE_DB_HOST=${ENTE_DB_HOST:-postgres}
- ENTE_DB_PORT=${ENTE_DB_PORT:-5432}
- ENTE_DB_NAME=${ENTE_DB_NAME:-ente_db}
- ENTE_DB_USER=${SERVICE_USER_POSTGRES:-pguser}
- ENTE_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
- ENTE_KEY_ENCRYPTION=${SERVICE_REALBASE64_ENCRYPTION}
- ENTE_KEY_HASH=${SERVICE_REALBASE64_64_HASH}
- ENTE_JWT_SECRET=${SERVICE_REALBASE64_JWT}
- ENTE_INTERNAL_ADMIN=${ENTE_INTERNAL_ADMIN:-1580559962386438}
- ENTE_INTERNAL_DISABLE_REGISTRATION=${ENTE_INTERNAL_DISABLE_REGISTRATION:-false}
# S3/MinIO configuration
- S3_ARE_LOCAL_BUCKETS=true
- S3_USE_PATH_STYLE_URLS=true
- S3_B2_EU_CEN_KEY=${SERVICE_USER_MINIO}
- S3_B2_EU_CEN_SECRET=${SERVICE_PASSWORD_MINIO}
- S3_B2_EU_CEN_ENDPOINT=${SERVICE_URL_MINIO_3200}
- S3_B2_EU_CEN_REGION=eu-central-2
- S3_B2_EU_CEN_BUCKET=b2-eu-cen
- ENTE_DB_HOST=postgres
- ENTE_DB_PORT=5432
- 'ENTE_DB_NAME=${POSTGRES_DB:-ente_db}'
- 'ENTE_DB_USER=${SERVICE_USER_POSTGRES}'
- 'ENTE_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES}'
- 'ENTE_HTTP_USE_TLS=${ENTE_HTTP_USE_TLS:-false}'
- ENTE_S3_ARE_LOCAL_BUCKETS=false
- ENTE_S3_USE_PATH_STYLE_URLS=true
- 'ENTE_S3_B2_EU_CEN_KEY=${SERVICE_USER_MINIO}'
- 'ENTE_S3_B2_EU_CEN_SECRET=${SERVICE_PASSWORD_MINIO}'
- 'ENTE_S3_B2_EU_CEN_ENDPOINT=${SERVICE_FQDN_MINIO_9000}'
- ENTE_S3_B2_EU_CEN_REGION=eu-central-2
- ENTE_S3_B2_EU_CEN_BUCKET=b2-eu-cen
- 'ENTE_KEY_ENCRYPTION=${SERVICE_REALBASE64_ENCRYPTION}'
- 'ENTE_KEY_HASH=${SERVICE_REALBASE64_64_HASH}'
- 'ENTE_JWT_SECRET=${SERVICE_REALBASE64_JWT}'
- 'ENTE_INTERNAL_ADMIN=${ENTE_INTERNAL_ADMIN:-1580559962386438}'
- 'ENTE_INTERNAL_DISABLE_REGISTRATION=${ENTE_INTERNAL_DISABLE_REGISTRATION:-false}'
volumes:
- museum-data:/data
- museum-config:/config
- 'museum-data:/data'
- 'museum-config:/config'
depends_on:
postgres:
condition: service_healthy
minio:
condition: service_started
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/ping"]
interval: 5s
timeout: 5s
retries: 10
test:
- CMD
- wget
- '--spider'
- 'http://127.0.0.1:8080/ping'
interval: 30s
timeout: 10s
retries: 3
web:
image: ghcr.io/ente-io/web
image: 'ghcr.io/ente-io/web:ca03165f5e7f2a50105e6e40019c17ae6cdd934f' # Released at 2025-10-08T00:57:05-07:00
environment:
- SERVICE_URL_WEB_3000
- ENTE_API_ORIGIN=${SERVICE_URL_MUSEUM}
- ENTE_ALBUMS_ORIGIN=${SERVICE_URL_WEB_3002}
- 'ENTE_API_ORIGIN=${SERVICE_URL_MUSEUM}'
healthcheck:
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3000"]
interval: 5s
timeout: 5s
retries: 10
test:
- CMD
- curl
- '--fail'
- 'http://localhost:3000'
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
postgres:
image: postgres:15-alpine
image: 'postgres:15-alpine'
environment:
- POSTGRES_USER=${SERVICE_USER_POSTGRES}
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
- POSTGRES_DB=${POSTGRES_DB:-ente_db}
- 'POSTGRES_USER=${SERVICE_USER_POSTGRES}'
- 'POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}'
- 'POSTGRES_DB=${POSTGRES_DB:-ente_db}'
volumes:
- postgres-data:/var/lib/postgresql/data
- 'postgres-data:/var/lib/postgresql/data'
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
test:
- CMD-SHELL
- 'pg_isready -U ${SERVICE_USER_POSTGRES} -d ${POSTGRES_DB:-ente_db}'
interval: 10s
timeout: 5s
retries: 10
retries: 5
minio:
image: quay.io/minio/minio:latest
image: 'quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z' # Released at 2025-09-07T16-13-09Z
command: 'server /data --console-address ":9001"'
environment:
- SERVICE_URL_MINIO_9000
- MINIO_ROOT_USER=${SERVICE_USER_MINIO}
- MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO}
command: server /data --address ":9000" --console-address ":9001"
- MINIO_SERVER_URL=$MINIO_SERVER_URL
- MINIO_BROWSER_REDIRECT_URL=$MINIO_BROWSER_REDIRECT_URL
- MINIO_ROOT_USER=$SERVICE_USER_MINIO
- MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO
volumes:
- minio-data:/data
- 'minio-data:/data'
healthcheck:
test: ["CMD", "mc", "ready", "local"]
test:
- CMD
- mc
- ready
- local
interval: 5s
timeout: 20s
retries: 10
minio-init:
image: minio/mc:latest
exclude_from_hc: true
restart: no
image: 'minio/mc:RELEASE.2025-08-13T08-35-41Z' # Released at 2025-08-13T08-35-41Z
depends_on:
minio:
condition: service_healthy
condition: service_started
restart: on-failure
exclude_from_hc: true
environment:
- MINIO_ROOT_USER=${SERVICE_USER_MINIO}
- MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO}
entrypoint: >
- 'MINIO_ROOT_USER=${SERVICE_USER_MINIO}'
- 'MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO}'
- 'MINIO_CORS_URLS=$SERVICE_URL_MUSEUM,$SERVICE_URL_WEB'
entrypoint: |-
/bin/sh -c "
mc alias set minio http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD};
mc mb minio/b2-eu-cen --ignore-existing;
mc mb minio/wasabi-eu-central-2-v3 --ignore-existing;
mc mb minio/scw-eu-fr-v3 --ignore-existing;
echo 'MinIO buckets created successfully';
echo \"MINIO_CORS_URLS: \$${MINIO_CORS_URLS}\";
sleep 5;
until mc alias set minio http://minio:9000 \$${MINIO_ROOT_USER} \$${MINIO_ROOT_PASSWORD}; do
echo 'Waiting for MinIO...';
sleep 2;
done;
mc admin config set minio api cors_allow_origin='$MINIO_CORS_URLS' || true;
mc mb minio/b2-eu-cen --ignore-existing;
mc mb minio/wasabi-eu-central-2-v3 --ignore-existing;
mc mb minio/scw-eu-fr-v3 --ignore-existing;
echo 'MinIO buckets and CORS configured';
"