diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index 165e4b59e..d6d234b18 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -4,7 +4,9 @@
use App\Models\Project;
use App\Models\Server;
+use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class Select extends Component
@@ -105,7 +107,9 @@ public function updatedSelectedEnvironment()
public function loadServices()
{
$services = get_service_templates();
- $services = collect($services)->map(function ($service, $key) {
+ $templateLastUpdatedMap = $this->serviceTemplateLastUpdatedMap($services->keys());
+
+ $services = collect($services)->map(function ($service, $key) use ($templateLastUpdatedMap) {
$default_logo = 'images/default.webp';
$logo = data_get($service, 'logo', $default_logo);
$local_logo_path = public_path($logo);
@@ -116,6 +120,7 @@ public function loadServices()
'logo_github_url' => file_exists($local_logo_path)
? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo
: asset($default_logo),
+ 'templateLastUpdated' => $templateLastUpdatedMap[(string) $key] ?? null,
] + (array) $service;
})->all();
@@ -247,6 +252,7 @@ public function loadServices()
];
return [
+ 'serviceTemplatesLastUpdated' => $this->serviceTemplatesLastUpdated(),
'services' => $services,
'categories' => $categories,
'gitBasedApplications' => $gitBasedApplications,
@@ -268,6 +274,55 @@ public function instantSave()
}
}
+ private function serviceTemplatesLastUpdated(): ?string
+ {
+ return $this->formatLastModified($this->serviceTemplatesPath());
+ }
+
+ private function serviceTemplateLastUpdatedMap(Collection $serviceNames): array
+ {
+ $bundleMtime = file_exists($this->serviceTemplatesPath()) ? filemtime($this->serviceTemplatesPath()) : 0;
+
+ return Cache::remember(
+ "service-template-last-updated-map:{$bundleMtime}",
+ now()->addDay(),
+ fn () => $serviceNames
+ ->mapWithKeys(fn ($serviceName) => [
+ (string) $serviceName => $this->serviceTemplateLastUpdated((string) $serviceName),
+ ])
+ ->all()
+ );
+ }
+
+ private function serviceTemplateLastUpdated(string $serviceName): ?string
+ {
+ foreach (['yaml', 'yml'] as $extension) {
+ $templatePath = base_path("templates/compose/{$serviceName}.{$extension}");
+
+ if (file_exists($templatePath)) {
+ return $this->formatLastModified($templatePath);
+ }
+ }
+
+ return null;
+ }
+
+ private function serviceTemplatesPath(): string
+ {
+ return base_path('templates/'.config('constants.services.file_name'));
+ }
+
+ private function formatLastModified(string $path): ?string
+ {
+ if (! file_exists($path)) {
+ return null;
+ }
+
+ return CarbonImmutable::createFromTimestamp(filemtime($path))
+ ->timezone(config('app.timezone'))
+ ->format('M j, Y H:i');
+ }
+
public function setType(string $type)
{
$type = str($type)->lower()->slug()->value();
diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php
index c5482d9f7..debe3326f 100644
--- a/resources/views/livewire/project/new/select.blade.php
+++ b/resources/views/livewire/project/new/select.blade.php
@@ -138,9 +138,14 @@ class="w-full h-full p-2 transition-all duration-200 dark:bg-white/10 bg-black/1
-
+
Services
Reload List
+
+ Last Updated on Service Templates:
+
+
The respective trademarks mentioned here are owned by the respective companies, and use of them
@@ -154,7 +159,14 @@ class="w-full h-full p-2 transition-all duration-200 dark:bg-white/10 bg-black/1
-
+
@@ -237,6 +249,7 @@ function searchResources() {
isSticky: false,
selecting: false,
services: [],
+ serviceTemplatesLastUpdated: null,
gitBasedApplications: [],
dockerBasedApplications: [],
databases: [],
@@ -251,12 +264,14 @@ function searchResources() {
this.loading = true;
const {
services,
+ serviceTemplatesLastUpdated,
categories,
gitBasedApplications,
dockerBasedApplications,
databases
} = await this.$wire.loadServices();
this.services = services;
+ this.serviceTemplatesLastUpdated = serviceTemplatesLastUpdated;
this.categories = categories || [];
this.gitBasedApplications = gitBasedApplications;
this.dockerBasedApplications = dockerBasedApplications;
diff --git a/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php b/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php
new file mode 100644
index 000000000..99b9f7ad7
--- /dev/null
+++ b/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php
@@ -0,0 +1,70 @@
+loadServices();
+
+ expect($resources)
+ ->toHaveKey('serviceTemplatesLastUpdated')
+ ->and($resources['serviceTemplatesLastUpdated'])
+ ->toBe(CarbonImmutable::createFromTimestamp(filemtime($templatePath))->timezone(config('app.timezone'))->format('M j, Y H:i'));
+});
+
+it('returns each service template last updated timestamp', function () {
+ $component = new Select;
+ $templatePath = base_path('templates/compose/activepieces.yaml');
+
+ $resources = $component->loadServices();
+
+ expect($resources['services']['activepieces'])
+ ->toHaveKey('templateLastUpdated')
+ ->and($resources['services']['activepieces']['templateLastUpdated'])
+ ->toBe(CarbonImmutable::createFromTimestamp(filemtime($templatePath))->timezone(config('app.timezone'))->format('M j, Y H:i'));
+});
+
+it('uses a service template timestamp cache keyed by bundle mtime', function () {
+ $bundleMtime = filemtime(base_path('templates/'.config('constants.services.file_name')));
+ Cache::put("service-template-last-updated-map:{$bundleMtime}", [
+ 'activepieces' => 'Cached timestamp',
+ ], now()->addDay());
+
+ $resources = (new Select)->loadServices();
+
+ expect($resources['services']['activepieces']['templateLastUpdated'])->toBe('Cached timestamp');
+});
+
+it('does not use stale service template timestamp cache entries from another bundle mtime', function () {
+ $bundleMtime = filemtime(base_path('templates/'.config('constants.services.file_name')));
+ Cache::put('service-template-last-updated-map:'.($bundleMtime - 1), [
+ 'activepieces' => 'Stale cached timestamp',
+ ], now()->addDay());
+
+ $resources = (new Select)->loadServices();
+
+ expect($resources['services']['activepieces']['templateLastUpdated'])->not->toBe('Stale cached timestamp');
+});
+
+it('renders the service templates last updated hint placeholder', function () {
+ View::share('errors', new ViewErrorBag);
+
+ $view = $this->view('livewire.project.new.select', [
+ 'current_step' => 'type',
+ 'environments' => collect(),
+ ]);
+
+ $view->assertSee('Last Updated on Service Templates:');
+ $view->assertSee('serviceTemplatesLastUpdated');
+ $view->assertSee('service.templateLastUpdated');
+});