From d8d316b5f8c74e55ac3726c70067e947bac9d0d5 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 10:17:48 +0200
Subject: [PATCH 01/24] feat(search): implement global search functionality
with caching and modal interface
---
app/Livewire/GlobalSearch.php | 371 ++++++++++++++++++
app/Models/Application.php | 123 +++---
app/Models/Server.php | 3 +-
app/Models/Service.php | 3 +-
app/Models/StandaloneClickhouse.php | 8 +-
app/Models/StandaloneDragonfly.php | 8 +-
app/Models/StandaloneKeydb.php | 8 +-
app/Models/StandaloneMariadb.php | 8 +-
app/Models/StandaloneMongodb.php | 8 +-
app/Models/StandaloneMysql.php | 8 +-
app/Models/StandalonePostgresql.php | 8 +-
app/Models/StandaloneRedis.php | 8 +-
app/Traits/ClearsGlobalSearchCache.php | 53 +++
resources/views/components/navbar.blade.php | 27 +-
.../views/livewire/global-search.blade.php | 236 +++++++++++
15 files changed, 797 insertions(+), 83 deletions(-)
create mode 100644 app/Livewire/GlobalSearch.php
create mode 100644 app/Traits/ClearsGlobalSearchCache.php
create mode 100644 resources/views/livewire/global-search.blade.php
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
new file mode 100644
index 000000000..3b3075fc9
--- /dev/null
+++ b/app/Livewire/GlobalSearch.php
@@ -0,0 +1,371 @@
+searchQuery = '';
+ $this->isModalOpen = false;
+ $this->searchResults = [];
+ $this->allSearchableItems = [];
+ }
+
+ public function openSearchModal()
+ {
+ $this->isModalOpen = true;
+ $this->loadSearchableItems();
+ $this->dispatch('search-modal-opened');
+ }
+
+ public function closeSearchModal()
+ {
+ $this->isModalOpen = false;
+ $this->searchQuery = '';
+ $this->searchResults = [];
+ }
+
+ public static function getCacheKey($teamId)
+ {
+ return 'global_search_items_'.$teamId;
+ }
+
+ public static function clearTeamCache($teamId)
+ {
+ Cache::forget(self::getCacheKey($teamId));
+ }
+
+ public function updatedSearchQuery()
+ {
+ $this->search();
+ }
+
+ private function loadSearchableItems()
+ {
+ // Try to get from Redis cache first
+ $cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id);
+
+ $this->allSearchableItems = Cache::remember($cacheKey, 300, function () {
+ $items = collect();
+ $team = auth()->user()->currentTeam();
+
+ // Get all applications
+ $applications = Application::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($app) {
+ // Collect all FQDNs from the application
+ $fqdns = collect([]);
+
+ // For regular applications
+ if ($app->fqdn) {
+ $fqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn));
+ }
+
+ // For docker compose based applications
+ if ($app->build_pack === 'dockercompose' && $app->docker_compose_domains) {
+ try {
+ $composeDomains = json_decode($app->docker_compose_domains, true);
+ if (is_array($composeDomains)) {
+ foreach ($composeDomains as $serviceName => $domains) {
+ if (is_array($domains)) {
+ $fqdns = $fqdns->merge($domains);
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ // Ignore JSON parsing errors
+ }
+ }
+
+ $fqdnsString = $fqdns->implode(' ');
+
+ return [
+ 'id' => $app->id,
+ 'name' => $app->name,
+ 'type' => 'application',
+ 'uuid' => $app->uuid,
+ 'description' => $app->description,
+ 'link' => $app->link(),
+ 'project' => $app->environment->project->name ?? null,
+ 'environment' => $app->environment->name ?? null,
+ 'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
+ 'search_text' => strtolower($app->name.' '.$app->description.' '.$fqdnsString),
+ ];
+ });
+
+ // Get all services
+ $services = Service::ownedByCurrentTeam()
+ ->with(['environment.project', 'applications'])
+ ->get()
+ ->map(function ($service) {
+ // Collect all FQDNs from service applications
+ $fqdns = collect([]);
+ foreach ($service->applications as $app) {
+ if ($app->fqdn) {
+ $appFqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn));
+ $fqdns = $fqdns->merge($appFqdns);
+ }
+ }
+ $fqdnsString = $fqdns->implode(' ');
+
+ return [
+ 'id' => $service->id,
+ 'name' => $service->name,
+ 'type' => 'service',
+ 'uuid' => $service->uuid,
+ 'description' => $service->description,
+ 'link' => $service->link(),
+ 'project' => $service->environment->project->name ?? null,
+ 'environment' => $service->environment->name ?? null,
+ 'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
+ 'search_text' => strtolower($service->name.' '.$service->description.' '.$fqdnsString),
+ ];
+ });
+
+ // Get all standalone databases
+ $databases = collect();
+
+ // PostgreSQL
+ $databases = $databases->merge(
+ StandalonePostgresql::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'postgresql',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' postgresql '.$db->description),
+ ];
+ })
+ );
+
+ // MySQL
+ $databases = $databases->merge(
+ StandaloneMysql::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'mysql',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' mysql '.$db->description),
+ ];
+ })
+ );
+
+ // MariaDB
+ $databases = $databases->merge(
+ StandaloneMariadb::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'mariadb',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' mariadb '.$db->description),
+ ];
+ })
+ );
+
+ // MongoDB
+ $databases = $databases->merge(
+ StandaloneMongodb::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'mongodb',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' mongodb '.$db->description),
+ ];
+ })
+ );
+
+ // Redis
+ $databases = $databases->merge(
+ StandaloneRedis::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'redis',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' redis '.$db->description),
+ ];
+ })
+ );
+
+ // KeyDB
+ $databases = $databases->merge(
+ StandaloneKeydb::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'keydb',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' keydb '.$db->description),
+ ];
+ })
+ );
+
+ // Dragonfly
+ $databases = $databases->merge(
+ StandaloneDragonfly::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'dragonfly',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' dragonfly '.$db->description),
+ ];
+ })
+ );
+
+ // Clickhouse
+ $databases = $databases->merge(
+ StandaloneClickhouse::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'clickhouse',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' clickhouse '.$db->description),
+ ];
+ })
+ );
+
+ // Get all servers
+ $servers = Server::ownedByCurrentTeam()
+ ->get()
+ ->map(function ($server) {
+ return [
+ 'id' => $server->id,
+ 'name' => $server->name,
+ 'type' => 'server',
+ 'uuid' => $server->uuid,
+ 'description' => $server->description,
+ 'link' => $server->url(),
+ 'project' => null,
+ 'environment' => null,
+ 'search_text' => strtolower($server->name.' '.$server->ip.' '.$server->description),
+ ];
+ });
+
+ // Merge all collections
+ $items = $items->merge($applications)
+ ->merge($services)
+ ->merge($databases)
+ ->merge($servers);
+
+ return $items->toArray();
+ });
+ }
+
+ private function search()
+ {
+ if (strlen($this->searchQuery) < 2) {
+ $this->searchResults = [];
+
+ return;
+ }
+
+ $query = strtolower($this->searchQuery);
+
+ // Case-insensitive search in the items
+ $this->searchResults = collect($this->allSearchableItems)
+ ->filter(function ($item) use ($query) {
+ return str_contains($item['search_text'], $query);
+ })
+ ->take(20)
+ ->values()
+ ->toArray();
+ }
+
+ public function render()
+ {
+ return view('livewire.global-search');
+ }
+}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 07df53687..094e5c82b 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -4,6 +4,7 @@ namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use App\Services\ConfigurationGenerator;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasConfiguration;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -110,7 +111,7 @@ use Visus\Cuid2\Cuid2;
class Application extends BaseModel
{
- use HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
private static $parserVersion = '5';
@@ -123,66 +124,6 @@ class Application extends BaseModel
'http_basic_auth_password' => 'encrypted',
];
- public function customNetworkAliases(): Attribute
- {
- return Attribute::make(
- set: function ($value) {
- if (is_null($value) || $value === '') {
- return null;
- }
-
- // If it's already a JSON string, decode it
- if (is_string($value) && $this->isJson($value)) {
- $value = json_decode($value, true);
- }
-
- // If it's a string but not JSON, treat it as a comma-separated list
- if (is_string($value) && ! is_array($value)) {
- $value = explode(',', $value);
- }
-
- $value = collect($value)
- ->map(function ($alias) {
- if (is_string($alias)) {
- return str_replace(' ', '-', trim($alias));
- }
-
- return null;
- })
- ->filter()
- ->unique() // Remove duplicate values
- ->values()
- ->toArray();
-
- return empty($value) ? null : json_encode($value);
- },
- get: function ($value) {
- if (is_null($value)) {
- return null;
- }
-
- if (is_string($value) && $this->isJson($value)) {
- return json_decode($value, true);
- }
-
- return is_array($value) ? $value : [];
- }
- );
- }
-
- /**
- * Check if a string is a valid JSON
- */
- private function isJson($string)
- {
- if (! is_string($string)) {
- return false;
- }
- json_decode($string);
-
- return json_last_error() === JSON_ERROR_NONE;
- }
-
protected static function booted()
{
static::addGlobalScope('withRelations', function ($builder) {
@@ -250,6 +191,66 @@ class Application extends BaseModel
});
}
+ public function customNetworkAliases(): Attribute
+ {
+ return Attribute::make(
+ set: function ($value) {
+ if (is_null($value) || $value === '') {
+ return null;
+ }
+
+ // If it's already a JSON string, decode it
+ if (is_string($value) && $this->isJson($value)) {
+ $value = json_decode($value, true);
+ }
+
+ // If it's a string but not JSON, treat it as a comma-separated list
+ if (is_string($value) && ! is_array($value)) {
+ $value = explode(',', $value);
+ }
+
+ $value = collect($value)
+ ->map(function ($alias) {
+ if (is_string($alias)) {
+ return str_replace(' ', '-', trim($alias));
+ }
+
+ return null;
+ })
+ ->filter()
+ ->unique() // Remove duplicate values
+ ->values()
+ ->toArray();
+
+ return empty($value) ? null : json_encode($value);
+ },
+ get: function ($value) {
+ if (is_null($value)) {
+ return null;
+ }
+
+ if (is_string($value) && $this->isJson($value)) {
+ return json_decode($value, true);
+ }
+
+ return is_array($value) ? $value : [];
+ }
+ );
+ }
+
+ /**
+ * Check if a string is a valid JSON
+ */
+ private function isJson($string)
+ {
+ if (! is_string($string)) {
+ return false;
+ }
+ json_decode($string);
+
+ return json_last_error() === JSON_ERROR_NONE;
+ }
+
public static function ownedByCurrentTeamAPI(int $teamId)
{
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
diff --git a/app/Models/Server.php b/app/Models/Server.php
index cc5315c6f..829a4b5aa 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -13,6 +13,7 @@ use App\Jobs\RegenerateSslCertJob;
use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
use App\Services\ConfigurationRepository;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -55,7 +56,7 @@ use Visus\Cuid2\Cuid2;
class Server extends BaseModel
{
- use HasFactory, SchemalessAttributesTrait, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;
diff --git a/app/Models/Service.php b/app/Models/Service.php
index dd8d0ac7e..d42d471c6 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\ProcessStatus;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -41,7 +42,7 @@ use Visus\Cuid2\Cuid2;
)]
class Service extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
private static $parserVersion = '5';
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index 87c5c3422..146ee0a2d 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneClickhouse extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -43,6 +44,11 @@ class StandaloneClickhouse extends BaseModel
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneClickhouse::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 118c72726..90e7304f1 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneDragonfly extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -43,6 +44,11 @@ class StandaloneDragonfly extends BaseModel
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneDragonfly::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index 9d674b6c2..ad0cabf7e 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneKeydb extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -43,6 +44,11 @@ class StandaloneKeydb extends BaseModel
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneKeydb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index 616d536c1..3d9e38147 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMariadb extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -44,6 +45,11 @@ class StandaloneMariadb extends BaseModel
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneMariadb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index b26b6c967..7cccd332a 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMongodb extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -46,6 +47,11 @@ class StandaloneMongodb extends BaseModel
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneMongodb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index 7b6f1b94e..80269972f 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMysql extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -44,6 +45,11 @@ class StandaloneMysql extends BaseModel
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneMysql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index f13e6ffab..acde7a20c 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandalonePostgresql extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -44,6 +45,11 @@ class StandalonePostgresql extends BaseModel
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandalonePostgresql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
public function workdir()
{
return database_configuration_dir()."/{$this->uuid}";
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index 9f7c96a08..001ebe36a 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneRedis extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -45,6 +46,11 @@ class StandaloneRedis extends BaseModel
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneRedis::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php
new file mode 100644
index 000000000..fe6cbaa38
--- /dev/null
+++ b/app/Traits/ClearsGlobalSearchCache.php
@@ -0,0 +1,53 @@
+getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
+ });
+
+ static::created(function ($model) {
+ // Clear search cache when model is created
+ $teamId = $model->getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
+ });
+
+ static::deleted(function ($model) {
+ // Clear search cache when model is deleted
+ $teamId = $model->getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
+ });
+ }
+
+ private function getTeamIdForCache()
+ {
+ // For database models, team is accessed through environment.project.team
+ if (method_exists($this, 'team')) {
+ $team = $this->team();
+ if (filled($team)) {
+ return is_object($team) ? $team->id : null;
+ }
+ }
+
+ // For models with direct team_id property
+ if (property_exists($this, 'team_id') || isset($this->team_id)) {
+ return $this->team_id;
+ }
+
+ return null;
+ }
+}
diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php
index f61ea681e..1c5987e82 100644
--- a/resources/views/components/navbar.blade.php
+++ b/resources/views/components/navbar.blade.php
@@ -59,20 +59,20 @@
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%;
+ font-size: 93.75%;
}
- }
- `;
+
+ :root {
+ --vh: 1vh;
+ }
+
+ @media (min-width: 1024px) {
+ html {
+ font-size: 87.5%;
+ }
+ }
+ `;
document.head.appendChild(style);
}
}
@@ -82,6 +82,9 @@
Coolify
+
+
+
diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php
new file mode 100644
index 000000000..0792dadfb
--- /dev/null
+++ b/resources/views/livewire/global-search.blade.php
@@ -0,0 +1,236 @@
+
+
+
From 575793709bfb256e922a1a58158dbc18fa46b974 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 10:22:24 +0200
Subject: [PATCH 02/24] feat(search): enable query logging for global search
caching
---
app/Livewire/GlobalSearch.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
index 3b3075fc9..dacc0d4db 100644
--- a/app/Livewire/GlobalSearch.php
+++ b/app/Livewire/GlobalSearch.php
@@ -69,6 +69,7 @@ class GlobalSearch extends Component
$cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id);
$this->allSearchableItems = Cache::remember($cacheKey, 300, function () {
+ ray()->showQueries();
$items = collect();
$team = auth()->user()->currentTeam();
From f2236236039f966b856d9833f14dbf621d7e7a24 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 10:22:31 +0200
Subject: [PATCH 03/24] refactor(search): optimize cache clearing logic to only
trigger on searchable field changes
---
app/Traits/ClearsGlobalSearchCache.php | 42 +++++++++++++++++++++-----
1 file changed, 35 insertions(+), 7 deletions(-)
diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php
index fe6cbaa38..0bcc5d319 100644
--- a/app/Traits/ClearsGlobalSearchCache.php
+++ b/app/Traits/ClearsGlobalSearchCache.php
@@ -8,16 +8,18 @@ trait ClearsGlobalSearchCache
{
protected static function bootClearsGlobalSearchCache()
{
- static::saved(function ($model) {
- // Clear search cache when model is saved
- $teamId = $model->getTeamIdForCache();
- if (filled($teamId)) {
- GlobalSearch::clearTeamCache($teamId);
+ static::saving(function ($model) {
+ // Only clear cache if searchable fields are being changed
+ if ($model->hasSearchableChanges()) {
+ $teamId = $model->getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
}
});
static::created(function ($model) {
- // Clear search cache when model is created
+ // Always clear cache when model is created
$teamId = $model->getTeamIdForCache();
if (filled($teamId)) {
GlobalSearch::clearTeamCache($teamId);
@@ -25,7 +27,7 @@ trait ClearsGlobalSearchCache
});
static::deleted(function ($model) {
- // Clear search cache when model is deleted
+ // Always clear cache when model is deleted
$teamId = $model->getTeamIdForCache();
if (filled($teamId)) {
GlobalSearch::clearTeamCache($teamId);
@@ -33,6 +35,32 @@ trait ClearsGlobalSearchCache
});
}
+ private function hasSearchableChanges(): bool
+ {
+ // Define searchable fields based on model type
+ $searchableFields = ['name', 'description'];
+
+ // Add model-specific searchable fields
+ if ($this instanceof \App\Models\Application) {
+ $searchableFields[] = 'fqdn';
+ $searchableFields[] = 'docker_compose_domains';
+ } elseif ($this instanceof \App\Models\Server) {
+ $searchableFields[] = 'ip';
+ } elseif ($this instanceof \App\Models\Service) {
+ // Services don't have direct fqdn, but name and description are covered
+ }
+ // Database models only have name and description as searchable
+
+ // Check if any searchable field is dirty
+ foreach ($searchableFields as $field) {
+ if ($this->isDirty($field)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private function getTeamIdForCache()
{
// For database models, team is accessed through environment.project.team
From 65f24de101b4764505662534ec83ded4bc40d57e Mon Sep 17 00:00:00 2001
From: ShadowArcanist
Date: Fri, 19 Sep 2025 16:26:11 +0530
Subject: [PATCH 04/24] Changed Sentinel metrics color from yellow to blue +
cyan (cpu + memory)
---
resources/views/layouts/base.blade.php | 9 ++-
.../livewire/project/shared/metrics.blade.php | 74 ++++++++++---------
.../views/livewire/server/charts.blade.php | 60 +++++++--------
3 files changed, 75 insertions(+), 68 deletions(-)
diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php
index ebb134324..c074412d3 100644
--- a/resources/views/layouts/base.blade.php
+++ b/resources/views/layouts/base.blade.php
@@ -138,7 +138,8 @@
}
}
let theme = localStorage.theme
- let baseColor = '#FCD452'
+ let cpuColor = '#1e90ff'
+ let ramColor = '#00ced1'
let textColor = '#ffffff'
let editorBackground = '#181818'
let editorTheme = 'blackboard'
@@ -149,12 +150,14 @@
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
if (theme == 'dark') {
- baseColor = '#FCD452'
+ cpuColor = '#1e90ff'
+ ramColor = '#00ced1'
textColor = '#ffffff'
editorBackground = '#181818'
editorTheme = 'blackboard'
} else {
- baseColor = 'black'
+ cpuColor = '#1e90ff'
+ ramColor = '#00ced1'
textColor = '#000000'
editorBackground = '#ffffff'
editorTheme = null
diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php
index cfe83ded6..d6609d9e6 100644
--- a/resources/views/livewire/project/shared/metrics.blade.php
+++ b/resources/views/livewire/project/shared/metrics.blade.php
@@ -34,6 +34,7 @@
const optionsServerCpu = {
stroke: {
curve: 'straight',
+ width: 2,
},
chart: {
height: '150px',
@@ -68,16 +69,16 @@
enabled: false,
}
},
- grid: {
- show: true,
- borderColor: '',
- },
- colors: [baseColor],
- xaxis: {
- type: 'datetime',
- },
- series: [{
- name: "CPU %",
+ grid: {
+ show: true,
+ borderColor: '',
+ },
+ colors: [cpuColor],
+ xaxis: {
+ type: 'datetime',
+ },
+ series: [{
+ name: "CPU %",
data: []
}],
noData: {
@@ -101,11 +102,11 @@
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
checkTheme();
- serverCpuChart.updateOptions({
- series: [{
- data: chartData[0].seriesData,
- }],
- colors: [baseColor],
+ serverCpuChart.updateOptions({
+ series: [{
+ data: chartData[0].seriesData,
+ }],
+ colors: [cpuColor],
xaxis: {
type: 'datetime',
labels: {
@@ -143,6 +144,7 @@
const optionsServerMemory = {
stroke: {
curve: 'straight',
+ width: 2,
},
chart: {
height: '150px',
@@ -177,22 +179,22 @@
enabled: false,
}
},
- grid: {
- show: true,
- borderColor: '',
- },
- colors: [baseColor],
- xaxis: {
- type: 'datetime',
- labels: {
- show: true,
- style: {
- colors: textColor,
- }
- }
- },
- series: [{
- name: "Memory (MB)",
+ grid: {
+ show: true,
+ borderColor: '',
+ },
+ colors: [ramColor],
+ xaxis: {
+ type: 'datetime',
+ labels: {
+ show: true,
+ style: {
+ colors: textColor,
+ }
+ }
+ },
+ series: [{
+ name: "Memory (MB)",
data: []
}],
noData: {
@@ -217,11 +219,11 @@
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
checkTheme();
- serverMemoryChart.updateOptions({
- series: [{
- data: chartData[0].seriesData,
- }],
- colors: [baseColor],
+ serverMemoryChart.updateOptions({
+ series: [{
+ data: chartData[0].seriesData,
+ }],
+ colors: [ramColor],
xaxis: {
type: 'datetime',
labels: {
diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php
index b84e0240f..f5a2418fd 100644
--- a/resources/views/livewire/server/charts.blade.php
+++ b/resources/views/livewire/server/charts.blade.php
@@ -27,6 +27,7 @@
const optionsServerCpu = {
stroke: {
curve: 'straight',
+ width: 2,
},
chart: {
height: '150px',
@@ -61,16 +62,16 @@
enabled: false,
}
},
- grid: {
- show: true,
- borderColor: '',
- },
- colors: [baseColor],
- xaxis: {
- type: 'datetime',
- },
- series: [{
- name: 'CPU %',
+ grid: {
+ show: true,
+ borderColor: '',
+ },
+ colors: [cpuColor],
+ xaxis: {
+ type: 'datetime',
+ },
+ series: [{
+ name: 'CPU %',
data: []
}],
noData: {
@@ -95,11 +96,11 @@
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
checkTheme();
- serverCpuChart.updateOptions({
- series: [{
- data: chartData[0].seriesData,
- }],
- colors: [baseColor],
+ serverCpuChart.updateOptions({
+ series: [{
+ data: chartData[0].seriesData,
+ }],
+ colors: [cpuColor],
xaxis: {
type: 'datetime',
labels: {
@@ -138,6 +139,7 @@
const optionsServerMemory = {
stroke: {
curve: 'straight',
+ width: 2,
},
chart: {
height: '150px',
@@ -172,15 +174,15 @@
enabled: false,
}
},
- grid: {
- show: true,
- borderColor: '',
- },
- colors: [baseColor],
- xaxis: {
- type: 'datetime',
- labels: {
- show: true,
+ grid: {
+ show: true,
+ borderColor: '',
+ },
+ colors: [ramColor],
+ xaxis: {
+ type: 'datetime',
+ labels: {
+ show: true,
style: {
colors: textColor,
}
@@ -212,11 +214,11 @@
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
checkTheme();
- serverMemoryChart.updateOptions({
- series: [{
- data: chartData[0].seriesData,
- }],
- colors: [baseColor],
+ serverMemoryChart.updateOptions({
+ series: [{
+ data: chartData[0].seriesData,
+ }],
+ colors: [ramColor],
xaxis: {
type: 'datetime',
labels: {
From bfaefed1aea4864eb30e6c813a919279bae4e785 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 13:45:37 +0200
Subject: [PATCH 05/24] refactor(environment): streamline rendering of Docker
Build Secrets checkbox and adjust layout for environment variable settings
---
.../shared/environment-variable/all.blade.php | 24 ++++++-------
.../environment-variable/show.blade.php | 34 +++++++++----------
2 files changed, 27 insertions(+), 31 deletions(-)
diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php
index 6854ffaa4..cee6b291d 100644
--- a/resources/views/livewire/project/shared/environment-variable/all.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php
@@ -28,19 +28,17 @@
@endcan
@endif
- @if (data_get($resource, 'build_pack') !== 'dockercompose')
-
- @can('manageEnvironment', $resource)
-
- @else
-
- @endcan
-
- @endif
+
+ @can('manageEnvironment', $resource)
+
+ @else
+
+ @endcan
+
@endif
@if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose')
diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php
index 6598b66ff..953bc59fa 100644
--- a/resources/views/livewire/project/shared/environment-variable/show.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php
@@ -78,22 +78,20 @@
@if ($isSharedVariable)
@else
- @if (!$env->is_coolify)
- @if (!$env->is_nixpacks)
-
- @endif
-
- @if (!$env->is_nixpacks)
-
- @if ($is_multiline === false)
-
- @endif
+ @if (!$env->is_nixpacks)
+
+ @endif
+
+ @if (!$env->is_nixpacks)
+
+ @if ($is_multiline === false)
+
@endif
@endif
@endif
@@ -129,8 +127,8 @@
@if (!$is_redis_credential)
@if ($type === 'service')
+ helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
+ label="Available at Buildtime" />
From 593c1b476743b0129d7a346c8232d835ecb18600 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 13:46:00 +0200
Subject: [PATCH 06/24] fix(deployment): enhance Dockerfile modification for
build-time variables and secrets during deployment in case of docker compose
buildpack
---
app/Jobs/ApplicationDeploymentJob.php | 172 +++++++++++++++++++++++++-
1 file changed, 170 insertions(+), 2 deletions(-)
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index ae89649af..c880057e5 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -606,6 +606,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
'hidden' => true,
]);
+
+ // Modify Dockerfiles for ARGs and build secrets
+ $this->modify_dockerfiles_for_compose($composeFile);
// Build new container to limit downtime.
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
@@ -632,6 +635,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else {
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull";
}
+
+ if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
+ $build_args_string = $this->build_args->implode(' ');
+ $command .= " {$build_args_string}";
+ $this->application_deployment_queue->addLogEntry('Adding build arguments to Docker Compose build command.');
+ }
+
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
@@ -2830,8 +2840,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
// Get environment variables for secrets
$variables = $this->pull_request_id === 0
- ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get()
- : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get();
+ ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get()
+ : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get();
if ($variables->isEmpty()) {
return;
@@ -2868,6 +2878,164 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
}
+ private function modify_dockerfiles_for_compose($composeFile)
+ {
+ if ($this->application->build_pack !== 'dockercompose') {
+ return;
+ }
+
+ $variables = $this->pull_request_id === 0
+ ? $this->application->environment_variables()
+ ->where('key', 'not like', 'NIXPACKS_%')
+ ->where('is_buildtime', true)
+ ->get()
+ : $this->application->environment_variables_preview()
+ ->where('key', 'not like', 'NIXPACKS_%')
+ ->where('is_buildtime', true)
+ ->get();
+
+ if ($variables->isEmpty()) {
+ $this->application_deployment_queue->addLogEntry('No build-time variables to add to Dockerfiles.');
+
+ return;
+ }
+
+ $services = data_get($composeFile, 'services', []);
+
+ foreach ($services as $serviceName => $service) {
+ if (! isset($service['build'])) {
+ continue;
+ }
+
+ $context = '.';
+ $dockerfile = 'Dockerfile';
+
+ if (is_string($service['build'])) {
+ $context = $service['build'];
+ } elseif (is_array($service['build'])) {
+ $context = data_get($service['build'], 'context', '.');
+ $dockerfile = data_get($service['build'], 'dockerfile', 'Dockerfile');
+ }
+
+ $dockerfilePath = rtrim($context, '/').'/'.ltrim($dockerfile, '/');
+ if (str_starts_with($dockerfilePath, './')) {
+ $dockerfilePath = substr($dockerfilePath, 2);
+ }
+ if (str_starts_with($dockerfilePath, '/')) {
+ $dockerfilePath = substr($dockerfilePath, 1);
+ }
+
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "test -f {$this->workdir}/{$dockerfilePath} && echo 'exists' || echo 'not found'"),
+ 'hidden' => true,
+ 'save' => 'dockerfile_check_'.$serviceName,
+ ]);
+
+ if (str($this->saved_outputs->get('dockerfile_check_'.$serviceName))->trim()->toString() !== 'exists') {
+ $this->application_deployment_queue->addLogEntry("Dockerfile not found for service {$serviceName} at {$dockerfilePath}, skipping ARG injection.");
+
+ continue;
+ }
+
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$dockerfilePath}"),
+ 'hidden' => true,
+ 'save' => 'dockerfile_content_'.$serviceName,
+ ]);
+
+ $dockerfileContent = $this->saved_outputs->get('dockerfile_content_'.$serviceName);
+ if (! $dockerfileContent) {
+ continue;
+ }
+
+ $dockerfile_lines = collect(str($dockerfileContent)->trim()->explode("\n"));
+
+ $fromIndices = [];
+ $dockerfile_lines->each(function ($line, $index) use (&$fromIndices) {
+ if (str($line)->trim()->startsWith('FROM')) {
+ $fromIndices[] = $index;
+ }
+ });
+
+ if (empty($fromIndices)) {
+ $this->application_deployment_queue->addLogEntry("No FROM instruction found in Dockerfile for service {$serviceName}, skipping.");
+
+ continue;
+ }
+
+ $isMultiStage = count($fromIndices) > 1;
+
+ $argsToAdd = collect([]);
+ foreach ($variables as $env) {
+ $argsToAdd->push("ARG {$env->key}");
+ }
+
+ ray($argsToAdd);
+ if ($argsToAdd->isEmpty()) {
+ $this->application_deployment_queue->addLogEntry("Service {$serviceName}: No build-time variables to add.");
+
+ continue;
+ }
+
+ $totalAdded = 0;
+ $offset = 0;
+
+ foreach ($fromIndices as $stageIndex => $fromIndex) {
+ $adjustedIndex = $fromIndex + $offset;
+
+ $stageStart = $adjustedIndex + 1;
+ $stageEnd = isset($fromIndices[$stageIndex + 1])
+ ? $fromIndices[$stageIndex + 1] + $offset
+ : $dockerfile_lines->count();
+
+ $existingStageArgs = collect([]);
+ for ($i = $stageStart; $i < $stageEnd; $i++) {
+ $line = $dockerfile_lines->get($i);
+ if (! $line || ! str($line)->trim()->startsWith('ARG')) {
+ break;
+ }
+ $parts = explode(' ', trim($line), 2);
+ if (count($parts) >= 2) {
+ $argPart = $parts[1];
+ $keyValue = explode('=', $argPart, 2);
+ $existingStageArgs->push($keyValue[0]);
+ }
+ }
+
+ $stageArgsToAdd = $argsToAdd->filter(function ($arg) use ($existingStageArgs) {
+ $key = str($arg)->after('ARG ')->trim()->toString();
+
+ return ! $existingStageArgs->contains($key);
+ });
+
+ if ($stageArgsToAdd->isNotEmpty()) {
+ $dockerfile_lines->splice($adjustedIndex + 1, 0, $stageArgsToAdd->toArray());
+ $totalAdded += $stageArgsToAdd->count();
+ $offset += $stageArgsToAdd->count();
+ }
+ }
+
+ if ($totalAdded > 0) {
+ $dockerfile_base64 = base64_encode($dockerfile_lines->implode("\n"));
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}/{$dockerfilePath} > /dev/null"),
+ 'hidden' => true,
+ ]);
+
+ $stageInfo = $isMultiStage ? ' (multi-stage build, added to '.count($fromIndices).' stages)' : '';
+ $this->application_deployment_queue->addLogEntry("Added {$totalAdded} ARG declarations to Dockerfile for service {$serviceName}{$stageInfo}.");
+ } else {
+ $this->application_deployment_queue->addLogEntry("Service {$serviceName}: All required ARG declarations already exist.");
+ }
+
+ if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
+ $fullDockerfilePath = "{$this->workdir}/{$dockerfilePath}";
+ $this->modify_dockerfile_for_secrets($fullDockerfilePath);
+ $this->application_deployment_queue->addLogEntry("Modified Dockerfile for service {$serviceName} to use build secrets.");
+ }
+ }
+ }
+
private function add_build_secrets_to_compose($composeFile)
{
// Get environment variables for secrets
From 99fd4b424d186c6557c3f48aa43708935c827bef Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 14:17:10 +0200
Subject: [PATCH 07/24] feat(environment): add dynamic checkbox options for
environment variable settings based on user permissions and variable types
---
.../environment-variable/show.blade.php | 89 +++++++++++++++++++
1 file changed, 89 insertions(+)
diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php
index 953bc59fa..a04b477d5 100644
--- a/resources/views/livewire/project/shared/environment-variable/show.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php
@@ -21,6 +21,95 @@
step2ButtonText="Permanently Delete" />
@endcan
+ @can('update', $this->env)
+
+
+ @if (!$is_redis_credential)
+ @if ($type === 'service')
+
+
+
+
+ @else
+ @if ($is_shared)
+
+ @else
+ @if ($isSharedVariable)
+
+ @else
+ @if (!$env->is_nixpacks)
+
+ @endif
+
+ @if (!$env->is_nixpacks)
+
+ @if ($is_multiline === false)
+
+ @endif
+ @endif
+ @endif
+ @endif
+ @endif
+ @endif
+
+
+ @else
+
+
+ @if (!$is_redis_credential)
+ @if ($type === 'service')
+
+
+
+
+ @else
+ @if ($is_shared)
+
+ @else
+ @if ($isSharedVariable)
+
+ @else
+
+
+
+ @if ($is_multiline === false)
+
+ @endif
+ @endif
+ @endif
+ @endif
+ @endif
+
+
+ @endcan
@else
@can('update', $this->env)
@if ($isDisabled)
From 3f48dcb5750011c4ab6db724988e170c1b2bb314 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 15:54:44 +0200
Subject: [PATCH 08/24] feat(redaction): implement sensitive information
redaction in logs and commands
---
app/Models/ApplicationDeploymentQueue.php | 43 +++++++++++++++++++-
app/Traits/ExecuteRemoteCommand.php | 48 +++++++++++++++++++++--
2 files changed, 86 insertions(+), 5 deletions(-)
diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php
index 2a9bea67a..8df6877ab 100644
--- a/app/Models/ApplicationDeploymentQueue.php
+++ b/app/Models/ApplicationDeploymentQueue.php
@@ -85,6 +85,47 @@ class ApplicationDeploymentQueue extends Model
return str($this->commit_message)->value();
}
+ private function redactSensitiveInfo($text)
+ {
+ $text = remove_iip($text);
+
+ $app = $this->application;
+ if (! $app) {
+ return $text;
+ }
+
+ $lockedVars = collect([]);
+
+ if ($app->environment_variables) {
+ $lockedVars = $lockedVars->merge(
+ $app->environment_variables
+ ->where('is_shown_once', true)
+ ->pluck('real_value', 'key')
+ ->filter()
+ );
+ }
+
+ if ($this->pull_request_id !== 0 && $app->environment_variables_preview) {
+ $lockedVars = $lockedVars->merge(
+ $app->environment_variables_preview
+ ->where('is_shown_once', true)
+ ->pluck('real_value', 'key')
+ ->filter()
+ );
+ }
+
+ foreach ($lockedVars as $key => $value) {
+ $escapedValue = preg_quote($value, '/');
+ $text = preg_replace(
+ '/'.$escapedValue.'/',
+ REDACTED,
+ $text
+ );
+ }
+
+ return $text;
+ }
+
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
{
if ($type === 'error') {
@@ -96,7 +137,7 @@ class ApplicationDeploymentQueue extends Model
}
$newLogEntry = [
'command' => null,
- 'output' => remove_iip($message),
+ 'output' => $this->redactSensitiveInfo($message),
'type' => $type,
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php
index 0c3414efe..f9df19c16 100644
--- a/app/Traits/ExecuteRemoteCommand.php
+++ b/app/Traits/ExecuteRemoteCommand.php
@@ -17,6 +17,46 @@ trait ExecuteRemoteCommand
public static int $batch_counter = 0;
+ private function redact_sensitive_info($text)
+ {
+ $text = remove_iip($text);
+
+ if (! isset($this->application)) {
+ return $text;
+ }
+
+ $lockedVars = collect([]);
+
+ if (isset($this->application->environment_variables)) {
+ $lockedVars = $lockedVars->merge(
+ $this->application->environment_variables
+ ->where('is_shown_once', true)
+ ->pluck('real_value', 'key')
+ ->filter()
+ );
+ }
+
+ if (isset($this->pull_request_id) && $this->pull_request_id !== 0 && isset($this->application->environment_variables_preview)) {
+ $lockedVars = $lockedVars->merge(
+ $this->application->environment_variables_preview
+ ->where('is_shown_once', true)
+ ->pluck('real_value', 'key')
+ ->filter()
+ );
+ }
+
+ foreach ($lockedVars as $key => $value) {
+ $escapedValue = preg_quote($value, '/');
+ $text = preg_replace(
+ '/'.$escapedValue.'/',
+ REDACTED,
+ $text
+ );
+ }
+
+ return $text;
+ }
+
public function execute_remote_command(...$commands)
{
static::$batch_counter++;
@@ -74,7 +114,7 @@ trait ExecuteRemoteCommand
// Track SSH retry event in Sentry
$this->trackSshRetryEvent($attempt, $maxRetries, $delay, $errorMessage, [
'server' => $this->server->name ?? $this->server->ip ?? 'unknown',
- 'command' => remove_iip($command),
+ 'command' => $this->redact_sensitive_info($command),
'trait' => 'ExecuteRemoteCommand',
]);
@@ -125,8 +165,8 @@ trait ExecuteRemoteCommand
$sanitized_output = sanitize_utf8_text($output);
$new_log_entry = [
- 'command' => remove_iip($command),
- 'output' => remove_iip($sanitized_output),
+ 'command' => $this->redact_sensitive_info($command),
+ 'output' => $this->redact_sensitive_info($sanitized_output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
@@ -194,7 +234,7 @@ trait ExecuteRemoteCommand
$retryMessage = "SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}";
$new_log_entry = [
- 'output' => remove_iip($retryMessage),
+ 'output' => $this->redact_sensitive_info($retryMessage),
'type' => 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => false,
From 0ef0247e14ac0f5a808b9a21600070fe0dc3917f Mon Sep 17 00:00:00 2001
From: ShadowArcanist
Date: Fri, 19 Sep 2025 22:40:08 +0530
Subject: [PATCH 09/24] Improved metrics graph tooltip to show usage in a
better way and added timestamp to the tooltip
---
app/Livewire/Project/Shared/Metrics.php | 12 +-
resources/css/utilities.css | 17 ++
.../livewire/project/shared/metrics.blade.php | 277 ++++++++++--------
.../views/livewire/server/charts.blade.php | 54 +++-
4 files changed, 222 insertions(+), 138 deletions(-)
diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php
index fdc35fc0f..9dc944f9d 100644
--- a/app/Livewire/Project/Shared/Metrics.php
+++ b/app/Livewire/Project/Shared/Metrics.php
@@ -8,7 +8,7 @@ class Metrics extends Component
{
public $resource;
- public $chartId = 'container-cpu';
+ public $chartId = 'metrics';
public $data;
@@ -33,6 +33,16 @@ class Metrics extends Component
try {
$cpuMetrics = $this->resource->getCpuMetrics($this->interval);
$memoryMetrics = $this->resource->getMemoryMetrics($this->interval);
+
+ // Debug logging
+ \Log::info('Metrics loadData called', [
+ 'chartId' => $this->chartId,
+ 'cpuMetrics' => $cpuMetrics,
+ 'memoryMetrics' => $memoryMetrics,
+ 'cpuEvent' => "refreshChartData-{$this->chartId}-cpu",
+ 'memoryEvent' => "refreshChartData-{$this->chartId}-memory"
+ ]);
+
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [
'seriesData' => $cpuMetrics,
]);
diff --git a/resources/css/utilities.css b/resources/css/utilities.css
index d09d7f49c..65869e02f 100644
--- a/resources/css/utilities.css
+++ b/resources/css/utilities.css
@@ -10,6 +10,23 @@
@apply hidden!;
}
+@utility apexcharts-tooltip-custom {
+ @apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-lg p-3 text-sm;
+ min-width: 160px;
+}
+
+@utility apexcharts-tooltip-custom-value {
+ @apply text-neutral-700 dark:text-neutral-300 mb-1;
+}
+
+@utility apexcharts-tooltip-value-bold {
+ @apply font-bold text-black dark:text-white;
+}
+
+@utility apexcharts-tooltip-custom-title {
+ @apply text-xs text-neutral-500 dark:text-neutral-400 font-medium;
+}
+
@utility input-sticky {
@apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300;
}
diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php
index d6609d9e6..9b08babb3 100644
--- a/resources/views/livewire/project/shared/metrics.blade.php
+++ b/resources/views/livewire/project/shared/metrics.blade.php
@@ -1,21 +1,20 @@
-
+
Metrics
Basic metrics for your container.
- @if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose')
-
Metrics are not available for Docker Compose applications yet!
- @elseif(!$resource->destination->server->isMetricsEnabled())
-
Metrics are only available for servers with Sentinel & Metrics enabled!
-
- @else
- @if (!str($resource->status)->contains('running'))
-
Metrics are only available when this resource is running!
+
+ @if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose')
+
Metrics are not available for Docker Compose applications yet!
+ @elseif(!$resource->destination->server->isMetricsEnabled())
+
Metrics are only available for servers with Sentinel & Metrics enabled!
+
@else
-
+ @if (!str($resource->status)->contains('running'))
+ Metrics are only available when this resource is running!
+ @else
+
+
@@ -77,63 +76,76 @@
xaxis: {
type: 'datetime',
},
- series: [{
- name: "CPU %",
- data: []
- }],
- noData: {
- text: 'Loading...',
- style: {
- color: textColor,
- }
- },
- tooltip: {
- enabled: true,
- marker: {
- show: false,
- }
- },
- legend: {
- show: false
- }
+ series: [{
+ name: "CPU %",
+ data: []
+ }],
+ noData: {
+ text: 'Loading...',
+ style: {
+ color: textColor,
+ }
+ },
+ tooltip: {
+ enabled: true,
+ marker: {
+ show: false,
+ },
+ custom: function({ series, seriesIndex, dataPointIndex, w }) {
+ const value = series[seriesIndex][dataPointIndex];
+ const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
+ const date = new Date(timestamp);
+ const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
+ String(date.getUTCMinutes()).padStart(2, '0') + ':' +
+ String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
+ date.getUTCFullYear() + '-' +
+ String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
+ String(date.getUTCDate()).padStart(2, '0');
+ return '';
+ }
+ },
+ legend: {
+ show: false
+ }
}
- const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu);
- serverCpuChart.render();
- document.addEventListener('livewire:init', () => {
- Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
- checkTheme();
- serverCpuChart.updateOptions({
- series: [{
- data: chartData[0].seriesData,
- }],
- colors: [cpuColor],
- xaxis: {
- type: 'datetime',
- labels: {
- show: true,
- style: {
- colors: textColor,
- }
- }
- },
- yaxis: {
- show: true,
- labels: {
- show: true,
- style: {
- colors: textColor,
- }
- }
- },
- noData: {
- text: 'Loading...',
- style: {
- color: textColor,
- }
- }
- });
- });
- });
+ const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu);
+ serverCpuChart.render();
+ Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
+ checkTheme();
+ serverCpuChart.updateOptions({
+ series: [{
+ data: chartData[0].seriesData,
+ }],
+ colors: [cpuColor],
+ xaxis: {
+ type: 'datetime',
+ labels: {
+ show: true,
+ style: {
+ colors: textColor,
+ }
+ }
+ },
+ yaxis: {
+ show: true,
+ labels: {
+ show: true,
+ style: {
+ colors: textColor,
+ }
+ }
+ },
+ noData: {
+ text: 'Loading...',
+ style: {
+ color: textColor,
+ }
+ }
+ });
+ });
Memory (MB)
@@ -195,65 +207,80 @@
},
series: [{
name: "Memory (MB)",
- data: []
- }],
- noData: {
- text: 'Loading...',
- style: {
- color: textColor,
- }
- },
- tooltip: {
- enabled: true,
- marker: {
- show: false,
- }
- },
- legend: {
- show: false
- }
+ data: []
+ }],
+ noData: {
+ text: 'Loading...',
+ style: {
+ color: textColor,
+ }
+ },
+ tooltip: {
+ enabled: true,
+ marker: {
+ show: false,
+ },
+ custom: function({ series, seriesIndex, dataPointIndex, w }) {
+ const value = series[seriesIndex][dataPointIndex];
+ const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
+ const date = new Date(timestamp);
+ const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
+ String(date.getUTCMinutes()).padStart(2, '0') + ':' +
+ String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
+ date.getUTCFullYear() + '-' +
+ String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
+ String(date.getUTCDate()).padStart(2, '0');
+ return '';
+ }
+ },
+ legend: {
+ show: false
+ }
}
- const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
- optionsServerMemory);
- serverMemoryChart.render();
- document.addEventListener('livewire:init', () => {
- Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
- checkTheme();
- serverMemoryChart.updateOptions({
- series: [{
- data: chartData[0].seriesData,
- }],
- colors: [ramColor],
- xaxis: {
- type: 'datetime',
- labels: {
- show: true,
- style: {
- colors: textColor,
- }
- }
- },
- yaxis: {
- min: 0,
- show: true,
- labels: {
- show: true,
- style: {
- colors: textColor,
- }
- }
- },
- noData: {
- text: 'Loading...',
- style: {
- color: textColor,
- }
- }
- });
- });
- });
+ const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
+ optionsServerMemory);
+ serverMemoryChart.render();
+ Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
+ checkTheme();
+ serverMemoryChart.updateOptions({
+ series: [{
+ data: chartData[0].seriesData,
+ }],
+ colors: [ramColor],
+ xaxis: {
+ type: 'datetime',
+ labels: {
+ show: true,
+ style: {
+ colors: textColor,
+ }
+ }
+ },
+ yaxis: {
+ min: 0,
+ show: true,
+ labels: {
+ show: true,
+ style: {
+ colors: textColor,
+ }
+ }
+ },
+ noData: {
+ text: 'Loading...',
+ style: {
+ color: textColor,
+ }
+ }
+ });
+ });
+
@endif
@endif
+
diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php
index f5a2418fd..2cb8e2c37 100644
--- a/resources/views/livewire/server/charts.blade.php
+++ b/resources/views/livewire/server/charts.blade.php
@@ -80,12 +80,27 @@
color: textColor,
}
},
- tooltip: {
- enabled: true,
- marker: {
- show: false,
- }
- },
+ tooltip: {
+ enabled: true,
+ marker: {
+ show: false,
+ },
+ custom: function({ series, seriesIndex, dataPointIndex, w }) {
+ const value = series[seriesIndex][dataPointIndex];
+ const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
+ const date = new Date(timestamp);
+ const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
+ String(date.getUTCMinutes()).padStart(2, '0') + ':' +
+ String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
+ date.getUTCFullYear() + '-' +
+ String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
+ String(date.getUTCDate()).padStart(2, '0');
+ return '';
+ }
+ },
legend: {
show: false
}
@@ -198,12 +213,27 @@
color: textColor,
}
},
- tooltip: {
- enabled: true,
- marker: {
- show: false,
- }
- },
+ tooltip: {
+ enabled: true,
+ marker: {
+ show: false,
+ },
+ custom: function({ series, seriesIndex, dataPointIndex, w }) {
+ const value = series[seriesIndex][dataPointIndex];
+ const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
+ const date = new Date(timestamp);
+ const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
+ String(date.getUTCMinutes()).padStart(2, '0') + ':' +
+ String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
+ date.getUTCFullYear() + '-' +
+ String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
+ String(date.getUTCDate()).padStart(2, '0');
+ return '';
+ }
+ },
legend: {
show: false
}
From 610ef310341d7bc7384349f80f787b0b9ce3c41e Mon Sep 17 00:00:00 2001
From: ShadowArcanist
Date: Fri, 19 Sep 2025 22:51:24 +0530
Subject: [PATCH 10/24] Hidden metrics charts grid borders on darkmode (it was
too bright on darkmode)
---
resources/css/utilities.css | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/resources/css/utilities.css b/resources/css/utilities.css
index 65869e02f..694ad61a3 100644
--- a/resources/css/utilities.css
+++ b/resources/css/utilities.css
@@ -6,6 +6,10 @@
@apply hidden!;
}
+@utility apexcharts-grid-borders {
+ @apply dark:hidden!;
+}
+
@utility apexcharts-xaxistooltip {
@apply hidden!;
}
From a0f4566580eb982705f5ceba3efff8faddddce16 Mon Sep 17 00:00:00 2001
From: ShadowArcanist
Date: Fri, 19 Sep 2025 22:55:25 +0530
Subject: [PATCH 11/24] Fixed Memory title on app metrics being larger than CPU
title
---
resources/views/livewire/project/shared/metrics.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php
index 9b08babb3..84e4595aa 100644
--- a/resources/views/livewire/project/shared/metrics.blade.php
+++ b/resources/views/livewire/project/shared/metrics.blade.php
@@ -148,7 +148,7 @@
});
- Memory (MB)
+ Memory (MB)
- Memory (MB)
+ Memory Usage
-
Memory (%)
+
Memory Usage