mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-12 16:15:17 +00:00
Merge branch 'next' into feat/copy-resource-logs-with-sanitization
This commit is contained in:
85 files changed
+8842
-1086
No files matched your search
+5887
-556
File diff suppressed because it is too large.
Load diff
@@ -30,7 +30,6 @@ class UpdateCoolify
|
||||
if (! $this->server) {
|
||||
return;
|
||||
}
|
||||
CleanupDocker::dispatch($this->server, false, false);
|
||||
|
||||
// Fetch fresh version from CDN instead of using cache
|
||||
try {
|
||||
@@ -117,17 +116,12 @@ class UpdateCoolify
|
||||
|
||||
private function update()
|
||||
{
|
||||
$helperImage = config('constants.coolify.helper_image');
|
||||
$latest_version = getHelperVersion();
|
||||
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
|
||||
|
||||
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
|
||||
instant_remote_process(["docker pull -q $image"], $this->server, false);
|
||||
|
||||
$latestHelperImageVersion = getHelperVersion();
|
||||
$upgradeScriptUrl = config('constants.coolify.upgrade_script_url');
|
||||
|
||||
remote_process([
|
||||
"curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion $latestHelperImageVersion",
|
||||
], $this->server);
|
||||
}
|
||||
}
|
||||
@@ -309,7 +309,9 @@ class Github extends Controller
|
||||
if (! $id || ! $branch) {
|
||||
return response('Nothing to do. No id or branch found.');
|
||||
}
|
||||
$applications = Application::where('repository_project_id', $id)->whereRelation('source', 'is_public', false);
|
||||
$applications = Application::where('repository_project_id', $id)
|
||||
->where('source_id', $github_app->id)
|
||||
->whereRelation('source', 'is_public', false);
|
||||
if ($x_github_event === 'push') {
|
||||
$applications = $applications->where('git_branch', $branch)->get();
|
||||
if ($applications->isEmpty()) {
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class NavbarDeleteTeam extends Component
|
||||
@@ -19,12 +17,8 @@ class NavbarDeleteTeam extends Component
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentTeam = currentTeam();
|
||||
|
||||
@@ -20,6 +20,8 @@ class Show extends Component
|
||||
|
||||
public bool $is_debug_enabled = false;
|
||||
|
||||
public bool $fullscreen = false;
|
||||
|
||||
private bool $deploymentFinishedDispatched = false;
|
||||
|
||||
public function getListeners()
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Exception;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
@@ -154,12 +151,8 @@ class BackupEdit extends Component
|
||||
{
|
||||
$this->authorize('manageBackups', $this->backup->database);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class BackupExecutions extends Component
|
||||
@@ -69,12 +67,8 @@ class BackupExecutions extends Component
|
||||
|
||||
public function deleteBackup($executionId, $password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$execution = $this->backup->executions()->where('id', $executionId)->first();
|
||||
|
||||
@@ -128,6 +128,7 @@ class GithubPrivateRepository extends Component
|
||||
$this->loadBranchByPage();
|
||||
}
|
||||
}
|
||||
$this->branches = sortBranchesByPriority($this->branches);
|
||||
$this->selected_branch_name = data_get($this->branches, '0.name', 'main');
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,9 @@ namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Database extends Component
|
||||
@@ -96,12 +93,8 @@ class Database extends Component
|
||||
try {
|
||||
$this->authorize('delete', $this->database);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->database->delete();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
@@ -16,8 +15,6 @@ use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -62,7 +59,7 @@ class FileStorage extends Component
|
||||
$this->fs_path = $this->fileStorage->fs_path;
|
||||
}
|
||||
|
||||
$this->isReadOnly = $this->fileStorage->isReadOnlyVolume();
|
||||
$this->isReadOnly = $this->fileStorage->shouldBeReadOnlyInUI();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
@@ -104,7 +101,8 @@ class FileStorage extends Component
|
||||
public function loadStorageOnServer()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
// Loading content is a read operation, so we use 'view' permission
|
||||
$this->authorize('view', $this->resource);
|
||||
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
$this->syncData();
|
||||
@@ -140,12 +138,8 @@ class FileStorage extends Component
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
@@ -128,12 +125,8 @@ class ServiceApplicationView extends Component
|
||||
try {
|
||||
$this->authorize('delete', $this->application);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->application->delete();
|
||||
|
||||
@@ -67,7 +67,7 @@ class Storage extends Component
|
||||
public function refreshStorages()
|
||||
{
|
||||
$this->fileStorage = $this->resource->fileStorages()->get();
|
||||
$this->resource->refresh();
|
||||
$this->resource->load('persistentStorages.resource');
|
||||
}
|
||||
|
||||
public function getFilesProperty()
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -93,12 +90,8 @@ class Danger extends Component
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->resource) {
|
||||
|
||||
@@ -5,12 +5,9 @@ namespace App\Livewire\Project\Shared;
|
||||
use App\Actions\Application\StopApplicationOneServer;
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Events\ApplicationStatusChanged;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -140,12 +137,8 @@ class Destination extends Component
|
||||
public function removeServer(int $network_id, int $server_id, $password)
|
||||
{
|
||||
try {
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared\Storages;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
@@ -67,7 +64,7 @@ class Show extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->syncData(false);
|
||||
$this->isReadOnly = $this->storage->isReadOnlyVolume();
|
||||
$this->isReadOnly = $this->storage->shouldBeReadOnlyInUI();
|
||||
}
|
||||
|
||||
public function submit()
|
||||
@@ -84,12 +81,8 @@ class Show extends Component
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->storage->delete();
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Delete extends Component
|
||||
@@ -29,12 +26,8 @@ class Delete extends Component
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->authorize('delete', $this->server);
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
namespace App\Livewire\Server\Security;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -44,13 +41,9 @@ class TerminalAccess extends Component
|
||||
throw new \Exception('Only team administrators and owners can modify terminal access.');
|
||||
}
|
||||
|
||||
// Verify password unless two-step confirmation is disabled
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
// Verify password
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle the terminal setting
|
||||
|
||||
@@ -5,9 +5,12 @@ namespace App\Livewire\Server;
|
||||
use App\Actions\Server\StartSentinel;
|
||||
use App\Actions\Server\StopSentinel;
|
||||
use App\Events\ServerReachabilityChanged;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\Server;
|
||||
use App\Services\HetznerService;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
@@ -73,6 +76,17 @@ class Show extends Component
|
||||
|
||||
public bool $isValidating = false;
|
||||
|
||||
// Hetzner linking properties
|
||||
public Collection $availableHetznerTokens;
|
||||
|
||||
public ?int $selectedHetznerTokenId = null;
|
||||
|
||||
public ?array $matchedHetznerServer = null;
|
||||
|
||||
public ?string $hetznerSearchError = null;
|
||||
|
||||
public bool $hetznerNoMatchFound = false;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = $this->server->team_id ?? auth()->user()->currentTeam()->id;
|
||||
@@ -150,6 +164,9 @@ class Show extends Component
|
||||
$this->hetznerServerStatus = $this->server->hetzner_server_status;
|
||||
$this->isValidating = $this->server->is_validating ?? false;
|
||||
|
||||
// Load Hetzner tokens for linking
|
||||
$this->loadHetznerTokens();
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -465,6 +482,98 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function loadHetznerTokens(): void
|
||||
{
|
||||
$this->availableHetznerTokens = CloudProviderToken::ownedByCurrentTeam()
|
||||
->where('provider', 'hetzner')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function searchHetznerServer(): void
|
||||
{
|
||||
$this->hetznerSearchError = null;
|
||||
$this->hetznerNoMatchFound = false;
|
||||
$this->matchedHetznerServer = null;
|
||||
|
||||
if (! $this->selectedHetznerTokenId) {
|
||||
$this->hetznerSearchError = 'Please select a Hetzner token.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
$token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId);
|
||||
if (! $token) {
|
||||
$this->hetznerSearchError = 'Invalid token selected.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
$matched = $hetznerService->findServerByIp($this->server->ip);
|
||||
|
||||
if ($matched) {
|
||||
$this->matchedHetznerServer = $matched;
|
||||
} else {
|
||||
$this->hetznerNoMatchFound = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->hetznerSearchError = 'Failed to search Hetzner servers: '.$e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function linkToHetzner()
|
||||
{
|
||||
if (! $this->matchedHetznerServer) {
|
||||
$this->dispatch('error', 'No Hetzner server selected.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
$token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId);
|
||||
if (! $token) {
|
||||
$this->dispatch('error', 'Invalid token selected.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the server exists and is accessible with the token
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
$serverData = $hetznerService->getServer($this->matchedHetznerServer['id']);
|
||||
|
||||
if (empty($serverData)) {
|
||||
$this->dispatch('error', 'Could not find Hetzner server with ID: '.$this->matchedHetznerServer['id']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the server with Hetzner details
|
||||
$this->server->update([
|
||||
'cloud_provider_token_id' => $this->selectedHetznerTokenId,
|
||||
'hetzner_server_id' => $this->matchedHetznerServer['id'],
|
||||
'hetzner_server_status' => $serverData['status'] ?? null,
|
||||
]);
|
||||
|
||||
$this->hetznerServerStatus = $serverData['status'] ?? null;
|
||||
|
||||
// Clear the linking state
|
||||
$this->matchedHetznerServer = null;
|
||||
$this->selectedHetznerTokenId = null;
|
||||
$this->hetznerNoMatchFound = false;
|
||||
$this->hetznerSearchError = null;
|
||||
|
||||
$this->dispatch('success', 'Server successfully linked to Hetzner Cloud!');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.show');
|
||||
|
||||
@@ -5,8 +5,6 @@ namespace App\Livewire\Settings;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Rules\ValidIpOrCidr;
|
||||
use Auth;
|
||||
use Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -157,9 +155,7 @@ class Advanced extends Component
|
||||
|
||||
public function toggleTwoStepConfirmation($password): bool
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Team;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class AdminView extends Component
|
||||
@@ -58,12 +55,8 @@ class AdminView extends Component
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! auth()->user()->isInstanceAdmin()) {
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire;
|
||||
|
||||
use App\Actions\Server\UpdateCoolify;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Upgrade extends Component
|
||||
@@ -14,12 +15,20 @@ class Upgrade extends Component
|
||||
|
||||
public string $latestVersion = '';
|
||||
|
||||
public string $currentVersion = '';
|
||||
|
||||
protected $listeners = ['updateAvailable' => 'checkUpdate'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
}
|
||||
|
||||
public function checkUpdate()
|
||||
{
|
||||
try {
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
$this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false);
|
||||
if (isDev()) {
|
||||
$this->isUpgradeAvailable = true;
|
||||
@@ -41,4 +50,71 @@ class Upgrade extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getUpgradeStatus(): array
|
||||
{
|
||||
// Only root team members can view upgrade status
|
||||
if (auth()->user()?->currentTeam()?->id !== 0) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
$server = Server::find(0);
|
||||
if (! $server) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
$statusFile = '/data/coolify/source/.upgrade-status';
|
||||
|
||||
try {
|
||||
$content = instant_remote_process(
|
||||
["cat {$statusFile} 2>/dev/null || echo ''"],
|
||||
$server,
|
||||
false
|
||||
);
|
||||
$content = trim($content ?? '');
|
||||
} catch (\Throwable $e) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
if (empty($content)) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
$parts = explode('|', $content);
|
||||
if (count($parts) < 3) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
[$step, $message, $timestamp] = $parts;
|
||||
|
||||
// Check if status is stale (older than 10 minutes)
|
||||
try {
|
||||
$statusTime = new \DateTime($timestamp);
|
||||
$now = new \DateTime;
|
||||
$diffMinutes = ($now->getTimestamp() - $statusTime->getTimestamp()) / 60;
|
||||
|
||||
if ($diffMinutes > 10) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
if ($step === 'error') {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'step' => 0,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
$stepInt = (int) $step;
|
||||
$status = $stepInt >= 6 ? 'complete' : 'in_progress';
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'step' => $stepInt,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -209,6 +209,23 @@ class LocalFileVolume extends BaseModel
|
||||
return $query->get()->where('plain_mount_path', $path);
|
||||
}
|
||||
|
||||
// Check if this volume belongs to a service resource
|
||||
public function isServiceResource(): bool
|
||||
{
|
||||
return in_array($this->resource_type, [
|
||||
'App\Models\ServiceApplication',
|
||||
'App\Models\ServiceDatabase',
|
||||
]);
|
||||
}
|
||||
|
||||
// Determine if this volume should be read-only in the UI
|
||||
// File/directory mounts can be edited even for services
|
||||
public function shouldBeReadOnlyInUI(): bool
|
||||
{
|
||||
// Check for explicit :ro flag in compose (existing logic)
|
||||
return $this->isReadOnlyVolume();
|
||||
}
|
||||
|
||||
// Check if this volume is read-only by parsing the docker-compose content
|
||||
public function isReadOnlyVolume(): bool
|
||||
{
|
||||
@@ -239,22 +256,40 @@ class LocalFileVolume extends BaseModel
|
||||
$volumes = $compose['services'][$serviceName]['volumes'];
|
||||
|
||||
// Check each volume to find a match
|
||||
// Note: We match on mount_path (container path) only, since fs_path gets transformed
|
||||
// from relative (./file) to absolute (/data/coolify/services/uuid/file) during parsing
|
||||
foreach ($volumes as $volume) {
|
||||
// Volume can be string like "host:container:ro" or "host:container"
|
||||
if (is_string($volume)) {
|
||||
$parts = explode(':', $volume);
|
||||
|
||||
// Check if this volume matches our fs_path and mount_path
|
||||
// Check if this volume matches our mount_path
|
||||
if (count($parts) >= 2) {
|
||||
$hostPath = $parts[0];
|
||||
$containerPath = $parts[1];
|
||||
$options = $parts[2] ?? null;
|
||||
|
||||
// Match based on fs_path and mount_path
|
||||
if ($hostPath === $this->fs_path && $containerPath === $this->mount_path) {
|
||||
// Match based on mount_path
|
||||
// Remove leading slash from mount_path if present for comparison
|
||||
$mountPath = str($this->mount_path)->ltrim('/')->toString();
|
||||
$containerPathClean = str($containerPath)->ltrim('/')->toString();
|
||||
|
||||
if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) {
|
||||
return $options === 'ro';
|
||||
}
|
||||
}
|
||||
} elseif (is_array($volume)) {
|
||||
// Long-form syntax: { type: bind, source: ..., target: ..., read_only: true }
|
||||
$containerPath = data_get($volume, 'target');
|
||||
$readOnly = data_get($volume, 'read_only', false);
|
||||
|
||||
// Match based on mount_path
|
||||
// Remove leading slash from mount_path if present for comparison
|
||||
$mountPath = str($this->mount_path)->ltrim('/')->toString();
|
||||
$containerPathClean = str($containerPath)->ltrim('/')->toString();
|
||||
|
||||
if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) {
|
||||
return $readOnly === true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ class LocalPersistentVolume extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function resource()
|
||||
{
|
||||
return $this->morphTo('resource');
|
||||
}
|
||||
|
||||
public function application()
|
||||
{
|
||||
return $this->morphTo('resource');
|
||||
@@ -50,6 +55,54 @@ class LocalPersistentVolume extends Model
|
||||
);
|
||||
}
|
||||
|
||||
// Check if this volume belongs to a service resource
|
||||
public function isServiceResource(): bool
|
||||
{
|
||||
return in_array($this->resource_type, [
|
||||
'App\Models\ServiceApplication',
|
||||
'App\Models\ServiceDatabase',
|
||||
]);
|
||||
}
|
||||
|
||||
// Check if this volume belongs to a dockercompose application
|
||||
public function isDockerComposeResource(): bool
|
||||
{
|
||||
if ($this->resource_type !== 'App\Models\Application') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only access relationship if already eager loaded to avoid N+1
|
||||
if (! $this->relationLoaded('resource')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$application = $this->resource;
|
||||
if (! $application) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return data_get($application, 'build_pack') === 'dockercompose';
|
||||
}
|
||||
|
||||
// Determine if this volume should be read-only in the UI
|
||||
// Service volumes and dockercompose application volumes are read-only
|
||||
// (users should edit compose file directly)
|
||||
public function shouldBeReadOnlyInUI(): bool
|
||||
{
|
||||
// All service volumes should be read-only in UI
|
||||
if ($this->isServiceResource()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// All dockercompose application volumes should be read-only in UI
|
||||
if ($this->isDockerComposeResource()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for explicit :ro flag in compose (existing logic)
|
||||
return $this->isReadOnlyVolume();
|
||||
}
|
||||
|
||||
// Check if this volume is read-only by parsing the docker-compose content
|
||||
public function isReadOnlyVolume(): bool
|
||||
{
|
||||
@@ -85,6 +138,7 @@ class LocalPersistentVolume extends Model
|
||||
$volumes = $compose['services'][$serviceName]['volumes'];
|
||||
|
||||
// Check each volume to find a match
|
||||
// Note: We match on mount_path (container path) only, since host paths get transformed
|
||||
foreach ($volumes as $volume) {
|
||||
// Volume can be string like "host:container:ro" or "host:container"
|
||||
if (is_string($volume)) {
|
||||
@@ -104,6 +158,19 @@ class LocalPersistentVolume extends Model
|
||||
return $options === 'ro';
|
||||
}
|
||||
}
|
||||
} elseif (is_array($volume)) {
|
||||
// Long-form syntax: { type: bind/volume, source: ..., target: ..., read_only: true }
|
||||
$containerPath = data_get($volume, 'target');
|
||||
$readOnly = data_get($volume, 'read_only', false);
|
||||
|
||||
// Match based on mount_path
|
||||
// Remove leading slash from mount_path if present for comparison
|
||||
$mountPath = str($this->mount_path)->ltrim('/')->toString();
|
||||
$containerPathClean = str($containerPath)->ltrim('/')->toString();
|
||||
|
||||
if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) {
|
||||
return $readOnly === true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,28 @@ class S3Storage extends BaseModel
|
||||
'secret' => 'encrypted',
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot the model and register event listeners.
|
||||
*/
|
||||
protected static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Trim whitespace from credentials before saving to prevent
|
||||
// "Malformed Access Key Id" errors from accidental whitespace in pasted values.
|
||||
// Note: We use the saving event instead of Attribute mutators because key/secret
|
||||
// use Laravel's 'encrypted' cast. Attribute mutators fire before casts, which
|
||||
// would cause issues with the encryption/decryption cycle.
|
||||
static::saving(function (S3Storage $storage) {
|
||||
if ($storage->key !== null) {
|
||||
$storage->key = trim($storage->key);
|
||||
}
|
||||
if ($storage->secret !== null) {
|
||||
$storage->secret = trim($storage->secret);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam(array $select = ['*'])
|
||||
{
|
||||
$selectArray = collect($select)->concat(['id']);
|
||||
@@ -55,6 +77,36 @@ class S3Storage extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim whitespace from endpoint to prevent malformed URLs.
|
||||
*/
|
||||
protected function endpoint(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (?string $value) => $value ? trim($value) : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim whitespace from bucket name to prevent connection errors.
|
||||
*/
|
||||
protected function bucket(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (?string $value) => $value ? trim($value) : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim whitespace from region to prevent connection errors.
|
||||
*/
|
||||
protected function region(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (?string $value) => $value ? trim($value) : null,
|
||||
);
|
||||
}
|
||||
|
||||
public function testConnection(bool $shouldSave = false)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -443,4 +443,13 @@ class User extends Authenticatable implements SendsEmail
|
||||
&& $this->email_change_code_expires_at
|
||||
&& Carbon::now()->lessThan($this->email_change_code_expires_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has a password set.
|
||||
* OAuth users are created without passwords.
|
||||
*/
|
||||
public function hasPassword(): bool
|
||||
{
|
||||
return ! empty($this->password);
|
||||
}
|
||||
}
|
||||
@@ -43,21 +43,26 @@ class EmailChannel
|
||||
throw new Exception('No email recipients found');
|
||||
}
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
// Check if the recipient is part of the team
|
||||
if (! $members->contains('email', $recipient)) {
|
||||
$emailSettings = $notifiable->emailNotificationSettings;
|
||||
data_set($emailSettings, 'smtp_password', '********');
|
||||
data_set($emailSettings, 'resend_api_key', '********');
|
||||
send_internal_notification(sprintf(
|
||||
"Recipient is not part of the team: %s\nTeam: %s\nNotification: %s\nNotifiable: %s\nEmail Settings:\n%s",
|
||||
$recipient,
|
||||
$team,
|
||||
get_class($notification),
|
||||
get_class($notifiable),
|
||||
json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
|
||||
));
|
||||
throw new Exception('Recipient is not part of the team');
|
||||
// Skip team membership validation for test notifications
|
||||
$isTestNotification = data_get($notification, 'isTestNotification', false);
|
||||
|
||||
if (! $isTestNotification) {
|
||||
foreach ($recipients as $recipient) {
|
||||
// Check if the recipient is part of the team
|
||||
if (! $members->contains('email', $recipient)) {
|
||||
$emailSettings = $notifiable->emailNotificationSettings;
|
||||
data_set($emailSettings, 'smtp_password', '********');
|
||||
data_set($emailSettings, 'resend_api_key', '********');
|
||||
send_internal_notification(sprintf(
|
||||
"Recipient is not part of the team: %s\nTeam: %s\nNotification: %s\nNotifiable: %s\nEmail Settings:\n%s",
|
||||
$recipient,
|
||||
$team,
|
||||
get_class($notification),
|
||||
get_class($notifiable),
|
||||
json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
|
||||
));
|
||||
throw new Exception('Recipient is not part of the team');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ class Test extends Notification implements ShouldQueue
|
||||
|
||||
public $tries = 5;
|
||||
|
||||
public bool $isTestNotification = true;
|
||||
|
||||
public function __construct(public ?string $emails = null, public ?string $channel = null, public ?bool $ping = false)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
|
||||
@@ -8,6 +8,8 @@ use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class Test extends CustomEmailNotification
|
||||
{
|
||||
public bool $isTestNotification = true;
|
||||
|
||||
public function __construct(public string $emails, public bool $isTransactionalEmail = true)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
|
||||
@@ -161,4 +161,30 @@ class HetznerService
|
||||
{
|
||||
$this->request('delete', "/servers/{$serverId}");
|
||||
}
|
||||
|
||||
public function getServers(): array
|
||||
{
|
||||
return $this->requestPaginated('get', '/servers', 'servers');
|
||||
}
|
||||
|
||||
public function findServerByIp(string $ip): ?array
|
||||
{
|
||||
$servers = $this->getServers();
|
||||
|
||||
foreach ($servers as $server) {
|
||||
// Check IPv4
|
||||
$ipv4 = data_get($server, 'public_net.ipv4.ip');
|
||||
if ($ipv4 === $ip) {
|
||||
return $server;
|
||||
}
|
||||
|
||||
// Check IPv6 (Hetzner returns the full /64 block)
|
||||
$ipv6 = data_get($server, 'public_net.ipv6.ip');
|
||||
if ($ipv6 && str_starts_with($ip, rtrim($ipv6, '/'))) {
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ const SUPPORTED_OS = [
|
||||
const NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK = [
|
||||
'pgadmin',
|
||||
'postgresus',
|
||||
'redis-insight',
|
||||
];
|
||||
const NEEDS_TO_DISABLE_GZIP = [
|
||||
'beszel' => ['beszel'],
|
||||
|
||||
@@ -33,6 +33,7 @@ use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
@@ -300,6 +301,24 @@ function generate_application_name(string $git_repository, string $git_branch, ?
|
||||
return Str::kebab("$git_repository:$git_branch-$cuid");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort branches by priority: main first, master second, then alphabetically.
|
||||
*
|
||||
* @param Collection $branches Collection of branch objects with 'name' key
|
||||
*/
|
||||
function sortBranchesByPriority(Collection $branches): Collection
|
||||
{
|
||||
return $branches->sortBy(function ($branch) {
|
||||
$name = data_get($branch, 'name');
|
||||
|
||||
return match ($name) {
|
||||
'main' => '0_main',
|
||||
'master' => '1_master',
|
||||
default => '2_'.$name,
|
||||
};
|
||||
})->values();
|
||||
}
|
||||
|
||||
function base_ip(): string
|
||||
{
|
||||
if (isDev()) {
|
||||
@@ -3332,3 +3351,57 @@ function formatContainerStatus(string $status): string
|
||||
return str($status)->headline()->value();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if password confirmation should be skipped.
|
||||
* Returns true if:
|
||||
* - Two-step confirmation is globally disabled
|
||||
* - User has no password (OAuth users)
|
||||
*
|
||||
* Used by modal-confirmation.blade.php to determine if password step should be shown.
|
||||
*
|
||||
* @return bool True if password confirmation should be skipped
|
||||
*/
|
||||
function shouldSkipPasswordConfirmation(): bool
|
||||
{
|
||||
// Skip if two-step confirmation is globally disabled
|
||||
if (data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip if user has no password (OAuth users)
|
||||
if (! Auth::user()?->hasPassword()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify password for two-step confirmation.
|
||||
* Skips verification if:
|
||||
* - Two-step confirmation is globally disabled
|
||||
* - User has no password (OAuth users)
|
||||
*
|
||||
* @param mixed $password The password to verify (may be array if skipped by frontend)
|
||||
* @param \Livewire\Component|null $component Optional Livewire component to add errors to
|
||||
* @return bool True if verification passed (or skipped), false if password is incorrect
|
||||
*/
|
||||
function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $component = null): bool
|
||||
{
|
||||
// Skip if password confirmation should be skipped
|
||||
if (shouldSkipPasswordConfirmation()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verify the password
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
if ($component) {
|
||||
$component->addError('password', 'The provided password is incorrect.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.454',
|
||||
'version' => '4.0.0-beta.455',
|
||||
'helper_version' => '1.0.12',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
||||
@@ -11,16 +11,18 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cloud_provider_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('team_id')->constrained()->onDelete('cascade');
|
||||
$table->string('provider');
|
||||
$table->text('token');
|
||||
$table->string('name')->nullable();
|
||||
$table->timestamps();
|
||||
if (! Schema::hasTable('cloud_provider_tokens')) {
|
||||
Schema::create('cloud_provider_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('team_id')->constrained()->onDelete('cascade');
|
||||
$table->string('provider');
|
||||
$table->text('token');
|
||||
$table->string('name')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['team_id', 'provider']);
|
||||
});
|
||||
$table->index(['team_id', 'provider']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->bigInteger('hetzner_server_id')->nullable()->after('id');
|
||||
});
|
||||
if (! Schema::hasColumn('servers', 'hetzner_server_id')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->bigInteger('hetzner_server_id')->nullable()->after('id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +23,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('hetzner_server_id');
|
||||
});
|
||||
if (Schema::hasColumn('servers', 'hetzner_server_id')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('hetzner_server_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+11
-7
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->foreignId('cloud_provider_token_id')->nullable()->after('private_key_id')->constrained()->onDelete('set null');
|
||||
});
|
||||
if (! Schema::hasColumn('servers', 'cloud_provider_token_id')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->foreignId('cloud_provider_token_id')->nullable()->after('private_key_id')->constrained()->onDelete('set null');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,9 +23,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropForeign(['cloud_provider_token_id']);
|
||||
$table->dropColumn('cloud_provider_token_id');
|
||||
});
|
||||
if (Schema::hasColumn('servers', 'cloud_provider_token_id')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropForeign(['cloud_provider_token_id']);
|
||||
$table->dropColumn('cloud_provider_token_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+10
-6
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->string('hetzner_server_status')->nullable()->after('hetzner_server_id');
|
||||
});
|
||||
if (! Schema::hasColumn('servers', 'hetzner_server_status')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->string('hetzner_server_status')->nullable()->after('hetzner_server_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +23,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('hetzner_server_status');
|
||||
});
|
||||
if (Schema::hasColumn('servers', 'hetzner_server_status')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('hetzner_server_status');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->boolean('is_validating')->default(false)->after('hetzner_server_status');
|
||||
});
|
||||
if (! Schema::hasColumn('servers', 'is_validating')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->boolean('is_validating')->default(false)->after('hetzner_server_status');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +23,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('is_validating');
|
||||
});
|
||||
if (Schema::hasColumn('servers', 'is_validating')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('is_validating');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+10
-6
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->string('dev_helper_version')->nullable();
|
||||
});
|
||||
if (! Schema::hasColumn('instance_settings', 'dev_helper_version')) {
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->string('dev_helper_version')->nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +23,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('dev_helper_version');
|
||||
});
|
||||
if (Schema::hasColumn('instance_settings', 'dev_helper_version')) {
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('dev_helper_version');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('scheduled_tasks', function (Blueprint $table) {
|
||||
$table->integer('timeout')->default(300)->after('frequency');
|
||||
});
|
||||
if (! Schema::hasColumn('scheduled_tasks', 'timeout')) {
|
||||
Schema::table('scheduled_tasks', function (Blueprint $table) {
|
||||
$table->integer('timeout')->default(300)->after('frequency');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +23,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('scheduled_tasks', function (Blueprint $table) {
|
||||
$table->dropColumn('timeout');
|
||||
});
|
||||
if (Schema::hasColumn('scheduled_tasks', 'timeout')) {
|
||||
Schema::table('scheduled_tasks', function (Blueprint $table) {
|
||||
$table->dropColumn('timeout');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+31
-9
@@ -11,12 +11,29 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('scheduled_task_executions', function (Blueprint $table) {
|
||||
$table->timestamp('started_at')->nullable()->after('scheduled_task_id');
|
||||
$table->integer('retry_count')->default(0)->after('status');
|
||||
$table->decimal('duration', 10, 2)->nullable()->after('retry_count')->comment('Duration in seconds');
|
||||
$table->text('error_details')->nullable()->after('message');
|
||||
});
|
||||
if (! Schema::hasColumn('scheduled_task_executions', 'started_at')) {
|
||||
Schema::table('scheduled_task_executions', function (Blueprint $table) {
|
||||
$table->timestamp('started_at')->nullable()->after('scheduled_task_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('scheduled_task_executions', 'retry_count')) {
|
||||
Schema::table('scheduled_task_executions', function (Blueprint $table) {
|
||||
$table->integer('retry_count')->default(0)->after('status');
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('scheduled_task_executions', 'duration')) {
|
||||
Schema::table('scheduled_task_executions', function (Blueprint $table) {
|
||||
$table->decimal('duration', 10, 2)->nullable()->after('retry_count')->comment('Duration in seconds');
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('scheduled_task_executions', 'error_details')) {
|
||||
Schema::table('scheduled_task_executions', function (Blueprint $table) {
|
||||
$table->text('error_details')->nullable()->after('message');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,8 +41,13 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('scheduled_task_executions', function (Blueprint $table) {
|
||||
$table->dropColumn(['started_at', 'retry_count', 'duration', 'error_details']);
|
||||
});
|
||||
$columns = ['started_at', 'retry_count', 'duration', 'error_details'];
|
||||
foreach ($columns as $column) {
|
||||
if (Schema::hasColumn('scheduled_task_executions', $column)) {
|
||||
Schema::table('scheduled_task_executions', function (Blueprint $table) use ($column) {
|
||||
$table->dropColumn($column);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+25
-8
@@ -11,11 +11,23 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->integer('restart_count')->default(0)->after('status');
|
||||
$table->timestamp('last_restart_at')->nullable()->after('restart_count');
|
||||
$table->string('last_restart_type', 10)->nullable()->after('last_restart_at');
|
||||
});
|
||||
if (! Schema::hasColumn('applications', 'restart_count')) {
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->integer('restart_count')->default(0)->after('status');
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('applications', 'last_restart_at')) {
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->timestamp('last_restart_at')->nullable()->after('restart_count');
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('applications', 'last_restart_type')) {
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->string('last_restart_type', 10)->nullable()->after('last_restart_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,8 +35,13 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->dropColumn(['restart_count', 'last_restart_at', 'last_restart_type']);
|
||||
});
|
||||
$columns = ['restart_count', 'last_restart_at', 'last_restart_type'];
|
||||
foreach ($columns as $column) {
|
||||
if (Schema::hasColumn('applications', $column)) {
|
||||
Schema::table('applications', function (Blueprint $table) use ($column) {
|
||||
$table->dropColumn($column);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+10
-6
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->string('detected_traefik_version')->nullable();
|
||||
});
|
||||
if (! Schema::hasColumn('servers', 'detected_traefik_version')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->string('detected_traefik_version')->nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +23,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('detected_traefik_version');
|
||||
});
|
||||
if (Schema::hasColumn('servers', 'detected_traefik_version')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('detected_traefik_version');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+10
-6
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('traefik_outdated_email_notifications')->default(true);
|
||||
});
|
||||
if (! Schema::hasColumn('email_notification_settings', 'traefik_outdated_email_notifications')) {
|
||||
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('traefik_outdated_email_notifications')->default(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +23,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('traefik_outdated_email_notifications');
|
||||
});
|
||||
if (Schema::hasColumn('email_notification_settings', 'traefik_outdated_email_notifications')) {
|
||||
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('traefik_outdated_email_notifications');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+10
-6
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||
$table->text('telegram_notifications_traefik_outdated_thread_id')->nullable();
|
||||
});
|
||||
if (! Schema::hasColumn('telegram_notification_settings', 'telegram_notifications_traefik_outdated_thread_id')) {
|
||||
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||
$table->text('telegram_notifications_traefik_outdated_thread_id')->nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +23,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('telegram_notifications_traefik_outdated_thread_id');
|
||||
});
|
||||
if (Schema::hasColumn('telegram_notification_settings', 'telegram_notifications_traefik_outdated_thread_id')) {
|
||||
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('telegram_notifications_traefik_outdated_thread_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+10
-6
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->json('traefik_outdated_info')->nullable();
|
||||
});
|
||||
if (! Schema::hasColumn('servers', 'traefik_outdated_info')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->json('traefik_outdated_info')->nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +23,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('traefik_outdated_info');
|
||||
});
|
||||
if (Schema::hasColumn('servers', 'traefik_outdated_info')) {
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('traefik_outdated_info');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+22
-8
@@ -11,10 +11,17 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->boolean('inject_build_args_to_dockerfile')->default(true)->after('use_build_secrets');
|
||||
$table->boolean('include_source_commit_in_build')->default(false)->after('inject_build_args_to_dockerfile');
|
||||
});
|
||||
if (! Schema::hasColumn('application_settings', 'inject_build_args_to_dockerfile')) {
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->boolean('inject_build_args_to_dockerfile')->default(true)->after('use_build_secrets');
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('application_settings', 'include_source_commit_in_build')) {
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->boolean('include_source_commit_in_build')->default(false)->after('inject_build_args_to_dockerfile');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,9 +29,16 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('inject_build_args_to_dockerfile');
|
||||
$table->dropColumn('include_source_commit_in_build');
|
||||
});
|
||||
if (Schema::hasColumn('application_settings', 'inject_build_args_to_dockerfile')) {
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('inject_build_args_to_dockerfile');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('application_settings', 'include_source_commit_in_build')) {
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('include_source_commit_in_build');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+10
-6
@@ -11,9 +11,11 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->integer('deployment_queue_limit')->default(25)->after('concurrent_builds');
|
||||
});
|
||||
if (! Schema::hasColumn('server_settings', 'deployment_queue_limit')) {
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->integer('deployment_queue_limit')->default(25)->after('concurrent_builds');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +23,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('deployment_queue_limit');
|
||||
});
|
||||
if (Schema::hasColumn('server_settings', 'deployment_queue_limit')) {
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('deployment_queue_limit');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+10
-6
@@ -8,15 +8,19 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->integer('docker_images_to_keep')->default(2);
|
||||
});
|
||||
if (! Schema::hasColumn('application_settings', 'docker_images_to_keep')) {
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->integer('docker_images_to_keep')->default(2);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('docker_images_to_keep');
|
||||
});
|
||||
if (Schema::hasColumn('application_settings', 'docker_images_to_keep')) {
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('docker_images_to_keep');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+10
-6
@@ -8,15 +8,19 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->boolean('disable_application_image_retention')->default(false);
|
||||
});
|
||||
if (! Schema::hasColumn('server_settings', 'disable_application_image_retention')) {
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->boolean('disable_application_image_retention')->default(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('disable_application_image_retention');
|
||||
});
|
||||
if (Schema::hasColumn('server_settings', 'disable_application_image_retention')) {
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('disable_application_image_retention');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -13,25 +13,27 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
|
||||
$table->string('uuid')->nullable()->unique()->after('id');
|
||||
});
|
||||
|
||||
// Generate UUIDs for existing records using chunked processing
|
||||
DB::table('cloud_provider_tokens')
|
||||
->whereNull('uuid')
|
||||
->chunkById(500, function ($tokens) {
|
||||
foreach ($tokens as $token) {
|
||||
DB::table('cloud_provider_tokens')
|
||||
->where('id', $token->id)
|
||||
->update(['uuid' => (string) new Cuid2]);
|
||||
}
|
||||
if (! Schema::hasColumn('cloud_provider_tokens', 'uuid')) {
|
||||
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
|
||||
$table->string('uuid')->nullable()->unique()->after('id');
|
||||
});
|
||||
|
||||
// Make uuid non-nullable after filling in values
|
||||
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
|
||||
$table->string('uuid')->nullable(false)->change();
|
||||
});
|
||||
// Generate UUIDs for existing records using chunked processing
|
||||
DB::table('cloud_provider_tokens')
|
||||
->whereNull('uuid')
|
||||
->chunkById(500, function ($tokens) {
|
||||
foreach ($tokens as $token) {
|
||||
DB::table('cloud_provider_tokens')
|
||||
->where('id', $token->id)
|
||||
->update(['uuid' => (string) new Cuid2]);
|
||||
}
|
||||
});
|
||||
|
||||
// Make uuid non-nullable after filling in values
|
||||
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
|
||||
$table->string('uuid')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,8 +41,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
if (Schema::hasColumn('cloud_provider_tokens', 'uuid')) {
|
||||
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* Trims whitespace from S3 storage fields (key, secret, endpoint, bucket, region)
|
||||
* to fix "Malformed Access Key Id" errors that can occur when users
|
||||
* accidentally paste values with leading/trailing whitespace.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('s3_storages')
|
||||
->select(['id', 'key', 'secret', 'endpoint', 'bucket', 'region'])
|
||||
->orderBy('id')
|
||||
->chunk(100, function ($storages) {
|
||||
foreach ($storages as $storage) {
|
||||
try {
|
||||
DB::transaction(function () use ($storage) {
|
||||
$updates = [];
|
||||
|
||||
// Trim endpoint (not encrypted)
|
||||
if ($storage->endpoint !== null) {
|
||||
$trimmedEndpoint = trim($storage->endpoint);
|
||||
if ($trimmedEndpoint !== $storage->endpoint) {
|
||||
$updates['endpoint'] = $trimmedEndpoint;
|
||||
}
|
||||
}
|
||||
|
||||
// Trim bucket (not encrypted)
|
||||
if ($storage->bucket !== null) {
|
||||
$trimmedBucket = trim($storage->bucket);
|
||||
if ($trimmedBucket !== $storage->bucket) {
|
||||
$updates['bucket'] = $trimmedBucket;
|
||||
}
|
||||
}
|
||||
|
||||
// Trim region (not encrypted)
|
||||
if ($storage->region !== null) {
|
||||
$trimmedRegion = trim($storage->region);
|
||||
if ($trimmedRegion !== $storage->region) {
|
||||
$updates['region'] = $trimmedRegion;
|
||||
}
|
||||
}
|
||||
|
||||
// Trim key (encrypted) - verify re-encryption works before saving
|
||||
if ($storage->key !== null) {
|
||||
try {
|
||||
$decryptedKey = Crypt::decryptString($storage->key);
|
||||
$trimmedKey = trim($decryptedKey);
|
||||
if ($trimmedKey !== $decryptedKey) {
|
||||
$encryptedKey = Crypt::encryptString($trimmedKey);
|
||||
// Verify the new encryption is valid
|
||||
if (Crypt::decryptString($encryptedKey) === $trimmedKey) {
|
||||
$updates['key'] = $encryptedKey;
|
||||
} else {
|
||||
Log::warning("S3 storage ID {$storage->id}: Re-encryption verification failed for key, skipping");
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning("Could not decrypt S3 storage key for ID {$storage->id}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Trim secret (encrypted) - verify re-encryption works before saving
|
||||
if ($storage->secret !== null) {
|
||||
try {
|
||||
$decryptedSecret = Crypt::decryptString($storage->secret);
|
||||
$trimmedSecret = trim($decryptedSecret);
|
||||
if ($trimmedSecret !== $decryptedSecret) {
|
||||
$encryptedSecret = Crypt::encryptString($trimmedSecret);
|
||||
// Verify the new encryption is valid
|
||||
if (Crypt::decryptString($encryptedSecret) === $trimmedSecret) {
|
||||
$updates['secret'] = $encryptedSecret;
|
||||
} else {
|
||||
Log::warning("S3 storage ID {$storage->id}: Re-encryption verification failed for secret, skipping");
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning("Could not decrypt S3 storage secret for ID {$storage->id}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($updates)) {
|
||||
DB::table('s3_storages')->where('id', $storage->id)->update($updates);
|
||||
Log::info("Trimmed whitespace from S3 storage credentials for ID {$storage->id}", [
|
||||
'fields_updated' => array_keys($updates),
|
||||
]);
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("Failed to process S3 storage ID {$storage->id}: ".$e->getMessage());
|
||||
// Continue with next record instead of failing entire migration
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Cannot reverse trimming operation
|
||||
}
|
||||
};
|
||||
@@ -64,9 +64,45 @@ if [ -f /root/.docker/config.json ]; then
|
||||
DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json"
|
||||
fi
|
||||
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
echo "docker-compose.custom.yml detected." >>"$LOGFILE"
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
else
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
fi
|
||||
# Pull all required images before stopping containers
|
||||
# This ensures we don't take down the system if image pull fails (rate limits, network issues, etc.)
|
||||
echo "Pulling required Docker images..." >>"$LOGFILE"
|
||||
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify helper image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
docker pull postgres:15-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull PostgreSQL image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
docker pull redis:7-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull Redis image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
# Pull realtime image - version is hardcoded in docker-compose.prod.yml, extract it or use a known version
|
||||
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify realtime image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
echo "All images pulled successfully." >>"$LOGFILE"
|
||||
|
||||
# Stop and remove existing Coolify containers to prevent conflicts
|
||||
# This handles both old installations (project "source") and new ones (project "coolify")
|
||||
# Use nohup to ensure the script continues even if SSH connection is lost
|
||||
echo "Starting container restart sequence (detached)..." >>"$LOGFILE"
|
||||
|
||||
nohup bash -c "
|
||||
LOGFILE='$LOGFILE'
|
||||
DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT'
|
||||
REGISTRY_URL='$REGISTRY_URL'
|
||||
LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION'
|
||||
LATEST_IMAGE='$LATEST_IMAGE'
|
||||
|
||||
# Stop and remove containers
|
||||
echo 'Stopping existing Coolify containers...' >>\"\$LOGFILE\"
|
||||
for container in coolify coolify-db coolify-redis coolify-realtime; do
|
||||
if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then
|
||||
docker stop \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
|
||||
docker rm \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
|
||||
echo \" - Removed container: \$container\" >>\"\$LOGFILE\"
|
||||
fi
|
||||
done
|
||||
|
||||
# Start new containers
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
echo 'docker-compose.custom.yml detected.' >>\"\$LOGFILE\"
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
else
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
fi
|
||||
echo 'Upgrade completed.' >>\"\$LOGFILE\"
|
||||
" >>"$LOGFILE" 2>&1 &
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.454"
|
||||
"version": "4.0.0-beta.455"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.455"
|
||||
"version": "4.0.0-beta.456"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.12"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<use xlink:href="#_Image1" x="24.998" y="184.205" width="450px" height="132px"/>
|
||||
<defs>
|
||||
<image id="_Image1" width="450px" height="132px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcIAAACECAYAAAADbiuAAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nOy9d5xdV3nv/XvW2u30Nn1GvVjF3ZZtbATGxgRsDDEQ06svhJBAyg3Bl7RL8sILIclNIAFCPqaFNyTkGkMcbJqNiXFv2JZs2SqjNv3M6WW3tZ73jzMzmtGM5BkVS7bP9/PRRzpn773W2kv7rGc/62nAKcCOOufZUWfcjjqDdtTZbq9MbZ4+ls5lZDqX+W46lymkc5kD6VzmnadijG3atGnT5qWBcYr6zQDoBNCJ9fEanZ++IUJd3+RAW7UnCv2O6cREVTXYoGR4bvyciN1xZ/OmnSOnaKxt2rRp0+ZFDJ2KTu2ocwmA+7A6NiYuznaHTxS0eqZCADQEQvtNK22RcJgEsao0xdRlFzdv2vngqRhvmzZt2rR58SJPRCOxdCxmOdY6y7E6p/7owA2aC53rvG/tGvQ510DzeeLcdCcMAWgQ18IJipu+eU42IfsS2lnXAyMbB4dqr3aDDBgJ8/zcHvP83Fj4WGGmvcgN65Lm+TkrfKzgn4h7adOmTZs2Ly2OWyOMvHXVr9GPx/+TAGvW1z6ASL1U17PPNd/cn9F3TuykLofNl3d3HLHNTQNF4ZiZ6c+sOWhu229yqAFgY/OmnTtmzr1h3S8BxJo37TzveO+lTZs2bdq89DgmQei8fVWEq8G7IAhKqotEQ/0Pmgw1FCvSXEWIveGA+WWhZJEkGRDEIEAJdRnGvN9Bl9U0e5Kx1ggIIuaAtQY3PACAiNraXtU5Kmyzj5nrwVBRB+PlBJkS2g9v4krwCyLScKRFlvgqWprtDQDuat60c/AEzU2bNm3atHkJsGRBGLlhXTz8+eh9XPLPRKCBzQkWy6IjMhnpAzNYMzhQ1fDeCRN535nT2apontbEKkYmuhrU6toY6KzLmFXWvo6qyYqlK/UoAIio5Uc29FvaDfY1nzq4Qqajnr2qy6rdvJ3CxyYBAKLTgfGKrtld3NW8aeerjnUy2rRp06bNS48l2QiNuNnLefcvUQ2ugaeBPnsSK6JapiI5EgSK2BARCySFzVECbFKUtULKWSF1Wg30OGR2xHshCCIRBQhgP2ARj9TNhN3FhlGScadm5OKTVn/WICKHDJmUqeheszNpE5Gtak0gVDA2Z2Ce3wURswAGoDQA9Jnn54R5fu7J8LHCgjbKNm3atGnTZjaLDp+I3LDOUc+W/1k/VrwGALA8EmBdlM1sLA5BELmkJ5MxGwBYaw8Emztjs5uY0Q5FLjkqk7EeVarVdbEa41BJEgQr5XTOOm3mdBm1V80MeFmySAkj7Wzo84ho5mTWuuEPFY1wovIpABcAeOMS5qFNmzZt2rxEWZRGaOScVZS1vqzj9GvIWnmsijapP6LMdCwHQRDJqCsziRmhREQGxSIlXWs6YG511JmuGV2pMZmOKxF1XDDq7AWKm15MJiJ1GbPjixmLiDum2ZkkIjJnf09Epow7JQ4UdMPbnJpAOW7aY42mW1rCfLRp06ZNm5cYixKEQog/5H31t4h18SGjN7FaJu2YcMzItJ1PZBI+mcaMYBK2LEAjI2KRYQ4CkpnEPhGP2EbS7pFRMyItmdKeIpKixn4YF+mYKx0zdsQBzIKIBI5g2yRBUSMdM1FwyXy8/FoAextN9/7FtNumTZs2bV6aiOc+ZQqNnA50dqFDarQQZT/YNv2ZpMiQpIKwDWEt7yyLRCQCIBvW/EkiAjQDQJxMo9Na1VWwUpEuANC+gmoEx3VDYbmxR4/Wpj/Wj6uxNm3atGnzomexNkIGQFC6dqQTtOt3SaulFKpm0LBSTnZGb2OGX/aqYOR0oFxhSIdM4UrHsFt2vlbz3BKQoJgBGAT4GtAMDhlQvKiBCts0OGMi7DKb/qbYmyLpzn9v3rSzssj7bNOmTZs2LzEWKwg9AAqMI25fkhA1AK1YBkaMlQYZUwonEay0k1DNsBbWgogRJ5hxy9Gh1kHFA1pbnQ0zZUdljwPKWPPaV3vqQKDnfX84wjGXO+cur7kRIy4n/dfmHgnvl5tXXTO4fbAdX3gE+na9zQKwHcDbhtf+2yOnejxt2rRp83yyuK3RiPy6cXXffWSJWJhvjOFw5YwIFHO6FrpUuWEjbASTrBgyYsSlYwgSBFaMsObvRUsIsrBljQSBPQ1uqjltcFMBikHO4qI9SIq4vaa7alY4lAE2AvjMqs2rzOe88CVI4hObU/72yTfoiifVRPOMUz2eNm3atHm+WZRGGOabQ2bSvBYPlm/lRrBZXShqMu7MeHnKztQBIlq20LWqGUoAOe0pCEf6RsS0WDGmNMHVJOhZI26tJ0ldAMDlAFwOIDptUNYCfA19oAFKmBC9DvTeOthbhGZomwmVNLiV7Q1vA7Bu1eZVWwe3D7bjC6dIfGKzAWCPd+9wtpXT55RVI2nTpk2bU8ainWWaN+0sATBQCT0O9QQAUMQOjGVdIyIWmScEw0YwGDaCQQD21Fdau0opLxwJav6Bqd6LZspeT3K+E6ie8KDzHiAJosuB6LbnnfNc6DXJYnVA+G6WRvwEksrC11dtXjUvUPF0ZO3j73zT2sffuaCWfSI4f+j3enMvW/vbALIAdgF4B4BbAOCK+z4mrrjvY1tOVt9t2rRpczqxeK9RAHBVATEZISIbRJDdGSZD9i50KitepT21CoQxI2GFZIp9ACKqEfZC8zIAFTNuZxa6lhwJSAJP+uByAEqbgGgJS16czwwAwOhNx8Kt3aPVi5JdhbPMddrE+QB+sKR7PnV8CcC7T1bjrPQPzaTzJ1Mfb61+bvt3qp/bXgUArdQHWevbr7jvYy+Il4Y2bdq0OR4WtRVmvqkvR1L+vW74rwRIiJiVAjPUyKRBMeegiDpMprHg1qgRszqIIDnQKwGEwpZDSBmjIm0NUFkl4c/f5hQDkZYg9DTIEi1hGDda2mHSbGmKi4CIbCMTX25k4ggmq5N621gC4LMXdfEpIvtHmzYBeEP9gf01Z2NX58nqpzlcWjvxi2cSUx8jAND7zLsFgN8cfHTve5PCUZ3L+zMA2gWR27Rp86JmcTah7bV3scIbaHPMlZlojmRLkWQvEOwFA7pQhbGiu0RCpA+7siZMEZ8KiyAImpBdTj9WOP0gMpj8gMb9uRlipgQeAJAtAM3QeQ/CIFDMaNkNSz4QLkE1BCDjkaqfpE67wtFVm1eZg9sHjy9g8eTxNwBe6+2ahLe36J6sTgoP7A6UOzMF08/Becz8xVq+IK1YpogpAdmmTZs2L2YWJQhpMswAiLOg4WkhOI9ABbDnHWtJwNa2phK2iGGZE4DIwbBbp2I4E44h+iOtLVGDwJUAXAvRymFqQa6JAyFDj7jgRrhkIQgAwjZWBpd1HRjbk4/pQD3hrF42CVcTBiIfcb++6/ElN3iC6b/r+us4UK9zf/r0VQC2AbgaoR4/0f0k/nXrK8H8ieaz5QSARwG8AcAYALDSifDh4YouNDO+cDQWSPzapk2bNi82FusluI5NKqGV3mxBdN1NSntehILNikGSQJKUsGQSAsCkX6BCODdLjaBWED23HGWmhZ0OGWIgAl3wwZXjU+LMXGIZEVWb/7pjHe9vbAAhEJd3vjdyw7r/2bxp59Kl64nltSC8Hq20d/sLf/XUgZPUzx+B6HUIGAAOVD+3fWj6gHfzUwTNGQBw681htOJH27Rp0+ZFzYKC0OqIJgF8G62tsTQb1Idz42kAE0dqSJdrNjc9QBDIMkuyIxkYMasjqHihETcNM2FbIAA7GgAhy2uiQEQAGqDBJvRQs2UPVDxH4+N6CDVYb2WZkQTR64AkQQ27iwqwn3fD2XjCvKofqtIENJswxe8b/11aluzL/m5luDC85AZPHCl2w6Xf0NIxwRxCsQEgBIDMp84lAP8Aze+aOuevx/7o/o+PoZ2mtU2bFwu5r19OaJlevjT5/rt2nerxnE4cSSN8PYBrZz6tjQCWgHDMo7rzcxACYIA5LUyxX5iCyBSBDjSEKVt9KQbisiUEgZbfqkVAk8GuWrjhaYcaQSCDAEkgR4CPQRACgExGoOpuyxbpaVBdvQXAVwGcMkEY7i+s0lW3D8ABAHeexK4EFNcBpDAlCNEKofjIrHPuPYn9t2nT5tRwJoDfRyu4+sZTPJbTijmCcEoTfJqBLDbHPMSkDQlAEIukEwjLmJ/7bAqRjs8uxcTa18sD9j0zbs13uKgp0PZaaytUYToJ93MTaKi9DZAjIfodoMuGHnFBSQNkS7CroMc8UERCdNvQeR/BwfI+Ix1bMasVDsuNmcBFOqR9npJg8o7vX3sth+oPgvv3XcQKNTKxovBXT53MbVpCyP7Uv6ffJA5PnXeEN5I2bdq8EMl9/fKfAng1ANhW5P0X3vlbW4novQ+96kttzRDz4wh7AfQhLseRNmyYNB2/Rwj1USs5UNSenP0RADjQR/Z6ZAABL14Izr7UVTNp2ES3DYq2knSTJVraYnLK89QkBMOlFax0k5We1H44HIyVa7pxyPTFjvAZ+AmAnUseyInh1QDOZJfy8MX4SRaCACDgq2lNcFr4Rw87py0I27R5cTHCSt+rK672mo0uzfpMtHaC2mC+FtQqYpuQhojZIFNAVdxWCEPDz5AhQNYRFCfN8x1p6OR5HephFxSVEL2tLvSEB4oZkCtbazo3FLgUgLVWjcf3RbBAKAD9Is+8MXFrdbjwlpM1zkUQB8OHoiiAwknvTXEUtRlHpen/zMPn5vmwVbZp0+Z5YvL9d70n/vb+1QB2y1WZUiVXqGfSnYuqAftSYI5U8/ONMWtZ4rPosl6vK14fOXKuxmaIloa4gBanJ8u9yCQPUNTuJSIDQFU6RmKqwtKxYxIQlUA5nHeIGy3NkGwBihvQeR8iMKbG4wMMOGf0Line truncated
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 102 KiB |
@@ -29,17 +29,23 @@
|
||||
|
||||
@php
|
||||
use App\Models\InstanceSettings;
|
||||
// Global setting to disable ALL two-step confirmation (text + password)
|
||||
$disableTwoStepConfirmation = data_get(InstanceSettings::get(), 'disable_two_step_confirmation');
|
||||
// Skip ONLY password confirmation for OAuth users (they have no password)
|
||||
$skipPasswordConfirmation = shouldSkipPasswordConfirmation();
|
||||
if ($temporaryDisableTwoStepConfirmation) {
|
||||
$disableTwoStepConfirmation = false;
|
||||
$skipPasswordConfirmation = false;
|
||||
}
|
||||
// When password step is skipped, Step 2 becomes final - change button text from "Continue" to "Confirm"
|
||||
$effectiveStep2ButtonText = ($skipPasswordConfirmation && $step2ButtonText === 'Continue') ? 'Confirm' : $step2ButtonText;
|
||||
@endphp
|
||||
|
||||
<div {{ $ignoreWire ? 'wire:ignore' : '' }} x-data="{
|
||||
modalOpen: false,
|
||||
step: {{ empty($checkboxes) ? 2 : 1 }},
|
||||
initialStep: {{ empty($checkboxes) ? 2 : 1 }},
|
||||
finalStep: {{ $confirmWithPassword && !$disableTwoStepConfirmation ? 3 : 2 }},
|
||||
finalStep: {{ $confirmWithPassword && !$skipPasswordConfirmation ? 3 : 2 }},
|
||||
deleteText: '',
|
||||
password: '',
|
||||
actions: @js($actions),
|
||||
@@ -50,7 +56,7 @@
|
||||
})(),
|
||||
userConfirmationText: '',
|
||||
confirmWithText: @js($confirmWithText && !$disableTwoStepConfirmation),
|
||||
confirmWithPassword: @js($confirmWithPassword && !$disableTwoStepConfirmation),
|
||||
confirmWithPassword: @js($confirmWithPassword && !$skipPasswordConfirmation),
|
||||
submitAction: @js($submitAction),
|
||||
dispatchAction: @js($dispatchAction),
|
||||
passwordError: '',
|
||||
@@ -59,6 +65,7 @@
|
||||
dispatchEventType: @js($dispatchEventType),
|
||||
dispatchEventMessage: @js($dispatchEventMessage),
|
||||
disableTwoStepConfirmation: @js($disableTwoStepConfirmation),
|
||||
skipPasswordConfirmation: @js($skipPasswordConfirmation),
|
||||
resetModal() {
|
||||
this.step = this.initialStep;
|
||||
this.deleteText = '';
|
||||
@@ -68,7 +75,7 @@
|
||||
$wire.$refresh();
|
||||
},
|
||||
step1ButtonText: @js($step1ButtonText),
|
||||
step2ButtonText: @js($step2ButtonText),
|
||||
step2ButtonText: @js($effectiveStep2ButtonText),
|
||||
step3ButtonText: @js($step3ButtonText),
|
||||
validatePassword() {
|
||||
if (this.confirmWithPassword && !this.password) {
|
||||
@@ -92,10 +99,14 @@
|
||||
const paramsMatch = this.submitAction.match(/\((.*?)\)/);
|
||||
const params = paramsMatch ? paramsMatch[1].split(',').map(param => param.trim()) : [];
|
||||
|
||||
if (this.confirmWithPassword) {
|
||||
params.push(this.password);
|
||||
// Always pass password parameter (empty string if password confirmation is skipped)
|
||||
// This ensures consistent method signature for backend Livewire methods
|
||||
params.push(this.confirmWithPassword ? this.password : '');
|
||||
|
||||
// Only pass selectedActions if there are checkboxes with selections
|
||||
if (this.selectedActions.length > 0) {
|
||||
params.push(this.selectedActions);
|
||||
}
|
||||
params.push(this.selectedActions);
|
||||
return $wire[methodName](...params)
|
||||
.then(result => {
|
||||
if (result === true) {
|
||||
@@ -316,7 +327,7 @@
|
||||
if (dispatchEvent) {
|
||||
$wire.dispatch(dispatchEventType, dispatchEventMessage);
|
||||
}
|
||||
if (confirmWithPassword && !disableTwoStepConfirmation) {
|
||||
if (confirmWithPassword && !skipPasswordConfirmation) {
|
||||
step++;
|
||||
} else {
|
||||
modalOpen = false;
|
||||
@@ -330,7 +341,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Password confirmation -->
|
||||
@if (!$disableTwoStepConfirmation)
|
||||
@if (!$skipPasswordConfirmation)
|
||||
<div x-show="step === 3 && confirmWithPassword">
|
||||
<x-callout type="danger" title="Final Confirmation" class="mb-4">
|
||||
Please enter your password to confirm this destructive action.
|
||||
|
||||
@@ -3,47 +3,457 @@
|
||||
'lastDeploymentLink' => null,
|
||||
'resource' => null,
|
||||
])
|
||||
@php
|
||||
$projects = auth()->user()->currentTeam()->projects()->get();
|
||||
$environments = $resource->environment->project->environments()->with(['applications', 'services'])->get();
|
||||
$currentProjectUuid = data_get($resource, 'environment.project.uuid');
|
||||
$currentEnvironmentUuid = data_get($resource, 'environment.uuid');
|
||||
$currentResourceUuid = data_get($resource, 'uuid');
|
||||
@endphp
|
||||
<nav class="flex pt-2 pb-10">
|
||||
<ol class="flex flex-wrap items-center gap-y-1">
|
||||
<li class="inline-flex items-center">
|
||||
<div class="flex items-center">
|
||||
<a class="text-xs truncate lg:text-sm"
|
||||
href="{{ route('project.show', ['project_uuid' => data_get($resource, 'environment.project.uuid')]) }}">
|
||||
{{ data_get($resource, 'environment.project.name', 'Undefined Name') }}</a>
|
||||
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
|
||||
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<!-- Project Level -->
|
||||
<li class="inline-flex items-center" x-data="{ projectOpen: false }">
|
||||
<div class="flex items-center relative" @mouseenter="projectOpen = true" @mouseleave="projectOpen = false">
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning"
|
||||
href="{{ route('project.show', ['project_uuid' => $currentProjectUuid]) }}">
|
||||
{{ data_get($resource, 'environment.project.name', 'Undefined Name') }}
|
||||
</a>
|
||||
<span class="px-1 text-warning">
|
||||
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-down': projectOpen }" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<!-- Project Dropdown -->
|
||||
<div x-show="projectOpen"
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-20 top-full mt-1 w-56 -ml-2 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@foreach ($projects as $project)
|
||||
<a href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}"
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200 {{ $project->uuid === $currentProjectUuid ? 'dark:text-warning font-semibold' : '' }}"
|
||||
title="{{ $project->name }}">
|
||||
{{ $project->name }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<a class="text-xs truncate lg:text-sm"
|
||||
|
||||
<!-- Environment Level -->
|
||||
<li class="inline-flex items-center" x-data="{ envOpen: false, activeEnv: null, activeRes: null, activeMenuEnv: null }">
|
||||
<div class="flex items-center relative" @mouseenter="envOpen = true" @mouseleave="envOpen = false; activeEnv = null; activeRes = null; activeMenuEnv = null">
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning"
|
||||
href="{{ route('project.resource.index', [
|
||||
'environment_uuid' => data_get($resource, 'environment.uuid'),
|
||||
'project_uuid' => data_get($resource, 'environment.project.uuid'),
|
||||
]) }}">{{ data_get($resource, 'environment.name') }}</a>
|
||||
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
|
||||
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
'environment_uuid' => $currentEnvironmentUuid,
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
]) }}">
|
||||
{{ data_get($resource, 'environment.name') }}
|
||||
</a>
|
||||
<span class="px-1 text-warning">
|
||||
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-down': envOpen }" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<!-- Environment Dropdown Container -->
|
||||
<div x-show="envOpen"
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-20 top-full mt-1 flex items-start">
|
||||
<!-- Environment List -->
|
||||
<div class="w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@foreach ($environments as $environment)
|
||||
@php
|
||||
$envResources = collect()
|
||||
->merge($environment->applications->map(fn($app) => ['type' => 'application', 'resource' => $app]))
|
||||
->merge($environment->databases()->map(fn($db) => ['type' => 'database', 'resource' => $db]))
|
||||
->merge($environment->services->map(fn($svc) => ['type' => 'service', 'resource' => $svc]));
|
||||
@endphp
|
||||
<div @mouseenter="activeEnv = '{{ $environment->uuid }}'; activeRes = null" @mouseleave="activeEnv = null">
|
||||
<a href="{{ route('project.resource.index', [
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
]) }}"
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200 {{ $environment->uuid === $currentEnvironmentUuid ? 'dark:text-warning font-semibold' : '' }}"
|
||||
title="{{ $environment->name }}">
|
||||
<span class="truncate">{{ $environment->name }}</span>
|
||||
@if ($envResources->count() > 0)
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Resources Sub-dropdown (2nd level) -->
|
||||
@foreach ($environments as $environment)
|
||||
@php
|
||||
$envResources = collect()
|
||||
->merge($environment->applications->map(fn($app) => ['type' => 'application', 'resource' => $app]))
|
||||
->merge($environment->databases()->map(fn($db) => ['type' => 'database', 'resource' => $db]))
|
||||
->merge($environment->services->map(fn($svc) => ['type' => 'service', 'resource' => $svc]));
|
||||
@endphp
|
||||
@if ($envResources->count() > 0)
|
||||
<div x-show="activeEnv === '{{ $environment->uuid }}'"
|
||||
x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
@mouseenter="activeEnv = '{{ $environment->uuid }}'" @mouseleave="activeEnv = null; activeRes = null"
|
||||
class="ml-1 flex items-start">
|
||||
<div class="w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@foreach ($envResources as $envResource)
|
||||
@php
|
||||
$resType = $envResource['type'];
|
||||
$res = $envResource['resource'];
|
||||
$resRoute = match($resType) {
|
||||
'application' => route('project.application.configuration', [
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'application_uuid' => $res->uuid,
|
||||
]),
|
||||
'service' => route('project.service.configuration', [
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'service_uuid' => $res->uuid,
|
||||
]),
|
||||
'database' => route('project.database.configuration', [
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'database_uuid' => $res->uuid,
|
||||
]),
|
||||
};
|
||||
$isCurrentResource = $res->uuid === $currentResourceUuid;
|
||||
@endphp
|
||||
<div @mouseenter="activeRes = '{{ $environment->uuid }}-{{ $res->uuid }}'" @mouseleave="activeRes = null">
|
||||
<a href="{{ $resRoute }}"
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200 {{ $isCurrentResource ? 'dark:text-warning font-semibold' : '' }}"
|
||||
title="{{ $res->name }}">
|
||||
<span class="truncate">{{ $res->name }}</span>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Main Menu Sub-dropdown (3rd level) -->
|
||||
@foreach ($envResources as $envResource)
|
||||
@php
|
||||
$resType = $envResource['type'];
|
||||
$res = $envResource['resource'];
|
||||
$resParams = [
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
];
|
||||
if ($resType === 'application') {
|
||||
$resParams['application_uuid'] = $res->uuid;
|
||||
} elseif ($resType === 'service') {
|
||||
$resParams['service_uuid'] = $res->uuid;
|
||||
} else {
|
||||
$resParams['database_uuid'] = $res->uuid;
|
||||
}
|
||||
$resKey = $environment->uuid . '-' . $res->uuid;
|
||||
@endphp
|
||||
<div x-show="activeRes === '{{ $resKey }}'"
|
||||
x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
@mouseenter="activeRes = '{{ $resKey }}'" @mouseleave="activeRes = null; activeMenuEnv = null"
|
||||
class="ml-1 flex items-start">
|
||||
<!-- Main Menu List -->
|
||||
<div class="w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-coolgray-200">
|
||||
@if ($resType === 'application')
|
||||
<div @mouseenter="activeMenuEnv = '{{ $resKey }}-config'" @mouseleave="activeMenuEnv = null">
|
||||
<a href="{{ route('project.application.configuration', $resParams) }}"
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.application.deployment.index', $resParams) }}" class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Deployments</a>
|
||||
<a href="{{ route('project.application.logs', $resParams) }}" class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Logs</a>
|
||||
@can('canAccessTerminal')
|
||||
<a href="{{ route('project.application.command', $resParams) }}" class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Terminal</a>
|
||||
@endcan
|
||||
@elseif ($resType === 'service')
|
||||
<div @mouseenter="activeMenuEnv = '{{ $resKey }}-config'" @mouseleave="activeMenuEnv = null">
|
||||
<a href="{{ route('project.service.configuration', $resParams) }}"
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.service.logs', $resParams) }}" class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Logs</a>
|
||||
@can('canAccessTerminal')
|
||||
<a href="{{ route('project.service.command', $resParams) }}" class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Terminal</a>
|
||||
@endcan
|
||||
@else
|
||||
<div @mouseenter="activeMenuEnv = '{{ $resKey }}-config'" @mouseleave="activeMenuEnv = null">
|
||||
<a href="{{ route('project.database.configuration', $resParams) }}"
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.database.logs', $resParams) }}" class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Logs</a>
|
||||
@can('canAccessTerminal')
|
||||
<a href="{{ route('project.database.command', $resParams) }}" class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Terminal</a>
|
||||
@endcan
|
||||
@if (
|
||||
$res->getMorphClass() === 'App\Models\StandalonePostgresql' ||
|
||||
$res->getMorphClass() === 'App\Models\StandaloneMongodb' ||
|
||||
$res->getMorphClass() === 'App\Models\StandaloneMysql' ||
|
||||
$res->getMorphClass() === 'App\Models\StandaloneMariadb')
|
||||
<a href="{{ route('project.database.backup.index', $resParams) }}" class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Backups</a>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Configuration Sub-menu (4th level) -->
|
||||
<div x-show="activeMenuEnv === '{{ $resKey }}-config'"
|
||||
x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
@mouseenter="activeMenuEnv = '{{ $resKey }}-config'" @mouseleave="activeMenuEnv = null"
|
||||
class="ml-1 w-52 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@if ($resType === 'application')
|
||||
<a href="{{ route('project.application.configuration', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.application.environment-variables', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Environment Variables</a>
|
||||
<a href="{{ route('project.application.persistent-storage', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Persistent Storage</a>
|
||||
<a href="{{ route('project.application.source', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Source</a>
|
||||
<a href="{{ route('project.application.servers', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Servers</a>
|
||||
<a href="{{ route('project.application.scheduled-tasks.show', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Scheduled Tasks</a>
|
||||
<a href="{{ route('project.application.webhooks', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.application.preview-deployments', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Preview Deployments</a>
|
||||
<a href="{{ route('project.application.healthcheck', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Healthcheck</a>
|
||||
<a href="{{ route('project.application.rollback', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Rollback</a>
|
||||
<a href="{{ route('project.application.resource-limits', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Resource Limits</a>
|
||||
<a href="{{ route('project.application.resource-operations', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Resource Operations</a>
|
||||
<a href="{{ route('project.application.metrics', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Metrics</a>
|
||||
<a href="{{ route('project.application.tags', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.application.advanced', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Advanced</a>
|
||||
<a href="{{ route('project.application.danger', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200 text-red-500">Danger Zone</a>
|
||||
@elseif ($resType === 'service')
|
||||
<a href="{{ route('project.service.configuration', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.service.environment-variables', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Environment Variables</a>
|
||||
<a href="{{ route('project.service.storages', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Storages</a>
|
||||
<a href="{{ route('project.service.scheduled-tasks.show', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Scheduled Tasks</a>
|
||||
<a href="{{ route('project.service.webhooks', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.service.resource-operations', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Resource Operations</a>
|
||||
<a href="{{ route('project.service.tags', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.service.danger', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200 text-red-500">Danger Zone</a>
|
||||
@else
|
||||
<a href="{{ route('project.database.configuration', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.database.environment-variables', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Environment Variables</a>
|
||||
<a href="{{ route('project.database.servers', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Servers</a>
|
||||
<a href="{{ route('project.database.persistent-storage', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Persistent Storage</a>
|
||||
<a href="{{ route('project.database.webhooks', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.database.resource-limits', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Resource Limits</a>
|
||||
<a href="{{ route('project.database.resource-operations', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Resource Operations</a>
|
||||
<a href="{{ route('project.database.metrics', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Metrics</a>
|
||||
<a href="{{ route('project.database.tags', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.database.danger', $resParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200 text-red-500">Danger Zone</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<span class="text-xs truncate lg:text-sm">{{ data_get($resource, 'name') }}</span>
|
||||
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
|
||||
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
|
||||
<!-- Resource Level -->
|
||||
@php
|
||||
$resourceUuid = data_get($resource, 'uuid');
|
||||
$resourceType = $resource->getMorphClass();
|
||||
$isApplication = $resourceType === 'App\Models\Application';
|
||||
$isService = $resourceType === 'App\Models\Service';
|
||||
$isDatabase = str_contains($resourceType, 'Database') || str_contains($resourceType, 'Standalone');
|
||||
$routeParams = [
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
'environment_uuid' => $currentEnvironmentUuid,
|
||||
];
|
||||
if ($isApplication) {
|
||||
$routeParams['application_uuid'] = $resourceUuid;
|
||||
} elseif ($isService) {
|
||||
$routeParams['service_uuid'] = $resourceUuid;
|
||||
} else {
|
||||
$routeParams['database_uuid'] = $resourceUuid;
|
||||
}
|
||||
@endphp
|
||||
<li class="inline-flex items-center" x-data="{ resourceOpen: false, activeMenu: null }">
|
||||
<div class="flex items-center relative" @mouseenter="resourceOpen = true" @mouseleave="resourceOpen = false; activeMenu = null">
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning"
|
||||
href="{{ $isApplication
|
||||
? route('project.application.configuration', $routeParams)
|
||||
: ($isService
|
||||
? route('project.service.configuration', $routeParams)
|
||||
: route('project.database.configuration', $routeParams)) }}">
|
||||
{{ data_get($resource, 'name') }}
|
||||
</a>
|
||||
<span class="px-1 text-warning">
|
||||
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-down': resourceOpen }" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<!-- Resource Dropdown Container -->
|
||||
<div x-show="resourceOpen"
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-20 top-full mt-1 flex items-start">
|
||||
<!-- Main Menu List -->
|
||||
<div class="w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-coolgray-200">
|
||||
@if ($isApplication)
|
||||
<!-- Application Main Menus -->
|
||||
<div @mouseenter="activeMenu = 'config'" @mouseleave="activeMenu = null">
|
||||
<a href="{{ route('project.application.configuration', $routeParams) }}"
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.application.deployment.index', $routeParams) }}"
|
||||
class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
Deployments
|
||||
</a>
|
||||
<a href="{{ route('project.application.logs', $routeParams) }}"
|
||||
class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
Logs
|
||||
</a>
|
||||
@can('canAccessTerminal')
|
||||
<a href="{{ route('project.application.command', $routeParams) }}"
|
||||
class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
Terminal
|
||||
</a>
|
||||
@endcan
|
||||
@elseif ($isService)
|
||||
<!-- Service Main Menus -->
|
||||
<div @mouseenter="activeMenu = 'config'" @mouseleave="activeMenu = null">
|
||||
<a href="{{ route('project.service.configuration', $routeParams) }}"
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.service.logs', $routeParams) }}"
|
||||
class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
Logs
|
||||
</a>
|
||||
@can('canAccessTerminal')
|
||||
<a href="{{ route('project.service.command', $routeParams) }}"
|
||||
class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
Terminal
|
||||
</a>
|
||||
@endcan
|
||||
@else
|
||||
<!-- Database Main Menus -->
|
||||
<div @mouseenter="activeMenu = 'config'" @mouseleave="activeMenu = null">
|
||||
<a href="{{ route('project.database.configuration', $routeParams) }}"
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.database.logs', $routeParams) }}"
|
||||
class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
Logs
|
||||
</a>
|
||||
@can('canAccessTerminal')
|
||||
<a href="{{ route('project.database.command', $routeParams) }}"
|
||||
class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
Terminal
|
||||
</a>
|
||||
@endcan
|
||||
@if (
|
||||
$resourceType === 'App\Models\StandalonePostgresql' ||
|
||||
$resourceType === 'App\Models\StandaloneMongodb' ||
|
||||
$resourceType === 'App\Models\StandaloneMysql' ||
|
||||
$resourceType === 'App\Models\StandaloneMariadb')
|
||||
<a href="{{ route('project.database.backup.index', $routeParams) }}"
|
||||
class="block px-4 py-2 text-sm hover:bg-coolgray-200 dark:hover:bg-coolgray-200">
|
||||
Backups
|
||||
</a>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Configuration Sub-menu -->
|
||||
<div x-show="activeMenu === 'config'"
|
||||
x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
@mouseenter="activeMenu = 'config'" @mouseleave="activeMenu = null"
|
||||
class="ml-1 w-52 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@if ($isApplication)
|
||||
<a href="{{ route('project.application.configuration', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.application.environment-variables', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Environment Variables</a>
|
||||
<a href="{{ route('project.application.persistent-storage', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Persistent Storage</a>
|
||||
<a href="{{ route('project.application.source', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Source</a>
|
||||
<a href="{{ route('project.application.servers', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Servers</a>
|
||||
<a href="{{ route('project.application.scheduled-tasks.show', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Scheduled Tasks</a>
|
||||
<a href="{{ route('project.application.webhooks', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.application.preview-deployments', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Preview Deployments</a>
|
||||
<a href="{{ route('project.application.healthcheck', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Healthcheck</a>
|
||||
<a href="{{ route('project.application.rollback', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Rollback</a>
|
||||
<a href="{{ route('project.application.resource-limits', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Resource Limits</a>
|
||||
<a href="{{ route('project.application.resource-operations', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Resource Operations</a>
|
||||
<a href="{{ route('project.application.metrics', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Metrics</a>
|
||||
<a href="{{ route('project.application.tags', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.application.advanced', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Advanced</a>
|
||||
<a href="{{ route('project.application.danger', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200 text-red-500">Danger Zone</a>
|
||||
@elseif ($isService)
|
||||
<a href="{{ route('project.service.configuration', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.service.environment-variables', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Environment Variables</a>
|
||||
<a href="{{ route('project.service.storages', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Storages</a>
|
||||
<a href="{{ route('project.service.scheduled-tasks.show', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Scheduled Tasks</a>
|
||||
<a href="{{ route('project.service.webhooks', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.service.resource-operations', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Resource Operations</a>
|
||||
<a href="{{ route('project.service.tags', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.service.danger', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200 text-red-500">Danger Zone</a>
|
||||
@else
|
||||
<a href="{{ route('project.database.configuration', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.database.environment-variables', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Environment Variables</a>
|
||||
<a href="{{ route('project.database.servers', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Servers</a>
|
||||
<a href="{{ route('project.database.persistent-storage', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Persistent Storage</a>
|
||||
<a href="{{ route('project.database.webhooks', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.database.resource-limits', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Resource Limits</a>
|
||||
<a href="{{ route('project.database.resource-operations', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Resource Operations</a>
|
||||
<a href="{{ route('project.database.metrics', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Metrics</a>
|
||||
<a href="{{ route('project.database.tags', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.database.danger', $routeParams) }}" class="block px-4 py-2 text-sm truncate hover:bg-coolgray-200 dark:hover:bg-coolgray-200 text-red-500">Danger Zone</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Current Section Status -->
|
||||
@if ($resource->getMorphClass() == 'App\Models\Service')
|
||||
<x-status.services :service="$resource" />
|
||||
@else
|
||||
@@ -51,3 +461,13 @@
|
||||
@endif
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.rotate-down {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.transition-transform {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
@props(['step' => 0])
|
||||
|
||||
{{--
|
||||
Step Mapping (Backend → UI):
|
||||
Backend steps 1-2 (config download, env update) → UI Step 1: Preparing
|
||||
Backend step 3 (pulling images) → UI Step 2: Helper + UI Step 3: Image
|
||||
Backend steps 4-5 (stop/start containers) → UI Step 4: Restart
|
||||
Backend step 6 (complete) → mapped in JS mapStepToUI() in upgrade.blade.php
|
||||
|
||||
The currentStep variable is inherited from parent Alpine component (upgradeModal).
|
||||
--}}
|
||||
<div class="w-full max-w-md mx-auto" x-data="{ activeStep: {{ $step }} }" x-effect="activeStep = $el.closest('[x-data]')?.__x?.$data?.currentStep ?? {{ $step }}">
|
||||
<div class="flex items-center justify-between">
|
||||
{{-- Step 1: Preparing --}}
|
||||
<div class="flex items-center flex-1">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center justify-center size-8 rounded-full border-2 transition-all duration-300"
|
||||
:class="{
|
||||
'bg-success border-success': currentStep > 1,
|
||||
'bg-warning/20 border-warning': currentStep === 1,
|
||||
'border-neutral-400 dark:border-coolgray-300': currentStep < 1
|
||||
}">
|
||||
<template x-if="currentStep > 1">
|
||||
<svg class="size-4 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="currentStep === 1">
|
||||
<svg class="size-4 text-warning animate-spin" 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>
|
||||
</template>
|
||||
<template x-if="currentStep < 1">
|
||||
<span class="text-xs font-medium text-neutral-500 dark:text-neutral-400">1</span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="mt-1.5 text-xs font-medium transition-colors duration-300"
|
||||
:class="{
|
||||
'text-success': currentStep > 1,
|
||||
'text-warning': currentStep === 1,
|
||||
'text-neutral-500 dark:text-neutral-400': currentStep < 1
|
||||
}">Preparing</span>
|
||||
</div>
|
||||
<div class="flex-1 h-0.5 mx-2 transition-all duration-300"
|
||||
:class="currentStep > 1 ? 'bg-success' : 'bg-neutral-300 dark:bg-coolgray-300'"></div>
|
||||
</div>
|
||||
|
||||
{{-- Step 2: Helper --}}
|
||||
<div class="flex items-center flex-1">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center justify-center size-8 rounded-full border-2 transition-all duration-300"
|
||||
:class="{
|
||||
'bg-success border-success': currentStep > 2,
|
||||
'bg-warning/20 border-warning': currentStep === 2,
|
||||
'border-neutral-400 dark:border-coolgray-300': currentStep < 2
|
||||
}">
|
||||
<template x-if="currentStep > 2">
|
||||
<svg class="size-4 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="currentStep === 2">
|
||||
<svg class="size-4 text-warning animate-spin" 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>
|
||||
</template>
|
||||
<template x-if="currentStep < 2">
|
||||
<span class="text-xs font-medium text-neutral-500 dark:text-neutral-400">2</span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="mt-1.5 text-xs font-medium transition-colors duration-300"
|
||||
:class="{
|
||||
'text-success': currentStep > 2,
|
||||
'text-warning': currentStep === 2,
|
||||
'text-neutral-500 dark:text-neutral-400': currentStep < 2
|
||||
}">Helper</span>
|
||||
</div>
|
||||
<div class="flex-1 h-0.5 mx-2 transition-all duration-300"
|
||||
:class="currentStep > 2 ? 'bg-success' : 'bg-neutral-300 dark:bg-coolgray-300'"></div>
|
||||
</div>
|
||||
|
||||
{{-- Step 3: Image --}}
|
||||
<div class="flex items-center flex-1">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center justify-center size-8 rounded-full border-2 transition-all duration-300"
|
||||
:class="{
|
||||
'bg-success border-success': currentStep > 3,
|
||||
'bg-warning/20 border-warning': currentStep === 3,
|
||||
'border-neutral-400 dark:border-coolgray-300': currentStep < 3
|
||||
}">
|
||||
<template x-if="currentStep > 3">
|
||||
<svg class="size-4 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="currentStep === 3">
|
||||
<svg class="size-4 text-warning animate-spin" 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>
|
||||
</template>
|
||||
<template x-if="currentStep < 3">
|
||||
<span class="text-xs font-medium text-neutral-500 dark:text-neutral-400">3</span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="mt-1.5 text-xs font-medium transition-colors duration-300"
|
||||
:class="{
|
||||
'text-success': currentStep > 3,
|
||||
'text-warning': currentStep === 3,
|
||||
'text-neutral-500 dark:text-neutral-400': currentStep < 3
|
||||
}">Image</span>
|
||||
</div>
|
||||
<div class="flex-1 h-0.5 mx-2 transition-all duration-300"
|
||||
:class="currentStep > 3 ? 'bg-success' : 'bg-neutral-300 dark:bg-coolgray-300'"></div>
|
||||
</div>
|
||||
|
||||
{{-- Step 4: Restart --}}
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center justify-center size-8 rounded-full border-2 transition-all duration-300"
|
||||
:class="{
|
||||
'bg-success border-success': currentStep > 4,
|
||||
'bg-warning/20 border-warning': currentStep === 4,
|
||||
'border-neutral-400 dark:border-coolgray-300': currentStep < 4
|
||||
}">
|
||||
<template x-if="currentStep > 4">
|
||||
<svg class="size-4 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="currentStep === 4">
|
||||
<svg class="size-4 text-warning animate-spin" 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>
|
||||
</template>
|
||||
<template x-if="currentStep < 4">
|
||||
<span class="text-xs font-medium text-neutral-500 dark:text-neutral-400">4</span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="mt-1.5 text-xs font-medium transition-colors duration-300"
|
||||
:class="{
|
||||
'text-success': currentStep > 4,
|
||||
'text-warning': currentStep === 4,
|
||||
'text-neutral-500 dark:text-neutral-400': currentStep < 4
|
||||
}">Restart</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,7 +6,7 @@
|
||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" />
|
||||
<div x-data="{
|
||||
fullscreen: false,
|
||||
fullscreen: @entangle('fullscreen'),
|
||||
alwaysScroll: {{ $isKeepAliveOn ? 'true' : 'false' }},
|
||||
intervalId: null,
|
||||
showTimestamps: true,
|
||||
@@ -34,6 +34,18 @@
|
||||
if (!this.searchQuery.trim()) return true;
|
||||
return text.toLowerCase().includes(this.searchQuery.toLowerCase());
|
||||
},
|
||||
hasActiveLogSelection() {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.isCollapsed || !selection.toString().trim()) {
|
||||
return false;
|
||||
}
|
||||
const logsContainer = document.getElementById('logs');
|
||||
if (!logsContainer) return false;
|
||||
|
||||
// Check if selection is within the logs container
|
||||
const range = selection.getRangeAt(0);
|
||||
return logsContainer.contains(range.commonAncestorContainer);
|
||||
},
|
||||
decodeHtml(text) {
|
||||
// Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS
|
||||
let decoded = text;
|
||||
@@ -50,6 +62,12 @@
|
||||
return decoded;
|
||||
},
|
||||
renderHighlightedLog(el, text) {
|
||||
// Skip re-render if user has text selected in logs (preserves copy ability)
|
||||
// But always render if the element is empty (initial render)
|
||||
if (el.textContent && this.hasActiveLogSelection()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decoded = this.decodeHtml(text);
|
||||
el.textContent = '';
|
||||
|
||||
@@ -129,6 +147,12 @@
|
||||
}
|
||||
},
|
||||
init() {
|
||||
// Prevent Livewire from morphing logs container when text is selected
|
||||
Livewire.hook('morph.updating', ({ el, component, toEl, skip }) => {
|
||||
if (el.id === 'logs' && this.hasActiveLogSelection()) {
|
||||
skip();
|
||||
}
|
||||
});
|
||||
// Re-render logs after Livewire updates
|
||||
document.addEventListener('livewire:navigated', () => {
|
||||
this.$nextTick(() => { this.renderTrigger++; });
|
||||
@@ -158,28 +182,28 @@
|
||||
}">
|
||||
<livewire:project.application.deployment-navbar
|
||||
:application_deployment_queue="$application_deployment_queue" />
|
||||
@if (data_get($application_deployment_queue, 'status') === 'in_progress')
|
||||
<div class="flex items-center gap-1 pt-2 ">Deployment is
|
||||
<div class="dark:text-warning">
|
||||
{{ Str::headline(data_get($this->application_deployment_queue, 'status')) }}.
|
||||
</div>
|
||||
<x-loading class="loading-ring" />
|
||||
</div>
|
||||
{{-- <div class="">Logs will be updated automatically.</div> --}}
|
||||
@else
|
||||
<div class="pt-2 ">Deployment is <span
|
||||
class="dark:text-warning">{{ Str::headline(data_get($application_deployment_queue, 'status')) }}</span>.
|
||||
</div>
|
||||
@endif
|
||||
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'relative'">
|
||||
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'mt-4 relative'">
|
||||
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
|
||||
class="flex flex-col w-full bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300"
|
||||
:class="fullscreen ? 'h-full' : 'mt-4 border border-dotted rounded-sm'">
|
||||
:class="fullscreen ? 'h-full' : 'border border-dotted rounded-sm'">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
|
||||
<span x-show="searchQuery.trim()" x-text="getMatchCount() + ' matches'"
|
||||
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
||||
<span x-show="!searchQuery.trim()"></span>
|
||||
<div class="flex items-center gap-3">
|
||||
@if (data_get($application_deployment_queue, 'status') === 'in_progress')
|
||||
<div class="flex items-center gap-1">
|
||||
<span>Deployment is</span>
|
||||
<span class="dark:text-warning">In Progress</span>
|
||||
<x-loading class="loading-ring loading-xs" />
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center gap-1">
|
||||
<span>Deployment is</span>
|
||||
<span class="dark:text-warning">{{ Str::headline(data_get($application_deployment_queue, 'status')) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
<span x-show="searchQuery.trim()" x-text="getMatchCount() + ' matches'"
|
||||
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative">
|
||||
<svg class="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
|
||||
@@ -267,7 +291,7 @@
|
||||
$searchableContent = $line['timestamp'] . ' ' . $lineContent;
|
||||
@endphp
|
||||
<div data-log-line data-log-content="{{ htmlspecialchars($searchableContent) }}"
|
||||
x-bind:class="{ 'hidden': !matchesSearch($el.dataset.logContent) }" @class([
|
||||
x-effect="renderTrigger; searchQuery; $el.classList.toggle('hidden', !matchesSearch($el.dataset.logContent))" @class([
|
||||
'mt-2' => isset($line['command']) && $line['command'],
|
||||
'flex gap-2',
|
||||
])>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<livewire:project.service.stack-form :service="$service" />
|
||||
<h3>Services</h3>
|
||||
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-1">
|
||||
@if($applications->isEmpty() && $databases->isEmpty())
|
||||
@if ($applications->isEmpty() && $databases->isEmpty())
|
||||
<div class="p-4 text-sm text-neutral-500">
|
||||
No services defined in this Docker Compose file.
|
||||
</div>
|
||||
@@ -76,7 +76,8 @@
|
||||
@if ($application->fqdn)
|
||||
<span class="flex gap-1 text-xs">{{ Str::limit($application->fqdn, 60) }}
|
||||
@can('update', $service)
|
||||
<x-modal-input title="Edit Domains" :closeOutside="false" minWidth="32rem" maxWidth="40rem">
|
||||
<x-modal-input title="Edit Domains" :closeOutside="false" minWidth="32rem"
|
||||
maxWidth="40rem">
|
||||
<x-slot:content>
|
||||
<span class="cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -100,7 +101,7 @@
|
||||
@endcan
|
||||
</span>
|
||||
@endif
|
||||
<div class="pt-2 text-xs">{{ formatContainerStatus($application->status) }}</div>
|
||||
<div class="pt-2 text-xs">{{ formatContainerStatus($application->status) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center px-4">
|
||||
<a class="mx-4 text-xs font-bold hover:underline"
|
||||
@@ -149,7 +150,7 @@
|
||||
@if ($database->description)
|
||||
<span class="text-xs">{{ Str::limit($database->description, 60) }}</span>
|
||||
@endif
|
||||
<div class="text-xs">{{ formatContainerStatus($database->status) }}</div>
|
||||
<div class="text-xs">{{ formatContainerStatus($database->status) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center px-4">
|
||||
@if ($database->isBackupSolutionAvailable() || $database->is_migrated)
|
||||
@@ -185,10 +186,6 @@
|
||||
<h2>Storages</h2>
|
||||
</div>
|
||||
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
|
||||
<div class="pb-4 dark:text-warning text-coollabs">If you would like to add a volume, you must add it to
|
||||
your compose file (<a class="underline"
|
||||
href="{{ route('project.service.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">General
|
||||
tab</a>).</div>
|
||||
@foreach ($applications as $application)
|
||||
<livewire:project.service.storage wire:key="application-{{ $application->id }}"
|
||||
:resource="$application" />
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
@endif
|
||||
<x-forms.textarea
|
||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||
helper="The content shown may be outdated. Click 'Load from server' to fetch the latest version."
|
||||
rows="20" id="content"
|
||||
readonly="{{ $fileStorage->is_based_on_git || $fileStorage->is_binary }}"></x-forms.textarea>
|
||||
@if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary)
|
||||
@@ -79,12 +80,19 @@
|
||||
@endif
|
||||
<x-forms.textarea
|
||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||
helper="The content shown may be outdated. Click 'Load from server' to fetch the latest version."
|
||||
rows="20" id="content" disabled></x-forms.textarea>
|
||||
@endcan
|
||||
@endif
|
||||
@else
|
||||
{{-- Read-only view --}}
|
||||
@if (!$fileStorage->is_directory)
|
||||
@can('view', $resource)
|
||||
<div class="flex gap-2">
|
||||
<x-forms.button type="button" wire:click="loadStorageOnServer">Load from
|
||||
server</x-forms.button>
|
||||
</div>
|
||||
@endcan
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
||||
@@ -93,6 +101,7 @@
|
||||
@endif
|
||||
<x-forms.textarea
|
||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||
helper="The content shown may be outdated. Click 'Load from server' to fetch the latest version."
|
||||
rows="20" id="content" disabled></x-forms.textarea>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@@ -275,15 +275,9 @@
|
||||
</div>
|
||||
<div>Persistent storage to preserve data between deployments.</div>
|
||||
</div>
|
||||
@if ($resource?->build_pack === 'dockercompose')
|
||||
<div class="dark:text-warning text-coollabs">Please modify storage layout in your Docker Compose
|
||||
file or reload the compose file to reread the storage layout.</div>
|
||||
@else
|
||||
@if ($resource->persistentStorages()->get()->count() === 0 && $fileStorage->count() == 0)
|
||||
<div>No storage found.</div>
|
||||
@endif
|
||||
@if ($resource->persistentStorages()->get()->count() === 0 && $fileStorage->count() == 0)
|
||||
<div>No storage found.</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$hasVolumes = $this->volumeCount > 0;
|
||||
$hasFiles = $this->fileCount > 0;
|
||||
@@ -370,7 +364,6 @@
|
||||
<h2>{{ Str::headline($resource->name) }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($resource->persistentStorages()->get()->count() === 0 && $fileStorage->count() == 0)
|
||||
<div>No storage found.</div>
|
||||
@endif
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
scrollDebounce: null,
|
||||
colorLogs: localStorage.getItem('coolify-color-logs') === 'true',
|
||||
searchQuery: '',
|
||||
renderTrigger: 0,
|
||||
containerName: '{{ $container ?? "logs" }}',
|
||||
makeFullscreen() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
@@ -17,6 +18,11 @@
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
},
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Escape' && this.fullscreen) {
|
||||
this.makeFullscreen();
|
||||
}
|
||||
},
|
||||
isScrolling: false,
|
||||
toggleScroll() {
|
||||
this.alwaysScroll = !this.alwaysScroll;
|
||||
@@ -80,6 +86,18 @@
|
||||
if (!this.searchQuery.trim()) return true;
|
||||
return line.toLowerCase().includes(this.searchQuery.toLowerCase());
|
||||
},
|
||||
hasActiveLogSelection() {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.isCollapsed || !selection.toString().trim()) {
|
||||
return false;
|
||||
}
|
||||
const logsContainer = document.getElementById('logs');
|
||||
if (!logsContainer) return false;
|
||||
|
||||
// Check if selection is within the logs container
|
||||
const range = selection.getRangeAt(0);
|
||||
return logsContainer.contains(range.commonAncestorContainer);
|
||||
},
|
||||
decodeHtml(text) {
|
||||
// Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS
|
||||
let decoded = text;
|
||||
@@ -96,6 +114,12 @@
|
||||
return decoded;
|
||||
},
|
||||
renderHighlightedLog(el, text) {
|
||||
// Skip re-render if user has text selected in logs (preserves copy ability)
|
||||
// But always render if the element is empty (initial render)
|
||||
if (el.textContent && this.hasActiveLogSelection()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decoded = this.decodeHtml(text);
|
||||
el.textContent = '';
|
||||
|
||||
@@ -167,8 +191,20 @@
|
||||
this.$wire.getLogs(true);
|
||||
this.logsLoaded = true;
|
||||
}
|
||||
// Prevent Livewire from morphing logs container when text is selected
|
||||
Livewire.hook('morph.updating', ({ el, component, toEl, skip }) => {
|
||||
if (el.id === 'logs' && this.hasActiveLogSelection()) {
|
||||
skip();
|
||||
}
|
||||
});
|
||||
// Re-render logs after Livewire updates
|
||||
Livewire.hook('commit', ({ succeed }) => {
|
||||
succeed(() => {
|
||||
this.$nextTick(() => { this.renderTrigger++; });
|
||||
});
|
||||
});
|
||||
}
|
||||
}">
|
||||
}" @keydown.window="handleKeyDown($event)">
|
||||
@if ($collapsible)
|
||||
<div class="flex gap-2 items-center p-4 cursor-pointer select-none hover:bg-gray-50 dark:hover:bg-coolgray-200"
|
||||
x-on:click="expanded = !expanded; if (expanded && !logsLoaded) { $wire.getLogs(true); logsLoaded = true; }">
|
||||
@@ -250,6 +286,23 @@
|
||||
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:click="toggleStreamLogs"
|
||||
title="{{ $streamLogs ? 'Stop Streaming' : 'Stream Logs' }}"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 {{ $streamLogs ? '!text-warning' : '' }}">
|
||||
@if ($streamLogs)
|
||||
{{-- Pause icon --}}
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor">
|
||||
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
|
||||
</svg>
|
||||
@else
|
||||
{{-- Play icon --}}
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor">
|
||||
<path d="M8 5v14l11-7L8 5z" />
|
||||
</svg>
|
||||
@endif
|
||||
</button>
|
||||
<button x-on:click="downloadLogs()" title="Download Logs"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
@@ -275,23 +328,6 @@
|
||||
d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:click="toggleStreamLogs"
|
||||
title="{{ $streamLogs ? 'Stop Streaming' : 'Stream Logs' }}"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 {{ $streamLogs ? '!text-warning' : '' }}">
|
||||
@if ($streamLogs)
|
||||
{{-- Pause icon --}}
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor">
|
||||
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
|
||||
</svg>
|
||||
@else
|
||||
{{-- Play icon --}}
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor">
|
||||
<path d="M8 5v14l11-7L8 5z" />
|
||||
</svg>
|
||||
@endif
|
||||
</button>
|
||||
<button title="Follow Logs" :class="alwaysScroll ? '!text-warning' : ''"
|
||||
x-on:click="toggleScroll"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
@@ -365,8 +401,8 @@
|
||||
|
||||
@endphp
|
||||
<div data-log-line data-log-content="{{ $line }}"
|
||||
x-effect="renderTrigger; searchQuery; $el.classList.toggle('hidden', !matchesSearch($el.dataset.logContent))"
|
||||
x-bind:class="{
|
||||
'hidden': !matchesSearch($el.dataset.logContent),
|
||||
'bg-red-500/10 dark:bg-red-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'error',
|
||||
'bg-yellow-500/10 dark:bg-yellow-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'warning',
|
||||
'bg-purple-500/10 dark:bg-purple-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'debug',
|
||||
@@ -377,7 +413,7 @@
|
||||
<span class="shrink-0 text-gray-500">{{ $timestamp }}</span>
|
||||
@endif
|
||||
<span data-line-text="{{ $logContent }}"
|
||||
x-effect="searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"
|
||||
x-effect="renderTrigger; searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"
|
||||
class="whitespace-pre-wrap break-all"></span>
|
||||
</div>
|
||||
@endforeach
|
||||
@@ -390,4 +426,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." id="frequency"
|
||||
label="Frequency" />
|
||||
<x-forms.input type="number" placeholder="300" id="timeout"
|
||||
helper="Maximum execution time in seconds (60-3600). Default is 300 seconds (5 minutes)."
|
||||
helper="Maximum execution time in seconds (60-36000). Default is 300 seconds (5 minutes)."
|
||||
label="Timeout (seconds)" />
|
||||
@if ($type === 'application')
|
||||
@if ($containerNames->count() > 1)
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" required />
|
||||
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" required />
|
||||
<x-forms.input type="number" placeholder="300" id="timeout"
|
||||
helper="Maximum execution time in seconds (60-3600)." label="Timeout (seconds)" required />
|
||||
helper="Maximum execution time in seconds (60-36000)." label="Timeout (seconds)" required />
|
||||
@if ($type === 'application')
|
||||
<x-forms.input placeholder="php"
|
||||
helper="You can leave this empty if your resource only has one container." id="container"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<div>
|
||||
<div class="flex flex-col gap-4">
|
||||
@if ($resource->type() === 'service' || data_get($resource, 'build_pack') === 'dockercompose')
|
||||
<div class="w-full p-2 text-sm rounded bg-warning/10 text-warning">
|
||||
Volume mounts are read-only. If you would like to add or modify a volume, you must edit your Docker
|
||||
Compose file and reload the compose file.
|
||||
</div>
|
||||
@endif
|
||||
@foreach ($resource->persistentStorages as $storage)
|
||||
@if ($resource->type() === 'service')
|
||||
<livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<div>
|
||||
<form wire:submit='submit' class="flex flex-col items-center gap-4 p-4 bg-white border lg:items-start dark:bg-base dark:border-coolgray-300 border-neutral-200">
|
||||
@if ($isReadOnly)
|
||||
<div class="w-full p-2 text-sm rounded bg-warning/10 text-warning">
|
||||
This volume is mounted as read-only and cannot be modified from the UI.
|
||||
</div>
|
||||
@if (!$storage->isServiceResource() && !$storage->isDockerComposeResource())
|
||||
<div class="w-full p-2 text-sm rounded bg-warning/10 text-warning">
|
||||
This volume is mounted as read-only and cannot be modified from the UI.
|
||||
</div>
|
||||
@endif
|
||||
@if ($isFirst)
|
||||
<div class="flex gap-2 items-end w-full md:flex-row flex-col">
|
||||
@if (
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Webhooks</h2>
|
||||
<x-helper
|
||||
helper="For more details goto our <a class='underline dark:text-white' href='https://coolify.io/docs/api/operations/deploy-by-tag-or-uuid' target='_blank'>docs</a>." />
|
||||
helper="For more details goto our <a class='underline dark:text-white' href='https://coolify.io/docs/api-reference/api/operations/deploy-by-tag-or-uuid' target='_blank'>docs</a>." />
|
||||
</div>
|
||||
<div>
|
||||
<x-forms.input readonly
|
||||
helper="See details in our <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/api/operations/deploy-by-tag-or-uuid'>documentation</a>."
|
||||
helper="See details in our <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/api-reference/api/operations/deploy-by-tag-or-uuid'>documentation</a>."
|
||||
label="Deploy Webhook (auth required)" id="deploywebhook"></x-forms.input>
|
||||
</div>
|
||||
@if ($resource->type() === 'application')
|
||||
|
||||
@@ -320,6 +320,64 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@if (!$server->hetzner_server_id && $availableHetznerTokens->isNotEmpty())
|
||||
<div class="pt-6">
|
||||
<h3>Link to Hetzner Cloud</h3>
|
||||
<p class="pb-4 text-sm dark:text-neutral-400">
|
||||
Link this server to a Hetzner Cloud instance to enable power controls and status monitoring.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-4 items-end">
|
||||
<div class="w-72">
|
||||
<x-forms.select wire:model="selectedHetznerTokenId" label="Hetzner Token"
|
||||
canGate="update" :canResource="$server">
|
||||
<option value="">Select a token...</option>
|
||||
@foreach ($availableHetznerTokens as $token)
|
||||
<option value="{{ $token->id }}">{{ $token->name }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
</div>
|
||||
<x-forms.button wire:click="searchHetznerServer"
|
||||
wire:loading.attr="disabled"
|
||||
canGate="update" :canResource="$server">
|
||||
<span wire:loading.remove wire:target="searchHetznerServer">Search by IP</span>
|
||||
<span wire:loading wire:target="searchHetznerServer">Searching...</span>
|
||||
</x-forms.button>
|
||||
</div>
|
||||
|
||||
@if ($hetznerSearchError)
|
||||
<div class="mt-4 p-4 border border-red-500 rounded-md bg-red-50 dark:bg-red-900/20">
|
||||
<p class="text-red-600 dark:text-red-400">{{ $hetznerSearchError }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($hetznerNoMatchFound)
|
||||
<div class="mt-4 p-4 border border-yellow-500 rounded-md bg-yellow-50 dark:bg-yellow-900/20">
|
||||
<p class="text-yellow-600 dark:text-yellow-400">
|
||||
No Hetzner server found matching IP: {{ $server->ip }}
|
||||
</p>
|
||||
<p class="text-sm dark:text-neutral-400 mt-1">
|
||||
Try a different token or verify the server IP is correct.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($matchedHetznerServer)
|
||||
<div class="mt-4 p-4 border border-green-500 rounded-md bg-green-50 dark:bg-green-900/20">
|
||||
<h4 class="font-semibold text-green-700 dark:text-green-400 mb-2">Match Found!</h4>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm mb-4">
|
||||
<div><span class="font-medium">Name:</span> {{ $matchedHetznerServer['name'] }}</div>
|
||||
<div><span class="font-medium">ID:</span> {{ $matchedHetznerServer['id'] }}</div>
|
||||
<div><span class="font-medium">Status:</span> {{ ucfirst($matchedHetznerServer['status']) }}</div>
|
||||
<div><span class="font-medium">Type:</span> {{ data_get($matchedHetznerServer, 'server_type.name', 'Unknown') }}</div>
|
||||
</div>
|
||||
<x-forms.button wire:click="linkToHetzner" isHighlighted canGate="update" :canResource="$server">
|
||||
Link This Server
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@if ($server->isFunctional() && !$server->isSwarm() && !$server->isBuildServer())
|
||||
<form wire:submit.prevent='submit'>
|
||||
<div class="flex gap-2 items-center pt-4 pb-2">
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<div @if ($isUpgradeAvailable) title="New version available" @else title="No upgrade available" @endif
|
||||
x-init="$wire.checkUpdate" x-data="upgradeModal">
|
||||
x-init="$wire.checkUpdate" x-data="upgradeModal({
|
||||
currentVersion: @js($currentVersion),
|
||||
latestVersion: @js($latestVersion)
|
||||
})">
|
||||
@if ($isUpgradeAvailable)
|
||||
<div :class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
|
||||
<button class="menu-item" @click="modalOpen=true" x-show="showProgress">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300 lds-heart" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
|
||||
</svg>
|
||||
In progress
|
||||
</button>
|
||||
<button class="menu-item cursor-pointer" @click="modalOpen=true" x-show="!showProgress">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
@@ -28,10 +29,9 @@
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-100" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
@@ -40,44 +40,131 @@
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-fit bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between pb-3">
|
||||
<h3 class="text-lg font-semibold">Upgrade confirmation</h3>
|
||||
<button x-show="!showProgress" @click="modalOpen=false"
|
||||
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold"
|
||||
x-text="upgradeComplete ? 'Upgrade Complete!' : (showProgress ? 'Upgrading...' : 'Upgrade Available')">
|
||||
</h3>
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{{ $currentVersion }} <span class="mx-1">→</span> {{ $latestVersion }}
|
||||
</div>
|
||||
</div>
|
||||
<button x-show="!showProgress || upgradeError" @click="upgradeError ? closeErrorModal() : modalOpen=false"
|
||||
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50 dark:text-neutral-400 dark:hover:text-white dark:hover:bg-coolgray-300">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative w-auto pb-8">
|
||||
<p>Are you sure you would like to upgrade your instance to {{ $latestVersion }}?</p>
|
||||
<br />
|
||||
|
||||
<x-callout type="warning" title="Caution">
|
||||
<p>Any deployments running during the update process will
|
||||
fail. Please ensure no deployments are in progress on any server before continuing.
|
||||
</p>
|
||||
</x-callout>
|
||||
<br />
|
||||
<p>You can review the changelogs <a class="font-bold underline dark:text-white"
|
||||
href="https://github.com/coollabsio/coolify/releases" target="_blank">here</a>.</p>
|
||||
<br />
|
||||
<p>If something goes wrong and you cannot upgrade your instance, You can check the following
|
||||
<a class="font-bold underline dark:text-white" href="https://coolify.io/docs/upgrade"
|
||||
target="_blank">guide</a> on what to do.
|
||||
</p>
|
||||
<div class="flex flex-col pt-4" x-show="showProgress">
|
||||
<h2>Progress <x-loading /></h2>
|
||||
<div x-html="currentStatus"></div>
|
||||
</div>
|
||||
{{-- Content --}}
|
||||
<div class="relative w-auto pb-6">
|
||||
{{-- Progress View --}}
|
||||
<template x-if="showProgress">
|
||||
<div class="space-y-6">
|
||||
{{-- Step Progress Indicator --}}
|
||||
<div class="pt-2">
|
||||
<x-upgrade-progress />
|
||||
</div>
|
||||
|
||||
{{-- Elapsed Time --}}
|
||||
<div class="text-center">
|
||||
<span class="text-sm text-neutral-500 dark:text-neutral-400">Elapsed time:</span>
|
||||
<span class="ml-2 font-mono text-sm" x-text="formatElapsedTime()"></span>
|
||||
</div>
|
||||
|
||||
{{-- Current Status Message --}}
|
||||
<div class="p-4 rounded-lg bg-neutral-200 dark:bg-coolgray-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<template x-if="!upgradeComplete && !upgradeError">
|
||||
<svg class="w-5 h-5 text-warning animate-spin"
|
||||
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>
|
||||
</template>
|
||||
<template x-if="upgradeComplete">
|
||||
<svg class="w-5 h-5 text-success" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="upgradeError">
|
||||
<svg class="w-5 h-5 text-error" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
<span x-text="currentStatus" class="text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Success State with Countdown --}}
|
||||
<template x-if="upgradeComplete">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
Reloading in <span x-text="successCountdown"
|
||||
class="font-bold text-warning"></span> seconds...
|
||||
</p>
|
||||
<x-forms.button @click="reloadNow()" type="button">
|
||||
Reload Now
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{{-- Error State with Close Button --}}
|
||||
<template x-if="upgradeError">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Check the logs on the server at /data/coolify/source/upgrade*.
|
||||
</p>
|
||||
<x-forms.button @click="closeErrorModal()" type="button">
|
||||
Close
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{{-- Confirmation View --}}
|
||||
<template x-if="!showProgress">
|
||||
<div class="space-y-4">
|
||||
{{-- Warning --}}
|
||||
<x-callout type="warning" title="Caution">
|
||||
<p>Any deployments running during the update process will
|
||||
fail.
|
||||
</p>
|
||||
</x-callout>
|
||||
|
||||
{{-- Help Links --}}
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
If something goes wrong, check the
|
||||
<a class="font-medium underline dark:text-white hover:text-neutral-800 dark:hover:text-neutral-300"
|
||||
href="https://coolify.io/docs/upgrade" target="_blank">upgrade guide</a> or the
|
||||
logs on the server at /data/coolify/source/upgrade*.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Footer Actions --}}
|
||||
<div class="flex gap-4" x-show="!showProgress">
|
||||
<x-forms.button @click="modalOpen=false"
|
||||
class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel
|
||||
</x-forms.button>
|
||||
<div class="flex-1"></div>
|
||||
<x-forms.button @click="confirmed" class="w-24" isHighlighted type="button">Continue
|
||||
<x-forms.button @click="confirmed" class="w-32" isHighlighted type="button">
|
||||
Upgrade Now
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,23 +176,64 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('upgradeModal', () => ({
|
||||
Alpine.data('upgradeModal', (config) => ({
|
||||
modalOpen: false,
|
||||
showProgress: false,
|
||||
currentStatus: '',
|
||||
checkHealthInterval: null,
|
||||
checkIfIamDeadInterval: null,
|
||||
checkUpgradeStatusInterval: null,
|
||||
elapsedInterval: null,
|
||||
healthCheckAttempts: 0,
|
||||
startTime: null,
|
||||
elapsedTime: 0,
|
||||
currentStep: 0,
|
||||
upgradeComplete: false,
|
||||
upgradeError: false,
|
||||
successCountdown: 3,
|
||||
currentVersion: config.currentVersion || '',
|
||||
latestVersion: config.latestVersion || '',
|
||||
serviceDown: false,
|
||||
|
||||
confirmed() {
|
||||
this.showProgress = true;
|
||||
this.$wire.$call('upgrade')
|
||||
this.currentStep = 1;
|
||||
this.currentStatus = 'Starting upgrade...';
|
||||
this.startTimer();
|
||||
// Trigger server-side upgrade script via Livewire
|
||||
this.$wire.$call('upgrade');
|
||||
// Start client-side status polling
|
||||
this.upgrade();
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
// Prevent accidental navigation during upgrade
|
||||
this.beforeUnloadHandler = (event) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
});
|
||||
};
|
||||
window.addEventListener('beforeunload', this.beforeUnloadHandler);
|
||||
},
|
||||
|
||||
startTimer() {
|
||||
this.startTime = Date.now();
|
||||
this.elapsedInterval = setInterval(() => {
|
||||
this.elapsedTime = Math.floor((Date.now() - this.startTime) / 1000);
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
formatElapsedTime() {
|
||||
const minutes = Math.floor(this.elapsedTime / 60);
|
||||
const seconds = this.elapsedTime % 60;
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
mapStepToUI(apiStep) {
|
||||
// Map backend steps (1-6) to UI steps (1-4)
|
||||
// Backend: 1=config, 2=env, 3=pull, 4=stop, 5=start, 6=complete
|
||||
// UI: 1=prepare, 2=pull images, 3=pull coolify, 4=restart
|
||||
if (apiStep <= 2) return 1;
|
||||
if (apiStep === 3) return 2;
|
||||
if (apiStep <= 5) return 3;
|
||||
return 4;
|
||||
},
|
||||
|
||||
getReviveStatusMessage(elapsedMinutes, attempts) {
|
||||
if (elapsedMinutes === 0) {
|
||||
return `Waiting for Coolify to come back online... (attempt ${attempts})`;
|
||||
@@ -119,68 +247,133 @@
|
||||
return `Still updating. If this takes longer than 15 minutes, please check server logs... (${elapsedMinutes} minutes elapsed)`;
|
||||
}
|
||||
},
|
||||
|
||||
revive() {
|
||||
if (this.checkHealthInterval) return true;
|
||||
this.healthCheckAttempts = 0;
|
||||
this.startTime = Date.now();
|
||||
console.log('Checking server\'s health...')
|
||||
this.currentStep = 4;
|
||||
console.log('Checking server\'s health...');
|
||||
this.checkHealthInterval = setInterval(() => {
|
||||
this.healthCheckAttempts++;
|
||||
const elapsedMinutes = Math.floor((Date.now() - this.startTime) / 60000);
|
||||
fetch('/api/health')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
this.currentStatus =
|
||||
'Coolify is back online. Reloading this page in 5 seconds...';
|
||||
if (this.checkHealthInterval) {
|
||||
clearInterval(this.checkHealthInterval);
|
||||
this.checkHealthInterval = null;
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000)
|
||||
this.showSuccess();
|
||||
} else {
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this
|
||||
.healthCheckAttempts);
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this
|
||||
.healthCheckAttempts);
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||
});
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
showSuccess() {
|
||||
if (this.checkHealthInterval) {
|
||||
clearInterval(this.checkHealthInterval);
|
||||
this.checkHealthInterval = null;
|
||||
}
|
||||
if (this.checkUpgradeStatusInterval) {
|
||||
clearInterval(this.checkUpgradeStatusInterval);
|
||||
this.checkUpgradeStatusInterval = null;
|
||||
}
|
||||
if (this.elapsedInterval) {
|
||||
clearInterval(this.elapsedInterval);
|
||||
this.elapsedInterval = null;
|
||||
}
|
||||
// Remove beforeunload handler now that upgrade is complete
|
||||
if (this.beforeUnloadHandler) {
|
||||
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
|
||||
this.beforeUnloadHandler = null;
|
||||
}
|
||||
|
||||
this.upgradeComplete = true;
|
||||
this.currentStep = 5;
|
||||
this.currentStatus = `Successfully upgraded to ${this.latestVersion}`;
|
||||
this.successCountdown = 3;
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
this.successCountdown--;
|
||||
if (this.successCountdown <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
window.location.reload();
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
reloadNow() {
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
// Stop all intervals
|
||||
if (this.checkHealthInterval) {
|
||||
clearInterval(this.checkHealthInterval);
|
||||
this.checkHealthInterval = null;
|
||||
}
|
||||
if (this.checkUpgradeStatusInterval) {
|
||||
clearInterval(this.checkUpgradeStatusInterval);
|
||||
this.checkUpgradeStatusInterval = null;
|
||||
}
|
||||
if (this.elapsedInterval) {
|
||||
clearInterval(this.elapsedInterval);
|
||||
this.elapsedInterval = null;
|
||||
}
|
||||
// Remove beforeunload handler so user can close modal
|
||||
if (this.beforeUnloadHandler) {
|
||||
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
|
||||
this.beforeUnloadHandler = null;
|
||||
}
|
||||
|
||||
this.upgradeError = true;
|
||||
this.currentStatus = `Error: ${message}`;
|
||||
},
|
||||
|
||||
closeErrorModal() {
|
||||
this.modalOpen = false;
|
||||
this.showProgress = false;
|
||||
this.upgradeError = false;
|
||||
this.currentStatus = '';
|
||||
this.currentStep = 0;
|
||||
},
|
||||
|
||||
upgrade() {
|
||||
if (this.checkIfIamDeadInterval || this.showProgress) return true;
|
||||
this.currentStatus = 'Update in progress. Pulling new images and preparing to restart Coolify...';
|
||||
this.checkIfIamDeadInterval = setInterval(() => {
|
||||
fetch('/api/health')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
this.currentStatus =
|
||||
"Update in progress. Pulling new images and preparing to restart Coolify..."
|
||||
} else {
|
||||
this.currentStatus = "Coolify is restarting with the new version..."
|
||||
if (this.checkIfIamDeadInterval) {
|
||||
clearInterval(this.checkIfIamDeadInterval);
|
||||
this.checkIfIamDeadInterval = null;
|
||||
}
|
||||
this.revive();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.currentStatus = "Coolify is restarting with the new version..."
|
||||
if (this.checkIfIamDeadInterval) {
|
||||
clearInterval(this.checkIfIamDeadInterval);
|
||||
this.checkIfIamDeadInterval = null;
|
||||
if (this.checkUpgradeStatusInterval) return true;
|
||||
this.currentStep = 1;
|
||||
this.currentStatus = 'Starting upgrade...';
|
||||
this.serviceDown = false;
|
||||
|
||||
// Poll upgrade status via Livewire
|
||||
this.checkUpgradeStatusInterval = setInterval(async () => {
|
||||
try {
|
||||
const data = await this.$wire.getUpgradeStatus();
|
||||
if (data.status === 'in_progress') {
|
||||
this.currentStep = this.mapStepToUI(data.step);
|
||||
this.currentStatus = data.message;
|
||||
} else if (data.status === 'complete') {
|
||||
this.showSuccess();
|
||||
} else if (data.status === 'error') {
|
||||
this.showError(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
// Service is down - switch to health check mode
|
||||
console.log('Livewire unavailable, switching to health check mode');
|
||||
if (!this.serviceDown) {
|
||||
this.serviceDown = true;
|
||||
this.currentStep = 4;
|
||||
this.currentStatus = 'Coolify is restarting with the new version...';
|
||||
if (this.checkUpgradeStatusInterval) {
|
||||
clearInterval(this.checkUpgradeStatusInterval);
|
||||
this.checkUpgradeStatusInterval = null;
|
||||
}
|
||||
this.revive();
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
+222
-12
@@ -7,27 +7,101 @@ LATEST_HELPER_VERSION=${2:-latest}
|
||||
REGISTRY_URL=${3:-ghcr.io}
|
||||
SKIP_BACKUP=${4:-false}
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
STATUS_FILE="/data/coolify/source/.upgrade-status"
|
||||
|
||||
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
||||
LOGFILE="/data/coolify/source/upgrade-${DATE}.log"
|
||||
|
||||
# Helper function to log with timestamp
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE"
|
||||
}
|
||||
|
||||
# Helper function to log section headers
|
||||
log_section() {
|
||||
echo "" >>"$LOGFILE"
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE"
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
}
|
||||
|
||||
# Helper function to write upgrade status for API polling
|
||||
write_status() {
|
||||
local step="$1"
|
||||
local message="$2"
|
||||
echo "${step}|${message}|$(date -Iseconds)" > "$STATUS_FILE"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Coolify Upgrade - ${DATE}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Initialize log file with header
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
echo "Coolify Upgrade Log" >>"$LOGFILE"
|
||||
echo "Started: $(date '+%Y-%m-%d %H:%M:%S')" >>"$LOGFILE"
|
||||
echo "Target Version: ${LATEST_IMAGE}" >>"$LOGFILE"
|
||||
echo "Helper Version: ${LATEST_HELPER_VERSION}" >>"$LOGFILE"
|
||||
echo "Registry URL: ${REGISTRY_URL}" >>"$LOGFILE"
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
|
||||
log_section "Step 1/6: Downloading configuration files"
|
||||
write_status "1" "Downloading configuration files"
|
||||
echo "1/6 Downloading latest configuration files..."
|
||||
log "Downloading docker-compose.yml from ${CDN}/docker-compose.yml"
|
||||
curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
||||
log "Downloading docker-compose.prod.yml from ${CDN}/docker-compose.prod.yml"
|
||||
curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
log "Downloading .env.production from ${CDN}/.env.production"
|
||||
curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
log "Configuration files downloaded successfully"
|
||||
echo " Done."
|
||||
|
||||
# Extract all images from docker-compose configuration
|
||||
log "Extracting all images from docker-compose configuration..."
|
||||
COMPOSE_FILES="-f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml"
|
||||
|
||||
# Check if custom compose file exists
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
COMPOSE_FILES="$COMPOSE_FILES -f /data/coolify/source/docker-compose.custom.yml"
|
||||
log "Including custom docker-compose.yml in image extraction"
|
||||
fi
|
||||
|
||||
# Get all unique images from docker compose config
|
||||
# LATEST_IMAGE env var is needed for image substitution in compose files
|
||||
IMAGES=$(LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u)
|
||||
|
||||
if [ -z "$IMAGES" ]; then
|
||||
log "ERROR: Failed to extract images from docker-compose files"
|
||||
write_status "error" "Failed to parse docker-compose configuration"
|
||||
echo " ERROR: Failed to parse docker-compose configuration. Aborting upgrade."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Images to pull:"
|
||||
echo "$IMAGES" | while read img; do log " - $img"; done
|
||||
|
||||
# Backup existing .env file before making any changes
|
||||
if [ "$SKIP_BACKUP" != "true" ]; then
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "Creating backup of existing .env file to .env-$DATE" >>"$LOGFILE"
|
||||
echo " Creating backup of .env file..."
|
||||
log "Creating backup of .env file to .env-$DATE"
|
||||
cp "$ENV_FILE" "$ENV_FILE-$DATE"
|
||||
log "Backup created: ${ENV_FILE}-${DATE}"
|
||||
else
|
||||
echo "No existing .env file found to backup" >>"$LOGFILE"
|
||||
log "WARNING: No existing .env file found to backup"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Merging .env.production values into .env" >>"$LOGFILE"
|
||||
log_section "Step 2/6: Updating environment configuration"
|
||||
write_status "2" "Updating environment configuration"
|
||||
echo ""
|
||||
echo "2/6 Updating environment configuration..."
|
||||
log "Merging .env.production values into .env"
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE" /data/coolify/source/.env.production > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
echo ".env file merged successfully" >>"$LOGFILE"
|
||||
log "Environment file merged successfully"
|
||||
|
||||
update_env_var() {
|
||||
local key="$1"
|
||||
@@ -36,37 +110,173 @@ update_env_var() {
|
||||
# If variable "key=" exists but has no value, update the value of the existing line
|
||||
if grep -q "^${key}=$" "$ENV_FILE"; then
|
||||
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
|
||||
echo " - Updated value of ${key} as the current value was empty" >>"$LOGFILE"
|
||||
log "Updated ${key} (was empty)"
|
||||
# If variable "key=" doesn't exist, append it to the file with value
|
||||
elif ! grep -q "^${key}=" "$ENV_FILE"; then
|
||||
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
|
||||
echo " - Added ${key} with default value as the variable was missing" >>"$LOGFILE"
|
||||
log "Added ${key} (was missing)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Checking and updating environment variables if necessary..." >>"$LOGFILE"
|
||||
log "Checking environment variables..."
|
||||
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||
log "Environment variables check complete"
|
||||
echo " Done."
|
||||
|
||||
# Make sure coolify network exists
|
||||
# It is created when starting Coolify with docker compose
|
||||
log "Checking Docker network 'coolify'..."
|
||||
if ! docker network inspect coolify >/dev/null 2>&1; then
|
||||
log "Network 'coolify' does not exist, creating..."
|
||||
if ! docker network create --attachable --ipv6 coolify 2>/dev/null; then
|
||||
echo "Failed to create coolify network with ipv6. Trying without ipv6..."
|
||||
log "Failed to create network with IPv6, trying without IPv6..."
|
||||
docker network create --attachable coolify 2>/dev/null
|
||||
log "Network 'coolify' created without IPv6"
|
||||
else
|
||||
log "Network 'coolify' created with IPv6 support"
|
||||
fi
|
||||
else
|
||||
log "Network 'coolify' already exists"
|
||||
fi
|
||||
|
||||
# Check if Docker config file exists
|
||||
DOCKER_CONFIG_MOUNT=""
|
||||
if [ -f /root/.docker/config.json ]; then
|
||||
DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json"
|
||||
log "Docker config mount enabled: /root/.docker/config.json"
|
||||
fi
|
||||
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
echo "docker-compose.custom.yml detected." >>"$LOGFILE"
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
log_section "Step 3/6: Pulling Docker images"
|
||||
write_status "3" "Pulling Docker images"
|
||||
echo ""
|
||||
echo "3/6 Pulling Docker images..."
|
||||
echo " This may take a few minutes depending on your connection."
|
||||
|
||||
# Also pull the helper image (not in compose files but needed for upgrade)
|
||||
HELPER_IMAGE="${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}"
|
||||
echo " - Pulling $HELPER_IMAGE..."
|
||||
log "Pulling image: $HELPER_IMAGE"
|
||||
if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then
|
||||
log "Successfully pulled $HELPER_IMAGE"
|
||||
else
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
log "ERROR: Failed to pull $HELPER_IMAGE"
|
||||
write_status "error" "Failed to pull $HELPER_IMAGE"
|
||||
echo " ERROR: Failed to pull $HELPER_IMAGE. Aborting upgrade."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pull all images from compose config
|
||||
# Using a for loop to avoid subshell issues with exit
|
||||
for IMAGE in $IMAGES; do
|
||||
if [ -n "$IMAGE" ]; then
|
||||
echo " - Pulling $IMAGE..."
|
||||
log "Pulling image: $IMAGE"
|
||||
if docker pull "$IMAGE" >>"$LOGFILE" 2>&1; then
|
||||
log "Successfully pulled $IMAGE"
|
||||
else
|
||||
log "ERROR: Failed to pull $IMAGE"
|
||||
write_status "error" "Failed to pull $IMAGE"
|
||||
echo " ERROR: Failed to pull $IMAGE. Aborting upgrade."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log "All images pulled successfully"
|
||||
echo " All images pulled successfully."
|
||||
|
||||
log_section "Step 4/6: Stopping and restarting containers"
|
||||
write_status "4" "Stopping containers"
|
||||
echo ""
|
||||
echo "4/6 Stopping containers and starting new ones..."
|
||||
echo " This step will restart all Coolify containers."
|
||||
echo " Check the log file for details: ${LOGFILE}"
|
||||
|
||||
# From this point forward, we need to ensure the script continues even if
|
||||
# the SSH connection is lost (which happens when coolify container stops)
|
||||
# We use a subshell with nohup to ensure completion
|
||||
log "Starting container restart sequence (detached)..."
|
||||
|
||||
nohup bash -c "
|
||||
LOGFILE='$LOGFILE'
|
||||
STATUS_FILE='$STATUS_FILE'
|
||||
DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT'
|
||||
REGISTRY_URL='$REGISTRY_URL'
|
||||
LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION'
|
||||
LATEST_IMAGE='$LATEST_IMAGE'
|
||||
|
||||
log() {
|
||||
echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] \$1\" >>\"\$LOGFILE\"
|
||||
}
|
||||
|
||||
write_status() {
|
||||
echo \"\$1|\$2|\$(date -Iseconds)\" > \"\$STATUS_FILE\"
|
||||
}
|
||||
|
||||
# Stop and remove containers
|
||||
for container in coolify coolify-db coolify-redis coolify-realtime; do
|
||||
if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then
|
||||
log \"Stopping container: \${container}\"
|
||||
docker stop \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
|
||||
log \"Removing container: \${container}\"
|
||||
docker rm \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
|
||||
log \"Container \${container} stopped and removed\"
|
||||
else
|
||||
log \"Container \${container} not found (skipping)\"
|
||||
fi
|
||||
done
|
||||
log \"Container cleanup complete\"
|
||||
|
||||
# Start new containers
|
||||
echo '' >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
log 'Step 5/6: Starting new containers'
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
write_status '5' 'Starting new containers'
|
||||
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
log 'Using custom docker-compose.yml'
|
||||
log 'Running docker compose up with custom configuration...'
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
else
|
||||
log 'Using standard docker-compose configuration'
|
||||
log 'Running docker compose up...'
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
fi
|
||||
log 'Docker compose up completed'
|
||||
|
||||
# Final log entry
|
||||
echo '' >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
log 'Step 6/6: Upgrade complete'
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
write_status '6' 'Upgrade complete'
|
||||
log 'Coolify upgrade completed successfully'
|
||||
log \"Version: \${LATEST_IMAGE}\"
|
||||
echo '' >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
echo \"Upgrade completed: \$(date '+%Y-%m-%d %H:%M:%S')\" >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
|
||||
# Clean up status file after a short delay to allow frontend to read completion
|
||||
sleep 10
|
||||
rm -f \"\$STATUS_FILE\"
|
||||
log 'Status file cleaned up'
|
||||
" >>"$LOGFILE" 2>&1 &
|
||||
|
||||
# Give the background process a moment to start
|
||||
sleep 2
|
||||
log "Container restart sequence started in background (PID: $!)"
|
||||
echo ""
|
||||
echo "5/6 Containers are being restarted in the background..."
|
||||
echo "6/6 Upgrade process initiated!"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Coolify upgrade to ${LATEST_IMAGE} in progress"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo " The upgrade will continue in the background."
|
||||
echo " Coolify will be available again shortly."
|
||||
echo " Log file: ${LOGFILE}"
|
||||
@@ -0,0 +1,18 @@
|
||||
# documentation: https://www.beszel.dev/guide/agent-installation
|
||||
# slogan: Monitoring agent for Beszel
|
||||
# category: monitoring
|
||||
# tags: beszel,monitoring,server,stats,alerts
|
||||
# logo: svgs/beszel.svg
|
||||
|
||||
services:
|
||||
beszel-agent:
|
||||
image: 'henrygd/beszel-agent:0.16.1' # Released on 14 Nov 2025
|
||||
environment:
|
||||
- LISTEN=/beszel_socket/beszel.sock
|
||||
- HUB_URL=${HUB_URL?}
|
||||
- 'TOKEN=${TOKEN?}'
|
||||
- 'KEY=${KEY?}'
|
||||
volumes:
|
||||
- beszel_agent_data:/var/lib/beszel-agent
|
||||
- beszel_socket:/beszel_socket
|
||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
||||
@@ -9,21 +9,21 @@
|
||||
# Add the public Key in "Key" env variable and token in the "Token" variable below (These are obtained from Beszel UI)
|
||||
services:
|
||||
beszel:
|
||||
image: 'henrygd/beszel:0.15.2' # Released on October 30 2025
|
||||
image: 'henrygd/beszel:0.16.1' # Released on 14 Nov 2025
|
||||
environment:
|
||||
- SERVICE_URL_BESZEL_8090
|
||||
volumes:
|
||||
- 'beszel_data:/beszel_data'
|
||||
- 'beszel_socket:/beszel_socket'
|
||||
beszel-agent:
|
||||
image: 'henrygd/beszel-agent:0.15.2' # Released on October 30 2025
|
||||
volumes:
|
||||
- beszel_agent_data:/var/lib/beszel-agent
|
||||
- beszel_socket:/beszel_socket
|
||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
||||
image: 'henrygd/beszel-agent:0.16.1' # Released on 14 Nov 2025
|
||||
environment:
|
||||
- LISTEN=/beszel_socket/beszel.sock
|
||||
- HUB_URL=http://beszel:8090
|
||||
- 'TOKEN=${TOKEN}'
|
||||
- 'KEY=${KEY}'
|
||||
volumes:
|
||||
- beszel_agent_data:/var/lib/beszel-agent
|
||||
- beszel_socket:/beszel_socket
|
||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
||||
|
||||
@@ -32,6 +32,7 @@ services:
|
||||
- ANTHROPIC_API_BASE=${ANTHROPIC_API_BASE}
|
||||
- VOYAGE_API_KEY=${VOYAGE_API_KEY}
|
||||
- VOYAGE_API_BASE=${VOYAGE_API_BASE}
|
||||
- STORE_MODEL_IN_DB=${STORE_MODEL_IN_DB}
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./litellm-config.yaml
|
||||
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
is_directory: true
|
||||
environment:
|
||||
- SERVICE_URL_PAPERLESS_8000
|
||||
- PAPERLESS_URL=$SERVICE_URL_PAPERLESS_8000
|
||||
- PAPERLESS_URL=$SERVICE_URL_PAPERLESS
|
||||
- PAPERLESS_ADMIN_PASSWORD=${SERVICE_PASSWORD_PAPERLESS}
|
||||
- PAPERLESS_ADMIN_USER=${SERVICE_USER_PAPERLESS}
|
||||
- PAPERLESS_REDIS=redis://redis:6379
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# documentation: https://help.penpot.app/technical-guide/getting-started/#install-with-docker
|
||||
# slogan: Penpot is the first Open Source design and prototyping platform for product teams.
|
||||
# category: productivity
|
||||
# tags: penpot,design,prototyping,figma,open,source
|
||||
# logo: svgs/penpot.svg
|
||||
# port: 8080
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: penpotapp/frontend:2.11.1 # Release on 13 Nov 2025
|
||||
depends_on:
|
||||
penpot-backend:
|
||||
condition: service_healthy
|
||||
penpot-exporter:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- SERVICE_URL_FRONTEND_8080
|
||||
- 'PENPOT_FLAGS=${PENPOT_BACKEND_FLAGS:-enable-login-with-password}'
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:8080"]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 15
|
||||
|
||||
penpot-backend:
|
||||
image: penpotapp/backend:2.11.1 # Release on 13 Nov 2025
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
penpot-valkey:
|
||||
condition: service_healthy
|
||||
minio-init:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
- 'PENPOT_FLAGS=${PENPOT_BACKEND_FLAGS:-enable-login-with-password}'
|
||||
|
||||
# Backend & URIs
|
||||
- PENPOT_HTTP_SERVER_PORT=6060
|
||||
- PENPOT_SECRET_KEY=${SERVICE_REALBASE64_64_PENPOT}
|
||||
- PENPOT_PUBLIC_URI=${SERVICE_URL_FRONTEND_8080}
|
||||
- PENPOT_BACKEND_URI=http://penpot-backend
|
||||
- PENPOT_EXPORTER_URI=http://penpot-exporter
|
||||
|
||||
# Database
|
||||
- PENPOT_DATABASE_URI=postgresql://postgres/${POSTGRES_DB:-penpot}
|
||||
- PENPOT_DATABASE_USERNAME=${SERVICE_USER_POSTGRES}
|
||||
- PENPOT_DATABASE_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
|
||||
|
||||
# Valkey
|
||||
- 'PENPOT_REDIS_URI=redis://penpot-valkey/0'
|
||||
|
||||
# Telemetry
|
||||
- PENPOT_TELEMETRY_ENABLED=${PENPOT_TELEMETRY_ENABLED:-false}
|
||||
|
||||
# S3 Storage Configuration (MinIO)
|
||||
- PENPOT_OBJECTS_STORAGE_BACKEND=s3
|
||||
- PENPOT_OBJECTS_STORAGE_S3_REGION=us-east-1
|
||||
- PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
- PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
||||
|
||||
# S3 Credentials (MinIO)
|
||||
- AWS_ACCESS_KEY_ID=${SERVICE_USER_MINIO}
|
||||
- AWS_SECRET_ACCESS_KEY=${SERVICE_PASSWORD_MINIO}
|
||||
|
||||
# Email Configuration
|
||||
- PENPOT_SMTP_DEFAULT_FROM=${PENPOT_SMTP_DEFAULT_FROM:-no-reply@example.com}
|
||||
- PENPOT_SMTP_DEFAULT_REPLY_TO=${PENPOT_SMTP_DEFAULT_REPLY_TO:-no-reply@example.com}
|
||||
- PENPOT_SMTP_HOST=${PENPOT_SMTP_HOST:-mailpit}
|
||||
- PENPOT_SMTP_PORT=${PENPOT_SMTP_PORT:-1025}
|
||||
- PENPOT_SMTP_USERNAME=${PENPOT_SMTP_USERNAME:-penpot}
|
||||
- PENPOT_SMTP_PASSWORD=${PENPOT_SMTP_PASSWORD:-penpot}
|
||||
- PENPOT_SMTP_TLS=${PENPOT_SMTP_TLS:-false}
|
||||
- PENPOT_SMTP_SSL=${PENPOT_SMTP_SSL:-false}
|
||||
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"require('http').get({host:'127.0.0.1', port:6060, path:'/readyz'}, res => process.exit(res.statusCode===200 ? 0 : 1)).on('error', () => process.exit(1));"
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
retries: 15
|
||||
|
||||
penpot-exporter:
|
||||
image: penpotapp/exporter:2.11.1 # Release on 13 Nov 2025
|
||||
environment:
|
||||
- PENPOT_PUBLIC_URI=${SERVICE_URL_FRONTEND_8080}
|
||||
- 'PENPOT_REDIS_URI=redis://penpot-valkey/0'
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:6061/readyz"]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 15
|
||||
|
||||
# MinIO S3 Local server
|
||||
minio:
|
||||
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z # Released on 15 October 2025
|
||||
command: server /data
|
||||
environment:
|
||||
- MINIO_ROOT_USER=${SERVICE_USER_MINIO}
|
||||
- MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO}
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
# S3 Bucket initialization
|
||||
minio-init:
|
||||
image: minio/mc:latest
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
sh -c "
|
||||
mc alias set local http://minio:9000 ${SERVICE_USER_MINIO} ${SERVICE_PASSWORD_MINIO} &&
|
||||
mc mb -p local/penpot || true &&
|
||||
mc anonymous set private local/penpot
|
||||
"
|
||||
restart: "no"
|
||||
exclude_from_hc: true
|
||||
|
||||
postgres:
|
||||
image: postgres:15
|
||||
volumes:
|
||||
- penpot-postgresql-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_INITDB_ARGS=--data-checksums
|
||||
- POSTGRES_USER=${SERVICE_USER_POSTGRES}
|
||||
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-penpot}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
penpot-valkey:
|
||||
image: 'valkey/valkey:8.1'
|
||||
volumes:
|
||||
- 'penpot-valkey-data:/data'
|
||||
environment:
|
||||
- 'VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu'
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- 'valkey-cli ping | grep PONG'
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
start_period: 3s
|
||||
|
||||
mailpit:
|
||||
image: axllent/mailpit:v1.28 # Released on 26 Nov 2025
|
||||
environment:
|
||||
- SERVICE_URL_MAILPIT_8025
|
||||
healthcheck:
|
||||
test: ['CMD', '/mailpit', 'readyz']
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: penpotapp/frontend:latest
|
||||
image: penpotapp/frontend:2.11.1 # Released on 13 Nov 2025
|
||||
volumes:
|
||||
- penpot-assets:/opt/data/assets
|
||||
depends_on:
|
||||
@@ -25,16 +25,16 @@ services:
|
||||
retries: 15
|
||||
|
||||
penpot-backend:
|
||||
image: penpotapp/backend:latest
|
||||
image: penpotapp/backend:2.11.1 # Released on 13 Nov 2025
|
||||
volumes:
|
||||
- penpot-assets:/opt/data/assets
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
penpot-valkey:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- PENPOT_FLAGS=${PENPOT_BACKEND_FLAGS:-enable-login-with-password enable-smtp enable-prepl-server}
|
||||
- PENPOT_FLAGS=${PENPOT_BACKEND_FLAGS:-enable-login-with-password enable-smtp}
|
||||
- PENPOT_HTTP_SERVER_PORT=6060
|
||||
- PENPOT_SECRET_KEY=$SERVICE_REALBASE64_64_PENPOT
|
||||
- PENPOT_PUBLIC_URI=$SERVICE_URL_FRONTEND_8080
|
||||
@@ -43,9 +43,9 @@ services:
|
||||
- PENPOT_DATABASE_URI=postgresql://postgres/${POSTGRES_DB:-penpot}
|
||||
- PENPOT_DATABASE_USERNAME=${SERVICE_USER_POSTGRES}
|
||||
- PENPOT_DATABASE_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
|
||||
- PENPOT_REDIS_URI=redis://redis/0
|
||||
- PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
|
||||
- PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets
|
||||
- 'PENPOT_REDIS_URI=redis://penpot-valkey/0'
|
||||
- PENPOT_OBJECTS_STORAGE_BACKEND=fs
|
||||
- PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets
|
||||
- PENPOT_TELEMETRY_ENABLED=${PENPOT_TELEMETRY_ENABLED:-false}
|
||||
- PENPOT_SMTP_DEFAULT_FROM=${PENPOT_SMTP_DEFAULT_FROM:-no-reply@example.com}
|
||||
- PENPOT_SMTP_DEFAULT_REPLY_TO=${PENPOT_SMTP_DEFAULT_REPLY_TO:-no-reply@example.com}
|
||||
@@ -62,10 +62,10 @@ services:
|
||||
retries: 15
|
||||
|
||||
penpot-exporter:
|
||||
image: penpotapp/exporter:latest
|
||||
image: penpotapp/exporter:2.11.1 # Released on 13 Nov 2025
|
||||
environment:
|
||||
- PENPOT_PUBLIC_URI=$SERVICE_URL_FRONTEND_8080
|
||||
- PENPOT_REDIS_URI=redis://redis/0
|
||||
- 'PENPOT_REDIS_URI=redis://penpot-valkey/0'
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://127.0.0.1:6061/readyz']
|
||||
interval: 2s
|
||||
@@ -73,7 +73,7 @@ services:
|
||||
retries: 15
|
||||
|
||||
mailpit:
|
||||
image: axllent/mailpit:latest
|
||||
image: axllent/mailpit:v1.28 # Released on 26 Nov 2025
|
||||
environment:
|
||||
- SERVICE_URL_MAILPIT_8025
|
||||
healthcheck:
|
||||
@@ -96,14 +96,18 @@ services:
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --appendonly yes
|
||||
|
||||
penpot-valkey:
|
||||
image: 'valkey/valkey:8.1'
|
||||
volumes:
|
||||
- penpot-redis-data:/data
|
||||
- 'penpot-valkey-data:/data'
|
||||
environment:
|
||||
- 'VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu'
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- 'valkey-cli ping | grep PONG'
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
start_period: 3s
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
services:
|
||||
postiz:
|
||||
image: ghcr.io/gitroomhq/postiz-app:v1.60.1
|
||||
image: ghcr.io/gitroomhq/postiz-app:v2.10.1
|
||||
environment:
|
||||
- SERVICE_URL_POSTIZ_5000
|
||||
- MAIN_URL=${SERVICE_URL_POSTIZ}
|
||||
|
||||
@@ -111,6 +111,8 @@ services:
|
||||
wings:
|
||||
image: 'ghcr.io/pterodactyl/wings:v1.11.13'
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "2022:2022"
|
||||
environment:
|
||||
- SERVICE_URL_WINGS_8443
|
||||
- 'TZ=${TIMEZONE:-UTC}'
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# documentation: https://github.com/hexlo/terraria-server-docker
|
||||
# slogan: Docker multi-arch Image for Terraria Server.
|
||||
# category: games
|
||||
# tags: terraria
|
||||
# logo: svgs/terraria.svg
|
||||
# port: 7777
|
||||
|
||||
services:
|
||||
terraria-server:
|
||||
image: 'hexlo/terraria-server-docker:1.4.4.9'
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ports:
|
||||
- '7777:7777'
|
||||
volumes:
|
||||
- 'terraria-server:/root/.local/share/Terraria/Worlds'
|
||||
environment:
|
||||
- 'WORLD=${WORLD:-/root/.local/share/Terraria/Worlds/world1.wld}'
|
||||
- 'AUTOCREATE=${AUTOCREATE:-2}'
|
||||
- 'WORLDNAME=${WORLDNAME:-world1}'
|
||||
- 'DIFFICULTY=${DIFFICULTY:-1}'
|
||||
- 'MAXPLAYERS=${MAXPLAYERS:-8}'
|
||||
- 'PASSWORD=${PASSWORD:-mypassword}'
|
||||
- 'MOTD=${MOTD:-Welcome to the server!}'
|
||||
- 'LANGUAGE=${LANGUAGE:-en/US}'
|
||||
- 'SECURE=${SECURE:-1}'
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
services:
|
||||
umami:
|
||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||
image: ghcr.io/umami-software/umami:3.0.2
|
||||
environment:
|
||||
- SERVICE_URL_UMAMI_3000
|
||||
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/$POSTGRES_DB
|
||||
|
||||
@@ -186,10 +186,25 @@
|
||||
"logo": "svgs/babybuddy.png",
|
||||
"minversion": "0.0.0"
|
||||
},
|
||||
"beszel-agent": {
|
||||
"documentation": "https://www.beszel.dev/guide/agent-installation?utm_source=coolify.io",
|
||||
"slogan": "Monitoring agent for Beszel",
|
||||
"compose": "c2VydmljZXM6CiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNURU49L2Jlc3plbF9zb2NrZXQvYmVzemVsLnNvY2sKICAgICAgLSAnSFVCX1VSTD0ke0hVQl9VUkw/fScKICAgICAgLSAnVE9LRU49JHtUT0tFTj99JwogICAgICAtICdLRVk9JHtLRVk/fScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCg==",
|
||||
"tags": [
|
||||
"beszel",
|
||||
"monitoring",
|
||||
"server",
|
||||
"stats",
|
||||
"alerts"
|
||||
],
|
||||
"category": "monitoring",
|
||||
"logo": "svgs/beszel.svg",
|
||||
"minversion": "0.0.0"
|
||||
},
|
||||
"beszel": {
|
||||
"documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io",
|
||||
"slogan": "A lightweight server resource monitoring hub with historical data, docker stats, and alerts.",
|
||||
"compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE1LjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9CRVNaRUxfODA5MAogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2RhdGE6L2Jlc3plbF9kYXRhJwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogIGJlc3plbC1hZ2VudDoKICAgIGltYWdlOiAnaGVucnlnZC9iZXN6ZWwtYWdlbnQ6MC4xNS4yJwogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2FnZW50X2RhdGE6L3Zhci9saWIvYmVzemVsLWFnZW50JwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RFTj0vYmVzemVsX3NvY2tldC9iZXN6ZWwuc29jawogICAgICAtICdIVUJfVVJMPWh0dHA6Ly9iZXN6ZWw6ODA5MCcKICAgICAgLSAnVE9LRU49JHtUT0tFTn0nCiAgICAgIC0gJ0tFWT0ke0tFWX0nCg==",
|
||||
"compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9CRVNaRUxfODA5MAogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2RhdGE6L2Jlc3plbF9kYXRhJwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogIGJlc3plbC1hZ2VudDoKICAgIGltYWdlOiAnaGVucnlnZC9iZXN6ZWwtYWdlbnQ6MC4xNi4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTElTVEVOPS9iZXN6ZWxfc29ja2V0L2Jlc3plbC5zb2NrCiAgICAgIC0gJ0hVQl9VUkw9aHR0cDovL2Jlc3plbDo4MDkwJwogICAgICAtICdUT0tFTj0ke1RPS0VOfScKICAgICAgLSAnS0VZPSR7S0VZfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCg==",
|
||||
"tags": [
|
||||
"beszel",
|
||||
"monitoring",
|
||||
@@ -2227,7 +2242,7 @@
|
||||
"litellm": {
|
||||
"documentation": "https://docs.litellm.ai?utm_source=coolify.io",
|
||||
"slogan": "Call all LLM APIs using the OpenAI format. Use Bedrock, Azure, OpenAI, Cohere, Anthropic, Ollama, Sagemaker, HuggingFace, Replicate, Groq (100+ LLMs)",
|
||||
"compose": "c2VydmljZXM6CiAgbGl0ZWxsbToKICAgIGltYWdlOiAnZ2hjci5pby9iZXJyaWFpL2xpdGVsbG0tZGF0YWJhc2U6bWFpbi1zdGFibGUnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTElURUxMTV80MDAwCiAgICAgIC0gJ0xJVEVMTE1fTE9HPSR7TElURUxMTV9MT0c6LUVSUk9SfScKICAgICAgLSAnTElURUxMTV9NT0RFPSR7TElURUxMTV9NT0RFOi1QUk9EVUNUSU9OfScKICAgICAgLSAnTElURUxMTV9NQVNURVJfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9NQVNURVJLRVl9JwogICAgICAtICdVSV9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9VSX0nCiAgICAgIC0gJ1VJX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9VSX0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1saXRlbGxtfScKICAgICAgLSAnUkVESVNfSE9TVD0ke1JFRElTX0hPU1Q6LXJlZGlzfScKICAgICAgLSAnUkVESVNfUE9SVD0ke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbGl0ZWxsbX0nCiAgICAgIC0gJ09QRU5BSV9BUElfS0VZPSR7T1BFTkFJX0FQSV9LRVl9JwogICAgICAtICdPUEVOQUlfQVBJX0JBU0U9JHtPUEVOQUlfQVBJX0JBU0V9JwogICAgICAtICdBTlRIUk9QSUNfQVBJX0tFWT0ke0FOVEhST1BJQ19BUElfS0VZfScKICAgICAgLSAnQU5USFJPUElDX0FQSV9CQVNFPSR7QU5USFJPUElDX0FQSV9CQVNFfScKICAgICAgLSAnVk9ZQUdFX0FQSV9LRVk9JHtWT1lBR0VfQVBJX0tFWX0nCiAgICAgIC0gJ1ZPWUFHRV9BUElfQkFTRT0ke1ZPWUFHRV9BUElfQkFTRX0nCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9saXRlbGxtLWNvbmZpZy55YW1sCiAgICAgICAgdGFyZ2V0OiAvYXBwL2NvbmZpZy55YW1sCiAgICAgICAgY29udGVudDogImdlbmVyYWxfc2V0dGluZ3M6XG4gIHByb3h5X2JhdGNoX3dyaXRlX2F0OiA2MFxuXG5yb3V0ZXJfc2V0dGluZ3M6XG4gIHJlZGlzX2hvc3Q6IG9zLmVudmlyb24vUkVESVNfSE9TVFxuICByZWRpc19wb3J0OiBvcy5lbnZpcm9uL1JFRElTX1BPUlRcbiAgcmVkaXNfcGFzc3dvcmQ6IG9zLmVudmlyb24vUkVESVNfUEFTU1dPUkRcbiAgZW5hYmxlX3ByZV9jYWxsX2NoZWNrOiB0cnVlXG5cbmxpdGVsbG1fc2V0dGluZ3M6XG4gIHNldF92ZXJib3NlOiBmYWxzZVxuICBqc29uX2xvZ3M6IHRydWVcbiAgbG9nX3Jhd19yZXF1ZXN0X3Jlc3BvbnNlOiB0cnVlXG4gICMgdHVybl9vZmZfbWVzc2FnZV9sb2dnaW5nOiBmYWxzZVxuICAjIHJlZGFjdF91c2VyX2FwaV9rZXlfaW5mbzogZmFsc2VcbiAgc2VydmljZV9jYWxsYmFjazogW1wicHJvbWV0aGV1c19zeXN0ZW1cIl1cbiAgZHJvcF9wYXJhbXM6IHRydWVcbiAgIyBtYXhfYnVkZ2V0OiAxMDBcbiAgIyBidWRnZXRfZHVyYXRpb246IDMwZFxuICBudW1fcmV0cmllczogM1xuICByZXF1ZXN0X3RpbWVvdXQ6IDYwMFxuICB0ZWxlbWV0cnk6IGZhbHNlXG4gIGNhY2hlOiB0cnVlXG4gIGNhY2hlX3BhcmFtczpcbiAgICB0eXBlOiByZWRpc1xuICAgIGhvc3Q6IG9zLmVudmlyb24vUkVESVNfSE9TVFxuICAgIHBvcnQ6IG9zLmVudmlyb24vUkVESVNfUE9SVFxuICAgIHBhc3N3b3JkOiBvcy5lbnZpcm9uL1JFRElTX1BBU1NXT1JEXG4gICAgbmFtZXNwYWNlOiBcImxpdGVsbG1fY2FjaGVcIlxuICAgIHR0bDogNjAwXG4gIHN1Y2Nlc3NfY2FsbGJhY2s6XG4gICAgIyAtIFwibGFuZ2Z1c2VcIlxuICAgIC0gXCJwcm9tZXRoZXVzXCJcbiAgZmFpbHVyZV9jYWxsYmFjazpcbiAgICAjIC0gXCJsYW5nZnVzZVwiXG4gICAgLSBcInByb21ldGhldXNcIlxubW9kZWxfbGlzdDpcbiAgIyBPcGVuQUlcbiAgLSBtb2RlbF9uYW1lOiBncHQtNFxuICAgIGxpdGVsbG1fcGFyYW1zOlxuICAgICAgbW9kZWw6IG9wZW5haS9ncHQtNFxuICAgICAgYXBpX2tleTogb3MuZW52aXJvbi9PUEVOQUlfQVBJX0tFWVxuICAgICAgYXBpX2Jhc2U6IG9zLmVudmlyb24vT1BFTkFJX0FQSV9CQVNFXG4gIC0gbW9kZWxfbmFtZTogZ3B0LTRvXG4gICAgbGl0ZWxsbV9wYXJhbXM6XG4gICAgICBtb2RlbDogb3BlbmFpL2dwdC00b1xuICAgICAgYXBpX2tleTogb3MuZW52aXJvbi9PUEVOQUlfQVBJX0tFWVxuICAgICAgYXBpX2Jhc2U6IG9zLmVudmlyb24vT1BFTkFJX0FQSV9CQVNFXG4gIC0gbW9kZWxfbmFtZTogZ3B0LTRvLW1pbmlcbiAgICBsaXRlbGxtX3BhcmFtczpcbiAgICAgIG1vZGVsOiBvcGVuYWkvZ3B0LTRvLW1pbmlcbiAgICAgIGFwaV9rZXk6IG9zLmVudmlyb24vT1BFTkFJX0FQSV9LRVlcbiAgICAgIGFwaV9iYXNlOiBvcy5lbnZpcm9uL09QRU5BSV9BUElfQkFTRVxuICAjIEFudGhyb3BpY1xuICAtIG1vZGVsX25hbWU6IGNsYXVkZS0zLWhhaWt1XG4gICAgbGl0ZWxsbV9wYXJhbXM6XG4gICAgICBtb2RlbDogY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDdcbiAgICAgIGFwaV9rZXk6IFwib3MuZW52aXJvbi9BTlRIUk9QSUNfQVBJX0tFWVwiXG4gICAgICBhcGlfYmFzZTogXCJvcy5lbnZpcm9uL0FOVEhST1BJQ19BUElfQkFTRVwiXG4gIC0gbW9kZWxfbmFtZTogY2xhdWRlLTMuNS1zb25uZXRcbiAgICBsaXRlbGxtX3BhcmFtczpcbiAgICAgIG1vZGVsOiBjbGF1ZGUtMy01LXNvbm5ldC0yMDI0MDYyMFxuICAgICAgYXBpX2tleTogXCJvcy5lbnZpcm9uL0FOVEhST1BJQ19BUElfS0VZXCJcbiAgICAgIGFwaV9iYXNlOiBcIm9zLmVudmlyb24vQU5USFJPUElDX0FQSV9CQVNFXCJcbiAgIyBWb3lhZ2VBSVxuICAtIG1vZGVsX25hbWU6IHZveWFnZS1sYXctMlxuICAgIG1vZGVsX2luZm86XG4gICAgICBvdXRwdXRfdmVjdG9yX3NpemU6IDEwMjRcbiAgICBsaXRlbGxtX3BhcmFtczpcbiAgICAgIG1vZGVsOiB2b3lhZ2Uvdm95YWdlLWxhdy0yXG4gICAgICBhcGlfa2V5OiBcIm9zLmVudmlyb24vVk9ZQUdFX0FQSV9LRVlcIlxuICAgICAgYXBpX2Jhc2U6IFwib3MuZW52aXJvbi9WT1lBR0VfQVBJX0JBU0VcIlxuICAgICAgIyBycG06IDMwMFxuICAgICAgIyB0cG06IDEwMDAwMDBcbiAgLSBtb2RlbF9uYW1lOiB2b3lhZ2UtbXVsdGlsaW5ndWFsLTJcbiAgICBtb2RlbF9pbmZvOlxuICAgICAgbW9kZTogZW1iZWRkaW5nXG4gICAgICBtYXhfdG9rZW5zOiAzMjAwMFxuICAgICAgbWF4X2lucHV0X3Rva2VuczogMzIwMDBcbiAgICAgIG91dHB1dF92ZWN0b3Jfc2l6ZTogMTAyNFxuICAgIGxpdGVsbG1fcGFyYW1zOlxuICAgICAgbW9kZWw6IHZveWFnZS92b3lhZ2UtbXVsdGlsaW5ndWFsLTJcbiAgICAgIGFwaV9rZXk6IFwib3MuZW52aXJvbi9WT1lBR0VfQVBJX0tFWVwiXG4gICAgICBhcGlfYmFzZTogXCJvcy5lbnZpcm9uL1ZPWUFHRV9BUElfQkFTRVwiXG4gICAgICBpbnB1dF9jb3N0X3Blcl90b2tlbjogMC4wMDAwMDAxMlxuICAgICAgb3V0cHV0X2Nvc3RfcGVyX3Rva2VLine truncated
|
||||
"compose": "c2VydmljZXM6CiAgbGl0ZWxsbToKICAgIGltYWdlOiAnZ2hjci5pby9iZXJyaWFpL2xpdGVsbG0tZGF0YWJhc2U6bWFpbi1zdGFibGUnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTElURUxMTV80MDAwCiAgICAgIC0gJ0xJVEVMTE1fTE9HPSR7TElURUxMTV9MT0c6LUVSUk9SfScKICAgICAgLSAnTElURUxMTV9NT0RFPSR7TElURUxMTV9NT0RFOi1QUk9EVUNUSU9OfScKICAgICAgLSAnTElURUxMTV9NQVNURVJfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9NQVNURVJLRVl9JwogICAgICAtICdVSV9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9VSX0nCiAgICAgIC0gJ1VJX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9VSX0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1saXRlbGxtfScKICAgICAgLSAnUkVESVNfSE9TVD0ke1JFRElTX0hPU1Q6LXJlZGlzfScKICAgICAgLSAnUkVESVNfUE9SVD0ke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbGl0ZWxsbX0nCiAgICAgIC0gJ09QRU5BSV9BUElfS0VZPSR7T1BFTkFJX0FQSV9LRVl9JwogICAgICAtICdPUEVOQUlfQVBJX0JBU0U9JHtPUEVOQUlfQVBJX0JBU0V9JwogICAgICAtICdBTlRIUk9QSUNfQVBJX0tFWT0ke0FOVEhST1BJQ19BUElfS0VZfScKICAgICAgLSAnQU5USFJPUElDX0FQSV9CQVNFPSR7QU5USFJPUElDX0FQSV9CQVNFfScKICAgICAgLSAnVk9ZQUdFX0FQSV9LRVk9JHtWT1lBR0VfQVBJX0tFWX0nCiAgICAgIC0gJ1ZPWUFHRV9BUElfQkFTRT0ke1ZPWUFHRV9BUElfQkFTRX0nCiAgICAgIC0gJ1NUT1JFX01PREVMX0lOX0RCPSR7U1RPUkVfTU9ERUxfSU5fREJ9JwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vbGl0ZWxsbS1jb25maWcueWFtbAogICAgICAgIHRhcmdldDogL2FwcC9jb25maWcueWFtbAogICAgICAgIGNvbnRlbnQ6ICJnZW5lcmFsX3NldHRpbmdzOlxuICBwcm94eV9iYXRjaF93cml0ZV9hdDogNjBcblxucm91dGVyX3NldHRpbmdzOlxuICByZWRpc19ob3N0OiBvcy5lbnZpcm9uL1JFRElTX0hPU1RcbiAgcmVkaXNfcG9ydDogb3MuZW52aXJvbi9SRURJU19QT1JUXG4gIHJlZGlzX3Bhc3N3b3JkOiBvcy5lbnZpcm9uL1JFRElTX1BBU1NXT1JEXG4gIGVuYWJsZV9wcmVfY2FsbF9jaGVjazogdHJ1ZVxuXG5saXRlbGxtX3NldHRpbmdzOlxuICBzZXRfdmVyYm9zZTogZmFsc2VcbiAganNvbl9sb2dzOiB0cnVlXG4gIGxvZ19yYXdfcmVxdWVzdF9yZXNwb25zZTogdHJ1ZVxuICAjIHR1cm5fb2ZmX21lc3NhZ2VfbG9nZ2luZzogZmFsc2VcbiAgIyByZWRhY3RfdXNlcl9hcGlfa2V5X2luZm86IGZhbHNlXG4gIHNlcnZpY2VfY2FsbGJhY2s6IFtcInByb21ldGhldXNfc3lzdGVtXCJdXG4gIGRyb3BfcGFyYW1zOiB0cnVlXG4gICMgbWF4X2J1ZGdldDogMTAwXG4gICMgYnVkZ2V0X2R1cmF0aW9uOiAzMGRcbiAgbnVtX3JldHJpZXM6IDNcbiAgcmVxdWVzdF90aW1lb3V0OiA2MDBcbiAgdGVsZW1ldHJ5OiBmYWxzZVxuICBjYWNoZTogdHJ1ZVxuICBjYWNoZV9wYXJhbXM6XG4gICAgdHlwZTogcmVkaXNcbiAgICBob3N0OiBvcy5lbnZpcm9uL1JFRElTX0hPU1RcbiAgICBwb3J0OiBvcy5lbnZpcm9uL1JFRElTX1BPUlRcbiAgICBwYXNzd29yZDogb3MuZW52aXJvbi9SRURJU19QQVNTV09SRFxuICAgIG5hbWVzcGFjZTogXCJsaXRlbGxtX2NhY2hlXCJcbiAgICB0dGw6IDYwMFxuICBzdWNjZXNzX2NhbGxiYWNrOlxuICAgICMgLSBcImxhbmdmdXNlXCJcbiAgICAtIFwicHJvbWV0aGV1c1wiXG4gIGZhaWx1cmVfY2FsbGJhY2s6XG4gICAgIyAtIFwibGFuZ2Z1c2VcIlxuICAgIC0gXCJwcm9tZXRoZXVzXCJcbm1vZGVsX2xpc3Q6XG4gICMgT3BlbkFJXG4gIC0gbW9kZWxfbmFtZTogZ3B0LTRcbiAgICBsaXRlbGxtX3BhcmFtczpcbiAgICAgIG1vZGVsOiBvcGVuYWkvZ3B0LTRcbiAgICAgIGFwaV9rZXk6IG9zLmVudmlyb24vT1BFTkFJX0FQSV9LRVlcbiAgICAgIGFwaV9iYXNlOiBvcy5lbnZpcm9uL09QRU5BSV9BUElfQkFTRVxuICAtIG1vZGVsX25hbWU6IGdwdC00b1xuICAgIGxpdGVsbG1fcGFyYW1zOlxuICAgICAgbW9kZWw6IG9wZW5haS9ncHQtNG9cbiAgICAgIGFwaV9rZXk6IG9zLmVudmlyb24vT1BFTkFJX0FQSV9LRVlcbiAgICAgIGFwaV9iYXNlOiBvcy5lbnZpcm9uL09QRU5BSV9BUElfQkFTRVxuICAtIG1vZGVsX25hbWU6IGdwdC00by1taW5pXG4gICAgbGl0ZWxsbV9wYXJhbXM6XG4gICAgICBtb2RlbDogb3BlbmFpL2dwdC00by1taW5pXG4gICAgICBhcGlfa2V5OiBvcy5lbnZpcm9uL09QRU5BSV9BUElfS0VZXG4gICAgICBhcGlfYmFzZTogb3MuZW52aXJvbi9PUEVOQUlfQVBJX0JBU0VcbiAgIyBBbnRocm9waWNcbiAgLSBtb2RlbF9uYW1lOiBjbGF1ZGUtMy1oYWlrdVxuICAgIGxpdGVsbG1fcGFyYW1zOlxuICAgICAgbW9kZWw6IGNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3XG4gICAgICBhcGlfa2V5OiBcIm9zLmVudmlyb24vQU5USFJPUElDX0FQSV9LRVlcIlxuICAgICAgYXBpX2Jhc2U6IFwib3MuZW52aXJvbi9BTlRIUk9QSUNfQVBJX0JBU0VcIlxuICAtIG1vZGVsX25hbWU6IGNsYXVkZS0zLjUtc29ubmV0XG4gICAgbGl0ZWxsbV9wYXJhbXM6XG4gICAgICBtb2RlbDogY2xhdWRlLTMtNS1zb25uZXQtMjAyNDA2MjBcbiAgICAgIGFwaV9rZXk6IFwib3MuZW52aXJvbi9BTlRIUk9QSUNfQVBJX0tFWVwiXG4gICAgICBhcGlfYmFzZTogXCJvcy5lbnZpcm9uL0FOVEhST1BJQ19BUElfQkFTRVwiXG4gICMgVm95YWdlQUlcbiAgLSBtb2RlbF9uYW1lOiB2b3lhZ2UtbGF3LTJcbiAgICBtb2RlbF9pbmZvOlxuICAgICAgb3V0cHV0X3ZlY3Rvcl9zaXplOiAxMDI0XG4gICAgbGl0ZWxsbV9wYXJhbXM6XG4gICAgICBtb2RlbDogdm95YWdlL3ZveWFnZS1sYXctMlxuICAgICAgYXBpX2tleTogXCJvcy5lbnZpcm9uL1ZPWUFHRV9BUElfS0VZXCJcbiAgICAgIGFwaV9iYXNlOiBcIm9zLmVudmlyb24vVk9ZQUdFX0FQSV9CQVNFXCJcbiAgICAgICMgcnBtOiAzMDBcbiAgICAgICMgdHBtOiAxMDAwMDAwXG4gIC0gbW9kZWxfbmFtZTogdm95YWdlLW11bHRpbGluZ3VhbC0yXG4gICAgbW9kZWxfaW5mbzpcbiAgICAgIG1vZGU6IGVtYmVkZGluZ1xuICAgICAgbWF4X3Rva2VuczogMzIwMDBcbiAgICAgIG1heF9pbnB1dF90b2tlbnM6IDMyMDAwXG4gICAgICBvdXRwdXRfdmVjdG9yX3NpemU6IDEwMjRcbiAgICBsaXRlbGxtX3BhcmFtczpcbiAgICAgIG1vZGVsOiB2b3lhZ2Uvdm95YWdlLW11bHRpbGluZ3VhbC0yXG4gICAgICBhcGlfa2V5OiBcIm9zLmVudmlyb24vVk9ZQUdFX0FQSV9LRVlcIlxuICAgICAgYXBpX2Jhc2U6IFwib3MuZW52aXJvbi9WT1lBR0VfQVBJX0JBU0VcIlxuICAgICAgaW5wdXRfY29zdF9Line truncated
|
||||
"tags": [
|
||||
"ai",
|
||||
"qdrant",
|
||||
@@ -3226,7 +3241,7 @@
|
||||
"paperless": {
|
||||
"documentation": "https://docs.paperless-ngx.com/configuration/?utm_source=coolify.io",
|
||||
"slogan": "Paperless-ngx is a community-supported open-source document management system that transforms your physical documents into a searchable online archive so you can keep, well, less paper.",
|
||||
"compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuNCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhcGVybGVzcy1yZWRpczovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICBwYXBlcmxlc3M6CiAgICBpbWFnZTogJ3BhcGVybGVzc25neC9wYXBlcmxlc3Mtbmd4OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mcycKICAgICAgICAtICctUycKICAgICAgICAtICctLW1heC10aW1lJwogICAgICAgIC0gJzInCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICdwYXBlcmxlc3MtZGF0YTovdXNyL3NyYy9wYXBlcmxlc3MvZGF0YScKICAgICAgLSAncGFwZXJsZXNzLW1lZGlhOi91c3Ivc3JjL3BhcGVybGVzcy9tZWRpYScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXhwb3J0CiAgICAgICAgdGFyZ2V0OiAvdXNyL3NyYy9wYXBlcmxlc3MvZXhwb3J0CiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NvbnN1bWUKICAgICAgICB0YXJnZXQ6IC91c3Ivc3JjL3BhcGVybGVzcy9jb25zdW1lCiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9QQVBFUkxFU1NfODAwMAogICAgICAtIFBBUEVSTEVTU19VUkw9JFNFUlZJQ0VfVVJMX1BBUEVSTEVTU184MDAwCiAgICAgIC0gJ1BBUEVSTEVTU19BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUEFQRVJMRVNTfScKICAgICAgLSAnUEFQRVJMRVNTX0FETUlOX1VTRVI9JHtTRVJWSUNFX1VTRVJfUEFQRVJMRVNTfScKICAgICAgLSAnUEFQRVJMRVNTX1JFRElTPXJlZGlzOi8vcmVkaXM6NjM3OScKICAgICAgLSAnUEFQRVJMRVNTX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1JFQUxCQVNFNjRfNjRfUEFQRVJMRVNTfScK",
|
||||
"compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuNCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhcGVybGVzcy1yZWRpczovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICBwYXBlcmxlc3M6CiAgICBpbWFnZTogJ3BhcGVybGVzc25neC9wYXBlcmxlc3Mtbmd4OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mcycKICAgICAgICAtICctUycKICAgICAgICAtICctLW1heC10aW1lJwogICAgICAgIC0gJzInCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICdwYXBlcmxlc3MtZGF0YTovdXNyL3NyYy9wYXBlcmxlc3MvZGF0YScKICAgICAgLSAncGFwZXJsZXNzLW1lZGlhOi91c3Ivc3JjL3BhcGVybGVzcy9tZWRpYScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXhwb3J0CiAgICAgICAgdGFyZ2V0OiAvdXNyL3NyYy9wYXBlcmxlc3MvZXhwb3J0CiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NvbnN1bWUKICAgICAgICB0YXJnZXQ6IC91c3Ivc3JjL3BhcGVybGVzcy9jb25zdW1lCiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9QQVBFUkxFU1NfODAwMAogICAgICAtIFBBUEVSTEVTU19VUkw9JFNFUlZJQ0VfVVJMX1BBUEVSTEVTUwogICAgICAtICdQQVBFUkxFU1NfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BBUEVSTEVTU30nCiAgICAgIC0gJ1BBUEVSTEVTU19BRE1JTl9VU0VSPSR7U0VSVklDRV9VU0VSX1BBUEVSTEVTU30nCiAgICAgIC0gJ1BBUEVSTEVTU19SRURJUz1yZWRpczovL3JlZGlzOjYzNzknCiAgICAgIC0gJ1BBUEVSTEVTU19TRUNSRVRfS0VZPSR7U0VSVklDRV9SRUFMQkFTRTY0XzY0X1BBUEVSTEVTU30nCg==",
|
||||
"tags": null,
|
||||
"category": "productivity",
|
||||
"logo": "svgs/paperless.svg",
|
||||
@@ -3262,10 +3277,27 @@
|
||||
"minversion": "0.0.0",
|
||||
"port": "80"
|
||||
},
|
||||
"penpot-with-s3": {
|
||||
"documentation": "https://help.penpot.app/technical-guide/getting-started/#install-with-docker?utm_source=coolify.io",
|
||||
"slogan": "Penpot is the first Open Source design and prototyping platform for product teams.",
|
||||
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9mcm9udGVuZDoyLjExLjEnCiAgICBkZXBlbmRzX29uOgogICAgICBwZW5wb3QtYmFja2VuZDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwZW5wb3QtZXhwb3J0ZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0ZST05URU5EXzgwODAKICAgICAgLSAnUEVOUE9UX0ZMQUdTPSR7UEVOUE9UX0JBQ0tFTkRfRkxBR1M6LWVuYWJsZS1sb2dpbi13aXRoLXBhc3N3b3JkfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcGVucG90LWJhY2tlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9iYWNrZW5kOjIuMTEuMScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHBlbnBvdC12YWxrZXk6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgbWluaW8taW5pdDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfY29tcGxldGVkX3N1Y2Nlc3NmdWxseQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BFTlBPVF9GTEFHUz0ke1BFTlBPVF9CQUNLRU5EX0ZMQUdTOi1lbmFibGUtbG9naW4td2l0aC1wYXNzd29yZH0nCiAgICAgIC0gUEVOUE9UX0hUVFBfU0VSVkVSX1BPUlQ9NjA2MAogICAgICAtICdQRU5QT1RfU0VDUkVUX0tFWT0ke1NFUlZJQ0VfUkVBTEJBU0U2NF82NF9QRU5QT1R9JwogICAgICAtICdQRU5QT1RfUFVCTElDX1VSST0ke1NFUlZJQ0VfVVJMX0ZST05URU5EXzgwODB9JwogICAgICAtICdQRU5QT1RfQkFDS0VORF9VUkk9aHR0cDovL3BlbnBvdC1iYWNrZW5kJwogICAgICAtICdQRU5QT1RfRVhQT1JURVJfVVJJPWh0dHA6Ly9wZW5wb3QtZXhwb3J0ZXInCiAgICAgIC0gJ1BFTlBPVF9EQVRBQkFTRV9VUkk9cG9zdGdyZXNxbDovL3Bvc3RncmVzLyR7UE9TVEdSRVNfREI6LXBlbnBvdH0nCiAgICAgIC0gJ1BFTlBPVF9EQVRBQkFTRV9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BFTlBPVF9EQVRBQkFTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQRU5QT1RfUkVESVNfVVJJPXJlZGlzOi8vcGVucG90LXZhbGtleS8wJwogICAgICAtICdQRU5QT1RfVEVMRU1FVFJZX0VOQUJMRUQ9JHtQRU5QT1RfVEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSBQRU5QT1RfT0JKRUNUU19TVE9SQUdFX0JBQ0tFTkQ9czMKICAgICAgLSBQRU5QT1RfT0JKRUNUU19TVE9SQUdFX1MzX1JFR0lPTj11cy1lYXN0LTEKICAgICAgLSBQRU5QT1RfT0JKRUNUU19TVE9SQUdFX1MzX0JVQ0tFVD1wZW5wb3QKICAgICAgLSAnUEVOUE9UX09CSkVDVFNfU1RPUkFHRV9TM19FTkRQT0lOVD1odHRwOi8vbWluaW86OTAwMCcKICAgICAgLSAnQVdTX0FDQ0VTU19LRVlfSUQ9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgICAgLSAnUEVOUE9UX1NNVFBfREVGQVVMVF9GUk9NPSR7UEVOUE9UX1NNVFBfREVGQVVMVF9GUk9NOi1uby1yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0RFRkFVTFRfUkVQTFlfVE89JHtQRU5QT1RfU01UUF9ERUZBVUxUX1JFUExZX1RPOi1uby1yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0hPU1Q9JHtQRU5QT1RfU01UUF9IT1NUOi1tYWlscGl0fScKICAgICAgLSAnUEVOUE9UX1NNVFBfUE9SVD0ke1BFTlBPVF9TTVRQX1BPUlQ6LTEwMjV9JwogICAgICAtICdQRU5QT1RfU01UUF9VU0VSTkFNRT0ke1BFTlBPVF9TTVRQX1VTRVJOQU1FOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfU01UUF9QQVNTV09SRD0ke1BFTlBPVF9TTVRQX1BBU1NXT1JEOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfU01UUF9UTFM9JHtQRU5QT1RfU01UUF9UTFM6LWZhbHNlfScKICAgICAgLSAnUEVOUE9UX1NNVFBfU1NMPSR7UEVOUE9UX1NNVFBfU1NMOi1mYWxzZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gInJlcXVpcmUoJ2h0dHAnKS5nZXQoe2hvc3Q6JzEyNy4wLjAuMScsIHBvcnQ6NjA2MCwgcGF0aDonL3JlYWR5eid9LCByZXMgPT4gcHJvY2Vzcy5leGl0KHJlcy5zdGF0dXNDb2RlPT09MjAwID8gMCA6IDEpKS5vbignZXJyb3InLCAoKSA9PiBwcm9jZXNzLmV4aXQoMSkpOyIKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogMTUKICBwZW5wb3QtZXhwb3J0ZXI6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9leHBvcnRlcjoyLjExLjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUEVOUE9UX1BVQkxJQ19VUkk9JHtTRVJWSUNFX1VSTF9GUk9OVEVORF84MDgwfScKICAgICAgLSAnUEVOUE9UX1JFRElTX1VSST1yZWRpczovL3BlbnBvdC12YWxrZXkvMCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2MDYxL3JlYWR5eicKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIG1pbmlvOgogICAgaW1hZ2U6ICdnaGNyLmlvL2Nvb2xsYWJzaW8vbWluaW86UkVMRUFTRS4yMDI1LTEwLTE1VDE3LTI5LTU1WicKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBtaW5pby1pbml0OgogICAgaW1hZ2U6ICdtaW5pby9tYzpsYXRlc3QnCiAgICBkZXBlbmRzX29uOgogICAgICBtaW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW50cnlwb2ludDogInNoIC1jIFwiXG4gIG1jIGFsaWFzIHNldCBsb2NhbCBodHRwOi8vbWluaW86OTAwMCAke1NFUlZJQ0VfVVNFUl9NSU5JT30gJHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfSAmJlxuICBtYyBtYiAtcCBsb2NhbC9wZW5wb3QgfHwgdHJ1ZSAmJlxuICBtYyBhbm9ueW1vdXMgc2V0IHByaXZhdGUgbG9jYWwvcGVucG90XG5cIlxuIgogICAgcmVzdGFydDogJ25vJwogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1JwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3JLine truncated
|
||||
"tags": [
|
||||
"penpot",
|
||||
"design",
|
||||
"prototyping",
|
||||
"figma",
|
||||
"open",
|
||||
"source"
|
||||
],
|
||||
"category": "productivity",
|
||||
"logo": "svgs/penpot.svg",
|
||||
"minversion": "0.0.0",
|
||||
"port": "8080"
|
||||
},
|
||||
"penpot": {
|
||||
"documentation": "https://help.penpot.app/technical-guide/getting-started/#install-with-docker?utm_source=coolify.io",
|
||||
"slogan": "Penpot is the first Open Source design and prototyping platform for product teams.",
|
||||
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9mcm9udGVuZDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtYXNzZXRzOi9vcHQvZGF0YS9hc3NldHMnCiAgICBkZXBlbmRzX29uOgogICAgICBwZW5wb3QtYmFja2VuZDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwZW5wb3QtZXhwb3J0ZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0ZST05URU5EXzgwODAKICAgICAgLSAnUEVOUE9UX0ZMQUdTPSR7UEVOUE9UX0ZST05URU5EX0ZMQUdTOi1lbmFibGUtbG9naW4td2l0aC1wYXNzd29yZH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHBlbnBvdC1iYWNrZW5kOgogICAgaW1hZ2U6ICdwZW5wb3RhcHAvYmFja2VuZDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtYXNzZXRzOi9vcHQvZGF0YS9hc3NldHMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BFTlBPVF9GTEFHUz0ke1BFTlBPVF9CQUNLRU5EX0ZMQUdTOi1lbmFibGUtbG9naW4td2l0aC1wYXNzd29yZCBlbmFibGUtc210cCBlbmFibGUtcHJlcGwtc2VydmVyfScKICAgICAgLSBQRU5QT1RfSFRUUF9TRVJWRVJfUE9SVD02MDYwCiAgICAgIC0gUEVOUE9UX1NFQ1JFVF9LRVk9JFNFUlZJQ0VfUkVBTEJBU0U2NF82NF9QRU5QT1QKICAgICAgLSBQRU5QT1RfUFVCTElDX1VSST0kU0VSVklDRV9VUkxfRlJPTlRFTkRfODA4MAogICAgICAtICdQRU5QT1RfQkFDS0VORF9VUkk9aHR0cDovL3BlbnBvdC1iYWNrZW5kJwogICAgICAtICdQRU5QT1RfRVhQT1JURVJfVVJJPWh0dHA6Ly9wZW5wb3QtZXhwb3J0ZXInCiAgICAgIC0gJ1BFTlBPVF9EQVRBQkFTRV9VUkk9cG9zdGdyZXNxbDovL3Bvc3RncmVzLyR7UE9TVEdSRVNfREI6LXBlbnBvdH0nCiAgICAgIC0gJ1BFTlBPVF9EQVRBQkFTRV9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BFTlBPVF9EQVRBQkFTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQRU5QT1RfUkVESVNfVVJJPXJlZGlzOi8vcmVkaXMvMCcKICAgICAgLSBQRU5QT1RfQVNTRVRTX1NUT1JBR0VfQkFDS0VORD1hc3NldHMtZnMKICAgICAgLSBQRU5QT1RfU1RPUkFHRV9BU1NFVFNfRlNfRElSRUNUT1JZPS9vcHQvZGF0YS9hc3NldHMKICAgICAgLSAnUEVOUE9UX1RFTEVNRVRSWV9FTkFCTEVEPSR7UEVOUE9UX1RFTEVNRVRSWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0RFRkFVTFRfRlJPTT0ke1BFTlBPVF9TTVRQX0RFRkFVTFRfRlJPTTotbm8tcmVwbHlAZXhhbXBsZS5jb219JwogICAgICAtICdQRU5QT1RfU01UUF9ERUZBVUxUX1JFUExZX1RPPSR7UEVOUE9UX1NNVFBfREVGQVVMVF9SRVBMWV9UTzotbm8tcmVwbHlAZXhhbXBsZS5jb219JwogICAgICAtICdQRU5QT1RfU01UUF9IT1NUPSR7UEVOUE9UX1NNVFBfSE9TVDotbWFpbHBpdH0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1BPUlQ9JHtQRU5QT1RfU01UUF9QT1JUOi0xMDI1fScKICAgICAgLSAnUEVOUE9UX1NNVFBfVVNFUk5BTUU9JHtQRU5QT1RfU01UUF9VU0VSTkFNRTotcGVucG90fScKICAgICAgLSAnUEVOUE9UX1NNVFBfUEFTU1dPUkQ9JHtQRU5QT1RfU01UUF9QQVNTV09SRDotcGVucG90fScKICAgICAgLSAnUEVOUE9UX1NNVFBfVExTPSR7UEVOUE9UX1NNVFBfVExTOi1mYWxzZX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1NTTD0ke1BFTlBPVF9TTVRQX1NTTDotZmFsc2V9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG5vZGUKICAgICAgICAtICctZScKICAgICAgICAtICJyZXF1aXJlKCdodHRwJykuZ2V0KHtob3N0OicxMjcuMC4wLjEnLCBwb3J0OjYwNjAsIHBhdGg6Jy9yZWFkeXonfSwgcmVzID0+IHByb2Nlc3MuZXhpdChyZXMuc3RhdHVzQ29kZT09PTIwMCA/IDAgOiAxKSkub24oJ2Vycm9yJywgKCkgPT4gcHJvY2Vzcy5leGl0KDEpKTsiCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMzBzCiAgICAgIHJldHJpZXM6IDE1CiAgcGVucG90LWV4cG9ydGVyOgogICAgaW1hZ2U6ICdwZW5wb3RhcHAvZXhwb3J0ZXI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUEVOUE9UX1BVQkxJQ19VUkk9JFNFUlZJQ0VfVVJMX0ZST05URU5EXzgwODAKICAgICAgLSAnUEVOUE9UX1JFRElTX1VSST1yZWRpczovL3JlZGlzLzAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NjA2MS9yZWFkeXonCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBtYWlscGl0OgogICAgaW1hZ2U6ICdheGxsZW50L21haWxwaXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTUFJTFBJVF84MDI1CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL21haWxwaXQKICAgICAgICAtIHJlYWR5egogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1JwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19JTklUREJfQVJHUz0tLWRhdGEtY2hlY2tzdW1zCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcGVucG90fScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny1hbHBpbmUnCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tYXBwZW5kb25seSB5ZXMnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtcmVkaXMtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
|
||||
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9mcm9udGVuZDoyLjExLjEnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtYXNzZXRzOi9vcHQvZGF0YS9hc3NldHMnCiAgICBkZXBlbmRzX29uOgogICAgICBwZW5wb3QtYmFja2VuZDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwZW5wb3QtZXhwb3J0ZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0ZST05URU5EXzgwODAKICAgICAgLSAnUEVOUE9UX0ZMQUdTPSR7UEVOUE9UX0ZST05URU5EX0ZMQUdTOi1lbmFibGUtbG9naW4td2l0aC1wYXNzd29yZH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHBlbnBvdC1iYWNrZW5kOgogICAgaW1hZ2U6ICdwZW5wb3RhcHAvYmFja2VuZDoyLjExLjEnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtYXNzZXRzOi9vcHQvZGF0YS9hc3NldHMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwZW5wb3QtdmFsa2V5OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUEVOUE9UX0ZMQUdTPSR7UEVOUE9UX0JBQ0tFTkRfRkxBR1M6LWVuYWJsZS1sb2dpbi13aXRoLXBhc3N3b3JkIGVuYWJsZS1zbXRwfScKICAgICAgLSBQRU5QT1RfSFRUUF9TRVJWRVJfUE9SVD02MDYwCiAgICAgIC0gUEVOUE9UX1NFQ1JFVF9LRVk9JFNFUlZJQ0VfUkVBTEJBU0U2NF82NF9QRU5QT1QKICAgICAgLSBQRU5QT1RfUFVCTElDX1VSST0kU0VSVklDRV9VUkxfRlJPTlRFTkRfODA4MAogICAgICAtICdQRU5QT1RfQkFDS0VORF9VUkk9aHR0cDovL3BlbnBvdC1iYWNrZW5kJwogICAgICAtICdQRU5QT1RfRVhQT1JURVJfVVJJPWh0dHA6Ly9wZW5wb3QtZXhwb3J0ZXInCiAgICAgIC0gJ1BFTlBPVF9EQVRBQkFTRV9VUkk9cG9zdGdyZXNxbDovL3Bvc3RncmVzLyR7UE9TVEdSRVNfREI6LXBlbnBvdH0nCiAgICAgIC0gJ1BFTlBPVF9EQVRBQkFTRV9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BFTlBPVF9EQVRBQkFTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQRU5QT1RfUkVESVNfVVJJPXJlZGlzOi8vcGVucG90LXZhbGtleS8wJwogICAgICAtIFBFTlBPVF9PQkpFQ1RTX1NUT1JBR0VfQkFDS0VORD1mcwogICAgICAtIFBFTlBPVF9PQkpFQ1RTX1NUT1JBR0VfRlNfRElSRUNUT1JZPS9vcHQvZGF0YS9hc3NldHMKICAgICAgLSAnUEVOUE9UX1RFTEVNRVRSWV9FTkFCTEVEPSR7UEVOUE9UX1RFTEVNRVRSWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0RFRkFVTFRfRlJPTT0ke1BFTlBPVF9TTVRQX0RFRkFVTFRfRlJPTTotbm8tcmVwbHlAZXhhbXBsZS5jb219JwogICAgICAtICdQRU5QT1RfU01UUF9ERUZBVUxUX1JFUExZX1RPPSR7UEVOUE9UX1NNVFBfREVGQVVMVF9SRVBMWV9UTzotbm8tcmVwbHlAZXhhbXBsZS5jb219JwogICAgICAtICdQRU5QT1RfU01UUF9IT1NUPSR7UEVOUE9UX1NNVFBfSE9TVDotbWFpbHBpdH0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1BPUlQ9JHtQRU5QT1RfU01UUF9QT1JUOi0xMDI1fScKICAgICAgLSAnUEVOUE9UX1NNVFBfVVNFUk5BTUU9JHtQRU5QT1RfU01UUF9VU0VSTkFNRTotcGVucG90fScKICAgICAgLSAnUEVOUE9UX1NNVFBfUEFTU1dPUkQ9JHtQRU5QT1RfU01UUF9QQVNTV09SRDotcGVucG90fScKICAgICAgLSAnUEVOUE9UX1NNVFBfVExTPSR7UEVOUE9UX1NNVFBfVExTOi1mYWxzZX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1NTTD0ke1BFTlBPVF9TTVRQX1NTTDotZmFsc2V9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG5vZGUKICAgICAgICAtICctZScKICAgICAgICAtICJyZXF1aXJlKCdodHRwJykuZ2V0KHtob3N0OicxMjcuMC4wLjEnLCBwb3J0OjYwNjAsIHBhdGg6Jy9yZWFkeXonfSwgcmVzID0+IHByb2Nlc3MuZXhpdChyZXMuc3RhdHVzQ29kZT09PTIwMCA/IDAgOiAxKSkub24oJ2Vycm9yJywgKCkgPT4gcHJvY2Vzcy5leGl0KDEpKTsiCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMzBzCiAgICAgIHJldHJpZXM6IDE1CiAgcGVucG90LWV4cG9ydGVyOgogICAgaW1hZ2U6ICdwZW5wb3RhcHAvZXhwb3J0ZXI6Mi4xMS4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUEVOUE9UX1BVQkxJQ19VUkk9JFNFUlZJQ0VfVVJMX0ZST05URU5EXzgwODAKICAgICAgLSAnUEVOUE9UX1JFRElTX1VSST1yZWRpczovL3BlbnBvdC12YWxrZXkvMCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2MDYxL3JlYWR5eicKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIG1haWxwaXQ6CiAgICBpbWFnZTogJ2F4bGxlbnQvbWFpbHBpdDp2MS4yOCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01BSUxQSVRfODAyNQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9tYWlscGl0CiAgICAgICAgLSByZWFkeXoKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BlbnBvdC1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfSU5JVERCX0FSR1M9LS1kYXRhLWNoZWNrc3VtcwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXBlbnBvdH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcGVucG90LXZhbGtleToKICAgIGltYWdlOiAndmFsa2V5L3ZhbGtleTo4LjEnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtdmFsa2V5LWRhdGE6L2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnVkFMS0VZX0VYVFJBX0ZMQUdTPS0tbWF4bWVtb3J5IDEyOG1iIC0tbWF4bWVtb3J5LXBvbGljeSB2b2xhdGlsZS1sZnUnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3ZhbGtleS1jbGkgcGluZyB8IGdyZXAgUE9ORycKICAgICAgaW50ZXJ2YWw6IDFzCiAgICAgIHRpbWVvdXQLine truncated
|
||||
"tags": [
|
||||
"penpot",
|
||||
"design",
|
||||
@@ -3469,7 +3501,7 @@
|
||||
"postiz": {
|
||||
"documentation": "https://docs.postiz.com?utm_source=coolify.io",
|
||||
"slogan": "Open source social media scheduling tool.",
|
||||
"compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOnYxLjYwLjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9QT1NUSVpfNTAwMAogICAgICAtICdNQUlOX1VSTD0ke1NFUlZJQ0VfVVJMX1BPU1RJWn0nCiAgICAgIC0gJ0ZST05URU5EX1VSTD0ke1NFUlZJQ0VfVVJMX1BPU1RJWn0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0JBQ0tFTkRfVVJMPSR7U0VSVklDRV9VUkxfUE9TVElafS9hcGknCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVFNFQ1JFVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1wb3N0aXotZGJ9JwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OScKICAgICAgLSAnQkFDS0VORF9JTlRFUk5BTF9VUkw9aHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICAtICdDTE9VREZMQVJFX0FDQ09VTlRfSUQ9JHtDTE9VREZMQVJFX0FDQ09VTlRfSUR9JwogICAgICAtICdDTE9VREZMQVJFX0FDQ0VTU19LRVk9JHtDTE9VREZMQVJFX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUTkFNRT0ke0NMT1VERkxBUkVfQlVDS0VUTkFNRX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUX1VSTD0ke0NMT1VERkxBUkVfQlVDS0VUX1VSTH0nCiAgICAgIC0gJ0NMT1VERkxBUkVfUkVHSU9OPSR7Q0xPVURGTEFSRV9SRUdJT059JwogICAgICAtICdTVE9SQUdFX1BST1ZJREVSPSR7U1RPUkFHRV9QUk9WSURFUjotbG9jYWx9JwogICAgICAtICdVUExPQURfRElSRUNUT1JZPSR7VVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfRElSRUNUT1JZPSR7TkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9TVEFUSUNfRElSRUNUT1JZfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fTkFNRT0ke0VNQUlMX0ZST01fTkFNRX0nCiAgICAgIC0gJ0VNQUlMX1BST1ZJREVSPSR7RU1BSUxfUFJPVklERVJ9JwogICAgICAtICdYX0FQSV9LRVk9JHtTRVJWSUNFX1hfQVBJfScKICAgICAgLSAnWF9BUElfU0VDUkVUPSR7U0VSVklDRV9YX1NFQ1JFVH0nCiAgICAgIC0gJ0xJTktFRElOX0NMSUVOVF9JRD0ke1NFUlZJQ0VfTElOS0VESU5fSUR9JwogICAgICAtICdMSU5LRURJTl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9MSU5LRURJTl9TRUNSRVR9JwogICAgICAtICdSRURESVRfQ0xJRU5UX0lEPSR7U0VSVklDRV9SRURESVRfQVBJfScKICAgICAgLSAnUkVERElUX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1JFRERJVF9TRUNSRVR9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX0lEPSR7U0VSVklDRV9HSVRIVUJfSUR9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfR0lUSFVCX1NFQ1JFVH0nCiAgICAgIC0gJ1RIUkVBRFNfQVBQX0lEPSR7U0VSVklDRV9USFJFQURTX0lEfScKICAgICAgLSAnVEhSRUFEU19BUFBfU0VDUkVUPSR7U0VSVklDRV9USFJFQURTX1NFQ1JFVH0nCiAgICAgIC0gJ0ZBQ0VCT09LX0FQUF9JRD0ke1NFUlZJQ0VfRkFDRUJPT0tfSUR9JwogICAgICAtICdGQUNFQk9PS19BUFBfU0VDUkVUPSR7U0VSVklDRV9GQUNFQk9PS19TRUNSRVR9JwogICAgICAtICdZT1VUVUJFX0NMSUVOVF9JRD0ke1NFUlZJQ0VfWU9VVFVCRV9JRH0nCiAgICAgIC0gJ1lPVVRVQkVfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfWU9VVFVCRV9TRUNSRVR9JwogICAgICAtICdUSUtUT0tfQ0xJRU5UX0lEPSR7U0VSVklDRV9USUtUT0tfSUR9JwogICAgICAtICdUSUtUT0tfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfVElLVE9LX1NFQ1JFVH0nCiAgICAgIC0gJ1BJTlRFUkVTVF9DTElFTlRfSUQ9JHtTRVJWSUNFX1BJTlRFUkVTVF9JRH0nCiAgICAgIC0gJ1BJTlRFUkVTVF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9QSU5URVJFU1RfU0VDUkVUfScKICAgICAgLSAnRFJJQkJCTEVfQ0xJRU5UX0lEPSR7U0VSVklDRV9EUklCQkxFX0lEfScKICAgICAgLSAnRFJJQkJCTEVfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfRFJJQkJMRV9TRUNSRVR9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9JRD0ke1NFUlZJQ0VfRElTQ09SRF9JRH0nCiAgICAgIC0gJ0RJU0NPUkRfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfRElTQ09SRF9TRUNSRVR9JwogICAgICAtICdESVNDT1JEX0JPVF9UT0tFTl9JRD0ke1NFUlZJQ0VfRElTQ09SRF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0lEPSR7U0VSVklDRV9TTEFDS19JRH0nCiAgICAgIC0gJ1NMQUNLX1NFQ1JFVD0ke1NFUlZJQ0VfU0xBQ0tfU0VDUkVUfScKICAgICAgLSAnU0xBQ0tfU0lHTklOR19TRUNSRVQ9JHtTTEFDS19TSUdOSU5HX1NFQ1JFVH0nCiAgICAgIC0gJ01BU1RPRE9OX0NMSUVOVF9JRD0ke1NFUlZJQ0VfTUFTVE9ET05fSUR9JwogICAgICAtICdNQVNUT0RPTl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9NQVNUT0RPTl9TRUNSRVR9JwogICAgICAtICdCRUVISUlWRV9BUElfS0VZPSR7U0VSVklDRV9CRUVISUlWRV9LRVl9JwogICAgICAtICdCRUVISUlWRV9QVUJMSUNBVElPTl9JRD0ke1NFUlZJQ0VfQkVFSElJVkVfUFVCSUR9JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke1NFUlZJQ0VfT1BFTkFJX0tFWX0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0NPUkRfU1VQUE9SVD0ke05FWFRfUFVCTElDX0RJU0NPUkRfU1VQUE9SVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX1BPTE9UTk89JHtORVhUX1BVQkxJQ19QT0xPVE5PfScKICAgICAgLSBJU19HRU5FUkFMPXRydWUKICAgICAgLSAnTlhfQUREX1BMVUdJTlM9JHtOWF9BRERfUExVR0lOUzotZmFsc2V9JwogICAgICAtICdOT1RfU0VDVVJFRD0ke05PVF9TRUNVUkVEOi1mYWxzZX0nCiAgICAgIC0gJ0ZFRV9BTU9VTlQ9JHtGRUVfQU1PVU5UOi0wLjA1fScKICAgICAgLSAnU1RSSVBFX1BVQkxJU0hBQkxFX0tFWT0ke1NUUklQRV9QVUJMSVNIQUJMRV9LRVl9JwogICAgICAtICdTVFJJUEVfU0VDUkVUX0tFWT0ke1NUUklQRV9TRUNSRVRfS0VZfScKICAgICAgLSAnU1RSSVBFX1NJR05JTkdfS0VZPSR7U1RSSVBFX1NJR05JTkdfS0VZfScKICAgICAgLSAnU1RSSVBFX1NJR05JTkdfS0VZX0NPTk5FQ1Q9JHtTVFJJUEVfU0lHTklOR19LRVlfQ09OTkVDVH0nCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0aXpfY29uZmlnOi9jb25maWcvJwogICAgICAtICdwb3N0aXpfdXBsb2FkczovdXBsb2Fkcy8nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICALine truncated
|
||||
"compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOnYyLjEwLjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9QT1NUSVpfNTAwMAogICAgICAtICdNQUlOX1VSTD0ke1NFUlZJQ0VfVVJMX1BPU1RJWn0nCiAgICAgIC0gJ0ZST05URU5EX1VSTD0ke1NFUlZJQ0VfVVJMX1BPU1RJWn0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0JBQ0tFTkRfVVJMPSR7U0VSVklDRV9VUkxfUE9TVElafS9hcGknCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVFNFQ1JFVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1wb3N0aXotZGJ9JwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OScKICAgICAgLSAnQkFDS0VORF9JTlRFUk5BTF9VUkw9aHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICAtICdDTE9VREZMQVJFX0FDQ09VTlRfSUQ9JHtDTE9VREZMQVJFX0FDQ09VTlRfSUR9JwogICAgICAtICdDTE9VREZMQVJFX0FDQ0VTU19LRVk9JHtDTE9VREZMQVJFX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUTkFNRT0ke0NMT1VERkxBUkVfQlVDS0VUTkFNRX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUX1VSTD0ke0NMT1VERkxBUkVfQlVDS0VUX1VSTH0nCiAgICAgIC0gJ0NMT1VERkxBUkVfUkVHSU9OPSR7Q0xPVURGTEFSRV9SRUdJT059JwogICAgICAtICdTVE9SQUdFX1BST1ZJREVSPSR7U1RPUkFHRV9QUk9WSURFUjotbG9jYWx9JwogICAgICAtICdVUExPQURfRElSRUNUT1JZPSR7VVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfRElSRUNUT1JZPSR7TkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9TVEFUSUNfRElSRUNUT1JZfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fTkFNRT0ke0VNQUlMX0ZST01fTkFNRX0nCiAgICAgIC0gJ0VNQUlMX1BST1ZJREVSPSR7RU1BSUxfUFJPVklERVJ9JwogICAgICAtICdYX0FQSV9LRVk9JHtTRVJWSUNFX1hfQVBJfScKICAgICAgLSAnWF9BUElfU0VDUkVUPSR7U0VSVklDRV9YX1NFQ1JFVH0nCiAgICAgIC0gJ0xJTktFRElOX0NMSUVOVF9JRD0ke1NFUlZJQ0VfTElOS0VESU5fSUR9JwogICAgICAtICdMSU5LRURJTl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9MSU5LRURJTl9TRUNSRVR9JwogICAgICAtICdSRURESVRfQ0xJRU5UX0lEPSR7U0VSVklDRV9SRURESVRfQVBJfScKICAgICAgLSAnUkVERElUX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1JFRERJVF9TRUNSRVR9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX0lEPSR7U0VSVklDRV9HSVRIVUJfSUR9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfR0lUSFVCX1NFQ1JFVH0nCiAgICAgIC0gJ1RIUkVBRFNfQVBQX0lEPSR7U0VSVklDRV9USFJFQURTX0lEfScKICAgICAgLSAnVEhSRUFEU19BUFBfU0VDUkVUPSR7U0VSVklDRV9USFJFQURTX1NFQ1JFVH0nCiAgICAgIC0gJ0ZBQ0VCT09LX0FQUF9JRD0ke1NFUlZJQ0VfRkFDRUJPT0tfSUR9JwogICAgICAtICdGQUNFQk9PS19BUFBfU0VDUkVUPSR7U0VSVklDRV9GQUNFQk9PS19TRUNSRVR9JwogICAgICAtICdZT1VUVUJFX0NMSUVOVF9JRD0ke1NFUlZJQ0VfWU9VVFVCRV9JRH0nCiAgICAgIC0gJ1lPVVRVQkVfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfWU9VVFVCRV9TRUNSRVR9JwogICAgICAtICdUSUtUT0tfQ0xJRU5UX0lEPSR7U0VSVklDRV9USUtUT0tfSUR9JwogICAgICAtICdUSUtUT0tfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfVElLVE9LX1NFQ1JFVH0nCiAgICAgIC0gJ1BJTlRFUkVTVF9DTElFTlRfSUQ9JHtTRVJWSUNFX1BJTlRFUkVTVF9JRH0nCiAgICAgIC0gJ1BJTlRFUkVTVF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9QSU5URVJFU1RfU0VDUkVUfScKICAgICAgLSAnRFJJQkJCTEVfQ0xJRU5UX0lEPSR7U0VSVklDRV9EUklCQkxFX0lEfScKICAgICAgLSAnRFJJQkJCTEVfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfRFJJQkJMRV9TRUNSRVR9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9JRD0ke1NFUlZJQ0VfRElTQ09SRF9JRH0nCiAgICAgIC0gJ0RJU0NPUkRfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfRElTQ09SRF9TRUNSRVR9JwogICAgICAtICdESVNDT1JEX0JPVF9UT0tFTl9JRD0ke1NFUlZJQ0VfRElTQ09SRF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0lEPSR7U0VSVklDRV9TTEFDS19JRH0nCiAgICAgIC0gJ1NMQUNLX1NFQ1JFVD0ke1NFUlZJQ0VfU0xBQ0tfU0VDUkVUfScKICAgICAgLSAnU0xBQ0tfU0lHTklOR19TRUNSRVQ9JHtTTEFDS19TSUdOSU5HX1NFQ1JFVH0nCiAgICAgIC0gJ01BU1RPRE9OX0NMSUVOVF9JRD0ke1NFUlZJQ0VfTUFTVE9ET05fSUR9JwogICAgICAtICdNQVNUT0RPTl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9NQVNUT0RPTl9TRUNSRVR9JwogICAgICAtICdCRUVISUlWRV9BUElfS0VZPSR7U0VSVklDRV9CRUVISUlWRV9LRVl9JwogICAgICAtICdCRUVISUlWRV9QVUJMSUNBVElPTl9JRD0ke1NFUlZJQ0VfQkVFSElJVkVfUFVCSUR9JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke1NFUlZJQ0VfT1BFTkFJX0tFWX0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0NPUkRfU1VQUE9SVD0ke05FWFRfUFVCTElDX0RJU0NPUkRfU1VQUE9SVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX1BPTE9UTk89JHtORVhUX1BVQkxJQ19QT0xPVE5PfScKICAgICAgLSBJU19HRU5FUkFMPXRydWUKICAgICAgLSAnTlhfQUREX1BMVUdJTlM9JHtOWF9BRERfUExVR0lOUzotZmFsc2V9JwogICAgICAtICdOT1RfU0VDVVJFRD0ke05PVF9TRUNVUkVEOi1mYWxzZX0nCiAgICAgIC0gJ0ZFRV9BTU9VTlQ9JHtGRUVfQU1PVU5UOi0wLjA1fScKICAgICAgLSAnU1RSSVBFX1BVQkxJU0hBQkxFX0tFWT0ke1NUUklQRV9QVUJMSVNIQUJMRV9LRVl9JwogICAgICAtICdTVFJJUEVfU0VDUkVUX0tFWT0ke1NUUklQRV9TRUNSRVRfS0VZfScKICAgICAgLSAnU1RSSVBFX1NJR05JTkdfS0VZPSR7U1RSSVBFX1NJR05JTkdfS0VZfScKICAgICAgLSAnU1RSSVBFX1NJR05JTkdfS0VZX0NPTk5FQ1Q9JHtTVFJJUEVfU0lHTklOR19LRVlfQ09OTkVDVH0nCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0aXpfY29uZmlnOi9jb25maWcvJwogICAgICAtICdwb3N0aXpfdXBsb2FkczovdXBsb2Fkcy8nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICALine truncated
|
||||
"tags": [
|
||||
"post everywhere",
|
||||
"social media",
|
||||
@@ -3564,7 +3596,7 @@
|
||||
"pterodactyl-with-wings": {
|
||||
"documentation": "https://pterodactyl.io/?utm_source=coolify.io",
|
||||
"slogan": "Pterodactyl is a free, open-source game server management panel",
|
||||
"compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMS4xMScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhbmVsLXZhcjovYXBwL3Zhci8nCiAgICAgIC0gJ3BhbmVsLW5naW54Oi9ldGMvbmdpbngvaHR0cC5kLycKICAgICAgLSAncGFuZWwtY2VydHM6L2V0Yy9sZXRzZW5jcnlwdC8nCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2V0Yy9lbnRyeXBvaW50LnNoCiAgICAgICAgdGFyZ2V0OiAvZW50cnlwb2ludC5zaAogICAgICAgIG1vZGU6ICcwNzU1JwogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vc2hcbnNldCAtZVxuXG4gZWNobyBcIlNldHRpbmcgbG9ncyBwZXJtaXNzaW9ucy4uLlwiXG4gY2hvd24gLVIgbmdpbng6IC9hcHAvc3RvcmFnZS9sb2dzL1xuXG4gVVNFUl9FWElTVFM9JChwaHAgYXJ0aXNhbiB0aW5rZXIgLS1uby1hbnNpIC0tZXhlY3V0ZT0nZWNobyBcXFB0ZXJvZGFjdHlsXFxNb2RlbHNcXFVzZXI6OndoZXJlKFwiZW1haWxcIiwgXCInXCIkQURNSU5fRU1BSUxcIidcIiktPmV4aXN0cygpID8gXCIxXCIgOiBcIjBcIjsnKVxuXG4gaWYgWyBcIiRVU0VSX0VYSVNUU1wiID0gXCIwXCIgXTsgdGhlblxuICAgZWNobyBcIkFkbWluIFVzZXIgZG9lcyBub3QgZXhpc3QsIGNyZWF0aW5nIHVzZXIgbm93LlwiXG4gICBwaHAgYXJ0aXNhbiBwOnVzZXI6bWFrZSAtLW5vLWludGVyYWN0aW9uIFxcXG4gICAgIC0tYWRtaW49MSBcXFxuICAgICAtLWVtYWlsPVwiJEFETUlOX0VNQUlMXCIgXFxcbiAgICAgLS11c2VybmFtZT1cIiRBRE1JTl9VU0VSTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1maXJzdD1cIiRBRE1JTl9GSVJTVE5BTUVcIiBcXFxuICAgICAtLW5hbWUtbGFzdD1cIiRBRE1JTl9MQVNUTkFNRVwiIFxcXG4gICAgIC0tcGFzc3dvcmQ9XCIkQURNSU5fUEFTU1dPUkRcIlxuICAgZWNobyBcIkFkbWluIHVzZXIgY3JlYXRlZCBzdWNjZXNzZnVsbHkhXCJcbiBlbHNlXG4gICBlY2hvIFwiQWRtaW4gVXNlciBhbHJlYWR5IGV4aXN0cywgc2tpcHBpbmcgY3JlYXRpb24uXCJcbiBmaVxuXG4gZXhlYyBzdXBlcnZpc29yZCAtLW5vZGFlbW9uXG4iCiAgICBjb21tYW5kOgogICAgICAtIC9lbnRyeXBvaW50LnNoCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ2N1cmwgLXNmIGh0dHA6Ly9sb2NhbGhvc3Q6ODAgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIEhBU0hJRFNfU0FMVD0kU0VSVklDRV9QQVNTV09SRF9IQVNISURTCiAgICAgIC0gSEFTSElEU19MRU5HVEg9OAogICAgICAtIFNFUlZJQ0VfVVJMX1BURVJPREFDVFlMXzgwCiAgICAgIC0gJ0FETUlOX0VNQUlMPSR7QURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfQURNSU59JwogICAgICAtICdBRE1JTl9GSVJTVE5BTUU9JHtBRE1JTl9GSVJTVE5BTUU6LUFkbWlufScKICAgICAgLSAnQURNSU5fTEFTVE5BTUU9JHtBRE1JTl9MQVNUTkFNRTotVXNlcn0nCiAgICAgIC0gJ0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICAgIC0gJ1BURVJPREFDVFlMX0hUVFBTPSR7UFRFUk9EQUNUWUxfSFRUUFM6LWZhbHNlfScKICAgICAgLSBBUFBfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBBUFBfRU5WSVJPTk1FTlRfT05MWT1mYWxzZQogICAgICAtIEFQUF9VUkw9JFNFUlZJQ0VfVVJMX1BURVJPREFDVFlMCiAgICAgIC0gJ0FQUF9USU1FWk9ORT0ke1RJTUVaT05FOi1VVEN9JwogICAgICAtICdBUFBfU0VSVklDRV9BVVRIT1I9JHtBUFBfU0VSVklDRV9BVVRIT1I6LWF1dGhvckBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0xPR19MRVZFTD0ke0xPR19MRVZFTDotZGVidWd9JwogICAgICAtIENBQ0hFX0RSSVZFUj1yZWRpcwogICAgICAtIFNFU1NJT05fRFJJVkVSPXJlZGlzCiAgICAgIC0gUVVFVUVfRFJJVkVSPXJlZGlzCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIERCX0RBVEFCQVNFPXB0ZXJvZGFjdHlsLWRiCiAgICAgIC0gREJfVVNFUk5BTUU9JFNFUlZJQ0VfVVNFUl9NWVNRTAogICAgICAtIERCX0hPU1Q9bWFyaWFkYgogICAgICAtIERCX1BPUlQ9MzMwNgogICAgICAtIERCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIC0gTUFJTF9GUk9NPSRNQUlMX0ZST00KICAgICAgLSBNQUlMX0RSSVZFUj0kTUFJTF9EUklWRVIKICAgICAgLSBNQUlMX0hPU1Q9JE1BSUxfSE9TVAogICAgICAtIE1BSUxfUE9SVD0kTUFJTF9QT1JUCiAgICAgIC0gTUFJTF9VU0VSTkFNRT0kTUFJTF9VU0VSTkFNRQogICAgICAtIE1BSUxfUEFTU1dPUkQ9JE1BSUxfUEFTU1dPUkQKICAgICAgLSBNQUlMX0VOQ1JZUFRJT049JE1BSUxfRU5DUllQVElPTgogIHdpbmdzOgogICAgaW1hZ2U6ICdnaGNyLmlvL3B0ZXJvZGFjdHlsL3dpbmdzOnYxLjExLjEzJwogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1dJTkdTXzg0NDMKICAgICAgLSAnVFo9JHtUSU1FWk9ORTotVVRDfScKICAgICAgLSBXSU5HU19VU0VSTkFNRT1wdGVyb2RhY3R5bAogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJy92YXIvbGliL2RvY2tlci9jb250YWluZXJzLzovdmFyL2xpYi9kb2NrZXIvY29udGFpbmVycy8nCiAgICAgIC0gJy92YXIvbGliL3B0ZXJvZGFjdHlsLzovdmFyL2xpYi9wdGVyb2RhY3R5bC8nCiAgICAgIC0gJy90bXAvcHRlcm9kYWN0eWwvOi90bXAvcHRlcm9kYWN0eWwvJwogICAgICAtICd3aW5ncy1sb2dzOi92YXIvbG9nL3B0ZXJvZGFjdHlsLycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2NvbmZpZy55bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvcHRlcm9kYWN0eWwvY29uZmlnLnltbAogICAgICAgIGNLine truncated
|
||||
"compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMS4xMScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhbmVsLXZhcjovYXBwL3Zhci8nCiAgICAgIC0gJ3BhbmVsLW5naW54Oi9ldGMvbmdpbngvaHR0cC5kLycKICAgICAgLSAncGFuZWwtY2VydHM6L2V0Yy9sZXRzZW5jcnlwdC8nCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2V0Yy9lbnRyeXBvaW50LnNoCiAgICAgICAgdGFyZ2V0OiAvZW50cnlwb2ludC5zaAogICAgICAgIG1vZGU6ICcwNzU1JwogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vc2hcbnNldCAtZVxuXG4gZWNobyBcIlNldHRpbmcgbG9ncyBwZXJtaXNzaW9ucy4uLlwiXG4gY2hvd24gLVIgbmdpbng6IC9hcHAvc3RvcmFnZS9sb2dzL1xuXG4gVVNFUl9FWElTVFM9JChwaHAgYXJ0aXNhbiB0aW5rZXIgLS1uby1hbnNpIC0tZXhlY3V0ZT0nZWNobyBcXFB0ZXJvZGFjdHlsXFxNb2RlbHNcXFVzZXI6OndoZXJlKFwiZW1haWxcIiwgXCInXCIkQURNSU5fRU1BSUxcIidcIiktPmV4aXN0cygpID8gXCIxXCIgOiBcIjBcIjsnKVxuXG4gaWYgWyBcIiRVU0VSX0VYSVNUU1wiID0gXCIwXCIgXTsgdGhlblxuICAgZWNobyBcIkFkbWluIFVzZXIgZG9lcyBub3QgZXhpc3QsIGNyZWF0aW5nIHVzZXIgbm93LlwiXG4gICBwaHAgYXJ0aXNhbiBwOnVzZXI6bWFrZSAtLW5vLWludGVyYWN0aW9uIFxcXG4gICAgIC0tYWRtaW49MSBcXFxuICAgICAtLWVtYWlsPVwiJEFETUlOX0VNQUlMXCIgXFxcbiAgICAgLS11c2VybmFtZT1cIiRBRE1JTl9VU0VSTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1maXJzdD1cIiRBRE1JTl9GSVJTVE5BTUVcIiBcXFxuICAgICAtLW5hbWUtbGFzdD1cIiRBRE1JTl9MQVNUTkFNRVwiIFxcXG4gICAgIC0tcGFzc3dvcmQ9XCIkQURNSU5fUEFTU1dPUkRcIlxuICAgZWNobyBcIkFkbWluIHVzZXIgY3JlYXRlZCBzdWNjZXNzZnVsbHkhXCJcbiBlbHNlXG4gICBlY2hvIFwiQWRtaW4gVXNlciBhbHJlYWR5IGV4aXN0cywgc2tpcHBpbmcgY3JlYXRpb24uXCJcbiBmaVxuXG4gZXhlYyBzdXBlcnZpc29yZCAtLW5vZGFlbW9uXG4iCiAgICBjb21tYW5kOgogICAgICAtIC9lbnRyeXBvaW50LnNoCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ2N1cmwgLXNmIGh0dHA6Ly9sb2NhbGhvc3Q6ODAgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIEhBU0hJRFNfU0FMVD0kU0VSVklDRV9QQVNTV09SRF9IQVNISURTCiAgICAgIC0gSEFTSElEU19MRU5HVEg9OAogICAgICAtIFNFUlZJQ0VfVVJMX1BURVJPREFDVFlMXzgwCiAgICAgIC0gJ0FETUlOX0VNQUlMPSR7QURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfQURNSU59JwogICAgICAtICdBRE1JTl9GSVJTVE5BTUU9JHtBRE1JTl9GSVJTVE5BTUU6LUFkbWlufScKICAgICAgLSAnQURNSU5fTEFTVE5BTUU9JHtBRE1JTl9MQVNUTkFNRTotVXNlcn0nCiAgICAgIC0gJ0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICAgIC0gJ1BURVJPREFDVFlMX0hUVFBTPSR7UFRFUk9EQUNUWUxfSFRUUFM6LWZhbHNlfScKICAgICAgLSBBUFBfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBBUFBfRU5WSVJPTk1FTlRfT05MWT1mYWxzZQogICAgICAtIEFQUF9VUkw9JFNFUlZJQ0VfVVJMX1BURVJPREFDVFlMCiAgICAgIC0gJ0FQUF9USU1FWk9ORT0ke1RJTUVaT05FOi1VVEN9JwogICAgICAtICdBUFBfU0VSVklDRV9BVVRIT1I9JHtBUFBfU0VSVklDRV9BVVRIT1I6LWF1dGhvckBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0xPR19MRVZFTD0ke0xPR19MRVZFTDotZGVidWd9JwogICAgICAtIENBQ0hFX0RSSVZFUj1yZWRpcwogICAgICAtIFNFU1NJT05fRFJJVkVSPXJlZGlzCiAgICAgIC0gUVVFVUVfRFJJVkVSPXJlZGlzCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIERCX0RBVEFCQVNFPXB0ZXJvZGFjdHlsLWRiCiAgICAgIC0gREJfVVNFUk5BTUU9JFNFUlZJQ0VfVVNFUl9NWVNRTAogICAgICAtIERCX0hPU1Q9bWFyaWFkYgogICAgICAtIERCX1BPUlQ9MzMwNgogICAgICAtIERCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIC0gTUFJTF9GUk9NPSRNQUlMX0ZST00KICAgICAgLSBNQUlMX0RSSVZFUj0kTUFJTF9EUklWRVIKICAgICAgLSBNQUlMX0hPU1Q9JE1BSUxfSE9TVAogICAgICAtIE1BSUxfUE9SVD0kTUFJTF9QT1JUCiAgICAgIC0gTUFJTF9VU0VSTkFNRT0kTUFJTF9VU0VSTkFNRQogICAgICAtIE1BSUxfUEFTU1dPUkQ9JE1BSUxfUEFTU1dPUkQKICAgICAgLSBNQUlMX0VOQ1JZUFRJT049JE1BSUxfRU5DUllQVElPTgogIHdpbmdzOgogICAgaW1hZ2U6ICdnaGNyLmlvL3B0ZXJvZGFjdHlsL3dpbmdzOnYxLjExLjEzJwogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIHBvcnRzOgogICAgICAtICcyMDIyOjIwMjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9XSU5HU184NDQzCiAgICAgIC0gJ1RaPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gV0lOR1NfVVNFUk5BTUU9cHRlcm9kYWN0eWwKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgICAtICcvdmFyL2xpYi9kb2NrZXIvY29udGFpbmVycy86L3Zhci9saWIvZG9ja2VyL2NvbnRhaW5lcnMvJwogICAgICAtICcvdmFyL2xpYi9wdGVyb2RhY3R5bC86L3Zhci9saWIvcHRlcm9kYWN0eWwvJwogICAgICAtICcvdG1wL3B0ZXJvZGFjdHlsLzovdG1wL3B0ZXJvZGFjdHlsLycKICAgICAgLSAnd2luZ3MtbG9nczovdmFyL2xvZy9wdGVyb2RhY3R5bC8nCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2V0Yy9jb25maWcueW1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL3BLine truncated
|
||||
"tags": [
|
||||
"game",
|
||||
"game server",
|
||||
@@ -4156,6 +4188,18 @@
|
||||
"minversion": "0.0.0",
|
||||
"port": "3000"
|
||||
},
|
||||
"terraria-server": {
|
||||
"documentation": "https://github.com/hexlo/terraria-server-docker?utm_source=coolify.io",
|
||||
"slogan": "Docker multi-arch Image for Terraria Server.",
|
||||
"compose": "c2VydmljZXM6CiAgdGVycmFyaWEtc2VydmVyOgogICAgaW1hZ2U6ICdoZXhsby90ZXJyYXJpYS1zZXJ2ZXItZG9ja2VyOjEuNC40LjknCiAgICBzdGRpbl9vcGVuOiB0cnVlCiAgICB0dHk6IHRydWUKICAgIHBvcnRzOgogICAgICAtICc3Nzc3Ojc3NzcnCiAgICB2b2x1bWVzOgogICAgICAtICd0ZXJyYXJpYS1zZXJ2ZXI6L3Jvb3QvLmxvY2FsL3NoYXJlL1RlcnJhcmlhL1dvcmxkcycKICAgIGVudmlyb25tZW50OgogICAgICAtICdXT1JMRD0ke1dPUkxEOi0vcm9vdC8ubG9jYWwvc2hhcmUvVGVycmFyaWEvV29ybGRzL3dvcmxkMS53bGR9JwogICAgICAtICdBVVRPQ1JFQVRFPSR7QVVUT0NSRUFURTotMn0nCiAgICAgIC0gJ1dPUkxETkFNRT0ke1dPUkxETkFNRTotd29ybGQxfScKICAgICAgLSAnRElGRklDVUxUWT0ke0RJRkZJQ1VMVFk6LTF9JwogICAgICAtICdNQVhQTEFZRVJTPSR7TUFYUExBWUVSUzotOH0nCiAgICAgIC0gJ1BBU1NXT1JEPSR7UEFTU1dPUkQ6LW15cGFzc3dvcmR9JwogICAgICAtICdNT1REPSR7TU9URDotV2VsY29tZSB0byB0aGUgc2VydmVyIX0nCiAgICAgIC0gJ0xBTkdVQUdFPSR7TEFOR1VBR0U6LWVuL1VTfScKICAgICAgLSAnU0VDVVJFPSR7U0VDVVJFOi0xfScK",
|
||||
"tags": [
|
||||
"terraria"
|
||||
],
|
||||
"category": "games",
|
||||
"logo": "svgs/terraria.svg",
|
||||
"minversion": "0.0.0",
|
||||
"port": "7777"
|
||||
},
|
||||
"tolgee": {
|
||||
"documentation": "https://tolgee.io/?utm_source=coolify.io",
|
||||
"slogan": "Tolgee is a localization management platform for developers and translators.",
|
||||
@@ -4272,7 +4316,7 @@
|
||||
"umami": {
|
||||
"documentation": "https://umami.is?utm_source=coolify.io",
|
||||
"slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.",
|
||||
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6cG9zdGdyZXNxbC1sYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
|
||||
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
|
||||
"tags": [
|
||||
"analytics",
|
||||
"insights",
|
||||
|
||||
@@ -186,10 +186,25 @@
|
||||
"logo": "svgs/babybuddy.png",
|
||||
"minversion": "0.0.0"
|
||||
},
|
||||
"beszel-agent": {
|
||||
"documentation": "https://www.beszel.dev/guide/agent-installation?utm_source=coolify.io",
|
||||
"slogan": "Monitoring agent for Beszel",
|
||||
"compose": "c2VydmljZXM6CiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNURU49L2Jlc3plbF9zb2NrZXQvYmVzemVsLnNvY2sKICAgICAgLSAnSFVCX1VSTD0ke0hVQl9VUkw/fScKICAgICAgLSAnVE9LRU49JHtUT0tFTj99JwogICAgICAtICdLRVk9JHtLRVk/fScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCg==",
|
||||
"tags": [
|
||||
"beszel",
|
||||
"monitoring",
|
||||
"server",
|
||||
"stats",
|
||||
"alerts"
|
||||
],
|
||||
"category": "monitoring",
|
||||
"logo": "svgs/beszel.svg",
|
||||
"minversion": "0.0.0"
|
||||
},
|
||||
"beszel": {
|
||||
"documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io",
|
||||
"slogan": "A lightweight server resource monitoring hub with historical data, docker stats, and alerts.",
|
||||
"compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE1LjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkVTWkVMXzgwOTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9kYXRhOi9iZXN6ZWxfZGF0YScKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICBiZXN6ZWwtYWdlbnQ6CiAgICBpbWFnZTogJ2hlbnJ5Z2QvYmVzemVsLWFnZW50OjAuMTUuMicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNURU49L2Jlc3plbF9zb2NrZXQvYmVzemVsLnNvY2sKICAgICAgLSAnSFVCX1VSTD1odHRwOi8vYmVzemVsOjgwOTAnCiAgICAgIC0gJ1RPS0VOPSR7VE9LRU59JwogICAgICAtICdLRVk9JHtLRVl9Jwo=",
|
||||
"compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkVTWkVMXzgwOTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9kYXRhOi9iZXN6ZWxfZGF0YScKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICBiZXN6ZWwtYWdlbnQ6CiAgICBpbWFnZTogJ2hlbnJ5Z2QvYmVzemVsLWFnZW50OjAuMTYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RFTj0vYmVzemVsX3NvY2tldC9iZXN6ZWwuc29jawogICAgICAtICdIVUJfVVJMPWh0dHA6Ly9iZXN6ZWw6ODA5MCcKICAgICAgLSAnVE9LRU49JHtUT0tFTn0nCiAgICAgIC0gJ0tFWT0ke0tFWX0nCiAgICB2b2x1bWVzOgogICAgICAtICdiZXN6ZWxfYWdlbnRfZGF0YTovdmFyL2xpYi9iZXN6ZWwtYWdlbnQnCiAgICAgIC0gJ2Jlc3plbF9zb2NrZXQ6L2Jlc3plbF9zb2NrZXQnCiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrOnJvJwo=",
|
||||
"tags": [
|
||||
"beszel",
|
||||
"monitoring",
|
||||
@@ -2227,7 +2242,7 @@
|
||||
"litellm": {
|
||||
"documentation": "https://docs.litellm.ai?utm_source=coolify.io",
|
||||
"slogan": "Call all LLM APIs using the OpenAI format. Use Bedrock, Azure, OpenAI, Cohere, Anthropic, Ollama, Sagemaker, HuggingFace, Replicate, Groq (100+ LLMs)",
|
||||
"compose": "c2VydmljZXM6CiAgbGl0ZWxsbToKICAgIGltYWdlOiAnZ2hjci5pby9iZXJyaWFpL2xpdGVsbG0tZGF0YWJhc2U6bWFpbi1zdGFibGUnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0xJVEVMTE1fNDAwMAogICAgICAtICdMSVRFTExNX0xPRz0ke0xJVEVMTE1fTE9HOi1FUlJPUn0nCiAgICAgIC0gJ0xJVEVMTE1fTU9ERT0ke0xJVEVMTE1fTU9ERTotUFJPRFVDVElPTn0nCiAgICAgIC0gJ0xJVEVMTE1fTUFTVEVSX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFTVEVSS0VZfScKICAgICAgLSAnVUlfVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfVUl9JwogICAgICAtICdVSV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfVUl9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGl0ZWxsbX0nCiAgICAgIC0gJ1JFRElTX0hPU1Q9JHtSRURJU19IT1NUOi1yZWRpc30nCiAgICAgIC0gJ1JFRElTX1BPUlQ9JHtSRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWxpdGVsbG19JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke09QRU5BSV9BUElfS0VZfScKICAgICAgLSAnT1BFTkFJX0FQSV9CQVNFPSR7T1BFTkFJX0FQSV9CQVNFfScKICAgICAgLSAnQU5USFJPUElDX0FQSV9LRVk9JHtBTlRIUk9QSUNfQVBJX0tFWX0nCiAgICAgIC0gJ0FOVEhST1BJQ19BUElfQkFTRT0ke0FOVEhST1BJQ19BUElfQkFTRX0nCiAgICAgIC0gJ1ZPWUFHRV9BUElfS0VZPSR7Vk9ZQUdFX0FQSV9LRVl9JwogICAgICAtICdWT1lBR0VfQVBJX0JBU0U9JHtWT1lBR0VfQVBJX0JBU0V9JwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vbGl0ZWxsbS1jb25maWcueWFtbAogICAgICAgIHRhcmdldDogL2FwcC9jb25maWcueWFtbAogICAgICAgIGNvbnRlbnQ6ICJnZW5lcmFsX3NldHRpbmdzOlxuICBwcm94eV9iYXRjaF93cml0ZV9hdDogNjBcblxucm91dGVyX3NldHRpbmdzOlxuICByZWRpc19ob3N0OiBvcy5lbnZpcm9uL1JFRElTX0hPU1RcbiAgcmVkaXNfcG9ydDogb3MuZW52aXJvbi9SRURJU19QT1JUXG4gIHJlZGlzX3Bhc3N3b3JkOiBvcy5lbnZpcm9uL1JFRElTX1BBU1NXT1JEXG4gIGVuYWJsZV9wcmVfY2FsbF9jaGVjazogdHJ1ZVxuXG5saXRlbGxtX3NldHRpbmdzOlxuICBzZXRfdmVyYm9zZTogZmFsc2VcbiAganNvbl9sb2dzOiB0cnVlXG4gIGxvZ19yYXdfcmVxdWVzdF9yZXNwb25zZTogdHJ1ZVxuICAjIHR1cm5fb2ZmX21lc3NhZ2VfbG9nZ2luZzogZmFsc2VcbiAgIyByZWRhY3RfdXNlcl9hcGlfa2V5X2luZm86IGZhbHNlXG4gIHNlcnZpY2VfY2FsbGJhY2s6IFtcInByb21ldGhldXNfc3lzdGVtXCJdXG4gIGRyb3BfcGFyYW1zOiB0cnVlXG4gICMgbWF4X2J1ZGdldDogMTAwXG4gICMgYnVkZ2V0X2R1cmF0aW9uOiAzMGRcbiAgbnVtX3JldHJpZXM6IDNcbiAgcmVxdWVzdF90aW1lb3V0OiA2MDBcbiAgdGVsZW1ldHJ5OiBmYWxzZVxuICBjYWNoZTogdHJ1ZVxuICBjYWNoZV9wYXJhbXM6XG4gICAgdHlwZTogcmVkaXNcbiAgICBob3N0OiBvcy5lbnZpcm9uL1JFRElTX0hPU1RcbiAgICBwb3J0OiBvcy5lbnZpcm9uL1JFRElTX1BPUlRcbiAgICBwYXNzd29yZDogb3MuZW52aXJvbi9SRURJU19QQVNTV09SRFxuICAgIG5hbWVzcGFjZTogXCJsaXRlbGxtX2NhY2hlXCJcbiAgICB0dGw6IDYwMFxuICBzdWNjZXNzX2NhbGxiYWNrOlxuICAgICMgLSBcImxhbmdmdXNlXCJcbiAgICAtIFwicHJvbWV0aGV1c1wiXG4gIGZhaWx1cmVfY2FsbGJhY2s6XG4gICAgIyAtIFwibGFuZ2Z1c2VcIlxuICAgIC0gXCJwcm9tZXRoZXVzXCJcbm1vZGVsX2xpc3Q6XG4gICMgT3BlbkFJXG4gIC0gbW9kZWxfbmFtZTogZ3B0LTRcbiAgICBsaXRlbGxtX3BhcmFtczpcbiAgICAgIG1vZGVsOiBvcGVuYWkvZ3B0LTRcbiAgICAgIGFwaV9rZXk6IG9zLmVudmlyb24vT1BFTkFJX0FQSV9LRVlcbiAgICAgIGFwaV9iYXNlOiBvcy5lbnZpcm9uL09QRU5BSV9BUElfQkFTRVxuICAtIG1vZGVsX25hbWU6IGdwdC00b1xuICAgIGxpdGVsbG1fcGFyYW1zOlxuICAgICAgbW9kZWw6IG9wZW5haS9ncHQtNG9cbiAgICAgIGFwaV9rZXk6IG9zLmVudmlyb24vT1BFTkFJX0FQSV9LRVlcbiAgICAgIGFwaV9iYXNlOiBvcy5lbnZpcm9uL09QRU5BSV9BUElfQkFTRVxuICAtIG1vZGVsX25hbWU6IGdwdC00by1taW5pXG4gICAgbGl0ZWxsbV9wYXJhbXM6XG4gICAgICBtb2RlbDogb3BlbmFpL2dwdC00by1taW5pXG4gICAgICBhcGlfa2V5OiBvcy5lbnZpcm9uL09QRU5BSV9BUElfS0VZXG4gICAgICBhcGlfYmFzZTogb3MuZW52aXJvbi9PUEVOQUlfQVBJX0JBU0VcbiAgIyBBbnRocm9waWNcbiAgLSBtb2RlbF9uYW1lOiBjbGF1ZGUtMy1oYWlrdVxuICAgIGxpdGVsbG1fcGFyYW1zOlxuICAgICAgbW9kZWw6IGNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3XG4gICAgICBhcGlfa2V5OiBcIm9zLmVudmlyb24vQU5USFJPUElDX0FQSV9LRVlcIlxuICAgICAgYXBpX2Jhc2U6IFwib3MuZW52aXJvbi9BTlRIUk9QSUNfQVBJX0JBU0VcIlxuICAtIG1vZGVsX25hbWU6IGNsYXVkZS0zLjUtc29ubmV0XG4gICAgbGl0ZWxsbV9wYXJhbXM6XG4gICAgICBtb2RlbDogY2xhdWRlLTMtNS1zb25uZXQtMjAyNDA2MjBcbiAgICAgIGFwaV9rZXk6IFwib3MuZW52aXJvbi9BTlRIUk9QSUNfQVBJX0tFWVwiXG4gICAgICBhcGlfYmFzZTogXCJvcy5lbnZpcm9uL0FOVEhST1BJQ19BUElfQkFTRVwiXG4gICMgVm95YWdlQUlcbiAgLSBtb2RlbF9uYW1lOiB2b3lhZ2UtbGF3LTJcbiAgICBtb2RlbF9pbmZvOlxuICAgICAgb3V0cHV0X3ZlY3Rvcl9zaXplOiAxMDI0XG4gICAgbGl0ZWxsbV9wYXJhbXM6XG4gICAgICBtb2RlbDogdm95YWdlL3ZveWFnZS1sYXctMlxuICAgICAgYXBpX2tleTogXCJvcy5lbnZpcm9uL1ZPWUFHRV9BUElfS0VZXCJcbiAgICAgIGFwaV9iYXNlOiBcIm9zLmVudmlyb24vVk9ZQUdFX0FQSV9CQVNFXCJcbiAgICAgICMgcnBtOiAzMDBcbiAgICAgICMgdHBtOiAxMDAwMDAwXG4gIC0gbW9kZWxfbmFtZTogdm95YWdlLW11bHRpbGluZ3VhbC0yXG4gICAgbW9kZWxfaW5mbzpcbiAgICAgIG1vZGU6IGVtYmVkZGluZ1xuICAgICAgbWF4X3Rva2VuczogMzIwMDBcbiAgICAgIG1heF9pbnB1dF90b2tlbnM6IDMyMDAwXG4gICAgICBvdXRwdXRfdmVjdG9yX3NpemU6IDEwMjRcbiAgICBsaXRlbGxtX3BhcmFtczpcbiAgICAgIG1vZGVsOiB2b3lhZ2Uvdm95YWdlLW11bHRpbGluZ3VhbC0yXG4gICAgICBhcGlfa2V5OiBcIm9zLmVudmlyb24vVk9ZQUdFX0FQSV9LRVlcIlxuICAgICAgYXBpX2Jhc2U6IFwib3MuZW52aXJvbi9WT1lBR0VfQVBJX0JBU0VcIlxuICAgICAgaW5wdXRfY29zdF9wZXJfdG9rZW46IDAuMDAwMDAwMTJcbiAgICAgIG91dHB1dF9jb3N0X3Blcl90b2tLine truncated
|
||||
"compose": "c2VydmljZXM6CiAgbGl0ZWxsbToKICAgIGltYWdlOiAnZ2hjci5pby9iZXJyaWFpL2xpdGVsbG0tZGF0YWJhc2U6bWFpbi1zdGFibGUnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0xJVEVMTE1fNDAwMAogICAgICAtICdMSVRFTExNX0xPRz0ke0xJVEVMTE1fTE9HOi1FUlJPUn0nCiAgICAgIC0gJ0xJVEVMTE1fTU9ERT0ke0xJVEVMTE1fTU9ERTotUFJPRFVDVElPTn0nCiAgICAgIC0gJ0xJVEVMTE1fTUFTVEVSX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFTVEVSS0VZfScKICAgICAgLSAnVUlfVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfVUl9JwogICAgICAtICdVSV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfVUl9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGl0ZWxsbX0nCiAgICAgIC0gJ1JFRElTX0hPU1Q9JHtSRURJU19IT1NUOi1yZWRpc30nCiAgICAgIC0gJ1JFRElTX1BPUlQ9JHtSRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWxpdGVsbG19JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke09QRU5BSV9BUElfS0VZfScKICAgICAgLSAnT1BFTkFJX0FQSV9CQVNFPSR7T1BFTkFJX0FQSV9CQVNFfScKICAgICAgLSAnQU5USFJPUElDX0FQSV9LRVk9JHtBTlRIUk9QSUNfQVBJX0tFWX0nCiAgICAgIC0gJ0FOVEhST1BJQ19BUElfQkFTRT0ke0FOVEhST1BJQ19BUElfQkFTRX0nCiAgICAgIC0gJ1ZPWUFHRV9BUElfS0VZPSR7Vk9ZQUdFX0FQSV9LRVl9JwogICAgICAtICdWT1lBR0VfQVBJX0JBU0U9JHtWT1lBR0VfQVBJX0JBU0V9JwogICAgICAtICdTVE9SRV9NT0RFTF9JTl9EQj0ke1NUT1JFX01PREVMX0lOX0RCfScKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2xpdGVsbG0tY29uZmlnLnlhbWwKICAgICAgICB0YXJnZXQ6IC9hcHAvY29uZmlnLnlhbWwKICAgICAgICBjb250ZW50OiAiZ2VuZXJhbF9zZXR0aW5nczpcbiAgcHJveHlfYmF0Y2hfd3JpdGVfYXQ6IDYwXG5cbnJvdXRlcl9zZXR0aW5nczpcbiAgcmVkaXNfaG9zdDogb3MuZW52aXJvbi9SRURJU19IT1NUXG4gIHJlZGlzX3BvcnQ6IG9zLmVudmlyb24vUkVESVNfUE9SVFxuICByZWRpc19wYXNzd29yZDogb3MuZW52aXJvbi9SRURJU19QQVNTV09SRFxuICBlbmFibGVfcHJlX2NhbGxfY2hlY2s6IHRydWVcblxubGl0ZWxsbV9zZXR0aW5nczpcbiAgc2V0X3ZlcmJvc2U6IGZhbHNlXG4gIGpzb25fbG9nczogdHJ1ZVxuICBsb2dfcmF3X3JlcXVlc3RfcmVzcG9uc2U6IHRydWVcbiAgIyB0dXJuX29mZl9tZXNzYWdlX2xvZ2dpbmc6IGZhbHNlXG4gICMgcmVkYWN0X3VzZXJfYXBpX2tleV9pbmZvOiBmYWxzZVxuICBzZXJ2aWNlX2NhbGxiYWNrOiBbXCJwcm9tZXRoZXVzX3N5c3RlbVwiXVxuICBkcm9wX3BhcmFtczogdHJ1ZVxuICAjIG1heF9idWRnZXQ6IDEwMFxuICAjIGJ1ZGdldF9kdXJhdGlvbjogMzBkXG4gIG51bV9yZXRyaWVzOiAzXG4gIHJlcXVlc3RfdGltZW91dDogNjAwXG4gIHRlbGVtZXRyeTogZmFsc2VcbiAgY2FjaGU6IHRydWVcbiAgY2FjaGVfcGFyYW1zOlxuICAgIHR5cGU6IHJlZGlzXG4gICAgaG9zdDogb3MuZW52aXJvbi9SRURJU19IT1NUXG4gICAgcG9ydDogb3MuZW52aXJvbi9SRURJU19QT1JUXG4gICAgcGFzc3dvcmQ6IG9zLmVudmlyb24vUkVESVNfUEFTU1dPUkRcbiAgICBuYW1lc3BhY2U6IFwibGl0ZWxsbV9jYWNoZVwiXG4gICAgdHRsOiA2MDBcbiAgc3VjY2Vzc19jYWxsYmFjazpcbiAgICAjIC0gXCJsYW5nZnVzZVwiXG4gICAgLSBcInByb21ldGhldXNcIlxuICBmYWlsdXJlX2NhbGxiYWNrOlxuICAgICMgLSBcImxhbmdmdXNlXCJcbiAgICAtIFwicHJvbWV0aGV1c1wiXG5tb2RlbF9saXN0OlxuICAjIE9wZW5BSVxuICAtIG1vZGVsX25hbWU6IGdwdC00XG4gICAgbGl0ZWxsbV9wYXJhbXM6XG4gICAgICBtb2RlbDogb3BlbmFpL2dwdC00XG4gICAgICBhcGlfa2V5OiBvcy5lbnZpcm9uL09QRU5BSV9BUElfS0VZXG4gICAgICBhcGlfYmFzZTogb3MuZW52aXJvbi9PUEVOQUlfQVBJX0JBU0VcbiAgLSBtb2RlbF9uYW1lOiBncHQtNG9cbiAgICBsaXRlbGxtX3BhcmFtczpcbiAgICAgIG1vZGVsOiBvcGVuYWkvZ3B0LTRvXG4gICAgICBhcGlfa2V5OiBvcy5lbnZpcm9uL09QRU5BSV9BUElfS0VZXG4gICAgICBhcGlfYmFzZTogb3MuZW52aXJvbi9PUEVOQUlfQVBJX0JBU0VcbiAgLSBtb2RlbF9uYW1lOiBncHQtNG8tbWluaVxuICAgIGxpdGVsbG1fcGFyYW1zOlxuICAgICAgbW9kZWw6IG9wZW5haS9ncHQtNG8tbWluaVxuICAgICAgYXBpX2tleTogb3MuZW52aXJvbi9PUEVOQUlfQVBJX0tFWVxuICAgICAgYXBpX2Jhc2U6IG9zLmVudmlyb24vT1BFTkFJX0FQSV9CQVNFXG4gICMgQW50aHJvcGljXG4gIC0gbW9kZWxfbmFtZTogY2xhdWRlLTMtaGFpa3VcbiAgICBsaXRlbGxtX3BhcmFtczpcbiAgICAgIG1vZGVsOiBjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwN1xuICAgICAgYXBpX2tleTogXCJvcy5lbnZpcm9uL0FOVEhST1BJQ19BUElfS0VZXCJcbiAgICAgIGFwaV9iYXNlOiBcIm9zLmVudmlyb24vQU5USFJPUElDX0FQSV9CQVNFXCJcbiAgLSBtb2RlbF9uYW1lOiBjbGF1ZGUtMy41LXNvbm5ldFxuICAgIGxpdGVsbG1fcGFyYW1zOlxuICAgICAgbW9kZWw6IGNsYXVkZS0zLTUtc29ubmV0LTIwMjQwNjIwXG4gICAgICBhcGlfa2V5OiBcIm9zLmVudmlyb24vQU5USFJPUElDX0FQSV9LRVlcIlxuICAgICAgYXBpX2Jhc2U6IFwib3MuZW52aXJvbi9BTlRIUk9QSUNfQVBJX0JBU0VcIlxuICAjIFZveWFnZUFJXG4gIC0gbW9kZWxfbmFtZTogdm95YWdlLWxhdy0yXG4gICAgbW9kZWxfaW5mbzpcbiAgICAgIG91dHB1dF92ZWN0b3Jfc2l6ZTogMTAyNFxuICAgIGxpdGVsbG1fcGFyYW1zOlxuICAgICAgbW9kZWw6IHZveWFnZS92b3lhZ2UtbGF3LTJcbiAgICAgIGFwaV9rZXk6IFwib3MuZW52aXJvbi9WT1lBR0VfQVBJX0tFWVwiXG4gICAgICBhcGlfYmFzZTogXCJvcy5lbnZpcm9uL1ZPWUFHRV9BUElfQkFTRVwiXG4gICAgICAjIHJwbTogMzAwXG4gICAgICAjIHRwbTogMTAwMDAwMFxuICAtIG1vZGVsX25hbWU6IHZveWFnZS1tdWx0aWxpbmd1YWwtMlxuICAgIG1vZGVsX2luZm86XG4gICAgICBtb2RlOiBlbWJlZGRpbmdcbiAgICAgIG1heF90b2tlbnM6IDMyMDAwXG4gICAgICBtYXhfaW5wdXRfdG9rZW5zOiAzMjAwMFxuICAgICAgb3V0cHV0X3ZlY3Rvcl9zaXplOiAxMDI0XG4gICAgbGl0ZWxsbV9wYXJhbXM6XG4gICAgICBtb2RlbDogdm95YWdlL3ZveWFnZS1tdWx0aWxpbmd1YWwtMlxuICAgICAgYXBpX2tleTogXCJvcy5lbnZpcm9uL1ZPWUFHRV9BUElfS0VZXCJcbiAgICAgIGFwaV9iYXNlOiBcIm9zLmVudmlyb24vVk9ZQUdFX0FQSV9CQVNFXCJcbiAgICAgIGlucHV0X2Nvc3RLine truncated
|
||||
"tags": [
|
||||
"ai",
|
||||
"qdrant",
|
||||
@@ -3226,7 +3241,7 @@
|
||||
"paperless": {
|
||||
"documentation": "https://docs.paperless-ngx.com/configuration/?utm_source=coolify.io",
|
||||
"slogan": "Paperless-ngx is a community-supported open-source document management system that transforms your physical documents into a searchable online archive so you can keep, well, less paper.",
|
||||
"compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuNCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhcGVybGVzcy1yZWRpczovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICBwYXBlcmxlc3M6CiAgICBpbWFnZTogJ3BhcGVybGVzc25neC9wYXBlcmxlc3Mtbmd4OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mcycKICAgICAgICAtICctUycKICAgICAgICAtICctLW1heC10aW1lJwogICAgICAgIC0gJzInCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICdwYXBlcmxlc3MtZGF0YTovdXNyL3NyYy9wYXBlcmxlc3MvZGF0YScKICAgICAgLSAncGFwZXJsZXNzLW1lZGlhOi91c3Ivc3JjL3BhcGVybGVzcy9tZWRpYScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXhwb3J0CiAgICAgICAgdGFyZ2V0OiAvdXNyL3NyYy9wYXBlcmxlc3MvZXhwb3J0CiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NvbnN1bWUKICAgICAgICB0YXJnZXQ6IC91c3Ivc3JjL3BhcGVybGVzcy9jb25zdW1lCiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSBQQVBFUkxFU1NfVVJMPSRTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSAnUEFQRVJMRVNTX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QQVBFUkxFU1N9JwogICAgICAtICdQQVBFUkxFU1NfQURNSU5fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QQVBFUkxFU1N9JwogICAgICAtICdQQVBFUkxFU1NfUkVESVM9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtICdQQVBFUkxFU1NfU0VDUkVUX0tFWT0ke1NFUlZJQ0VfUkVBTEJBU0U2NF82NF9QQVBFUkxFU1N9Jwo=",
|
||||
"compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuNCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhcGVybGVzcy1yZWRpczovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICBwYXBlcmxlc3M6CiAgICBpbWFnZTogJ3BhcGVybGVzc25neC9wYXBlcmxlc3Mtbmd4OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mcycKICAgICAgICAtICctUycKICAgICAgICAtICctLW1heC10aW1lJwogICAgICAgIC0gJzInCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICdwYXBlcmxlc3MtZGF0YTovdXNyL3NyYy9wYXBlcmxlc3MvZGF0YScKICAgICAgLSAncGFwZXJsZXNzLW1lZGlhOi91c3Ivc3JjL3BhcGVybGVzcy9tZWRpYScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXhwb3J0CiAgICAgICAgdGFyZ2V0OiAvdXNyL3NyYy9wYXBlcmxlc3MvZXhwb3J0CiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NvbnN1bWUKICAgICAgICB0YXJnZXQ6IC91c3Ivc3JjL3BhcGVybGVzcy9jb25zdW1lCiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSBQQVBFUkxFU1NfVVJMPSRTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTCiAgICAgIC0gJ1BBUEVSTEVTU19BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUEFQRVJMRVNTfScKICAgICAgLSAnUEFQRVJMRVNTX0FETUlOX1VTRVI9JHtTRVJWSUNFX1VTRVJfUEFQRVJMRVNTfScKICAgICAgLSAnUEFQRVJMRVNTX1JFRElTPXJlZGlzOi8vcmVkaXM6NjM3OScKICAgICAgLSAnUEFQRVJMRVNTX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1JFQUxCQVNFNjRfNjRfUEFQRVJMRVNTfScK",
|
||||
"tags": null,
|
||||
"category": "productivity",
|
||||
"logo": "svgs/paperless.svg",
|
||||
@@ -3262,10 +3277,27 @@
|
||||
"minversion": "0.0.0",
|
||||
"port": "80"
|
||||
},
|
||||
"penpot-with-s3": {
|
||||
"documentation": "https://help.penpot.app/technical-guide/getting-started/#install-with-docker?utm_source=coolify.io",
|
||||
"slogan": "Penpot is the first Open Source design and prototyping platform for product teams.",
|
||||
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9mcm9udGVuZDoyLjExLjEnCiAgICBkZXBlbmRzX29uOgogICAgICBwZW5wb3QtYmFja2VuZDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwZW5wb3QtZXhwb3J0ZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwCiAgICAgIC0gJ1BFTlBPVF9GTEFHUz0ke1BFTlBPVF9CQUNLRU5EX0ZMQUdTOi1lbmFibGUtbG9naW4td2l0aC1wYXNzd29yZH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHBlbnBvdC1iYWNrZW5kOgogICAgaW1hZ2U6ICdwZW5wb3RhcHAvYmFja2VuZDoyLjExLjEnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwZW5wb3QtdmFsa2V5OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIG1pbmlvLWluaXQ6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2NvbXBsZXRlZF9zdWNjZXNzZnVsbHkKICAgIGVudmlyb25tZW50OgogICAgICAtICdQRU5QT1RfRkxBR1M9JHtQRU5QT1RfQkFDS0VORF9GTEFHUzotZW5hYmxlLWxvZ2luLXdpdGgtcGFzc3dvcmR9JwogICAgICAtIFBFTlBPVF9IVFRQX1NFUlZFUl9QT1JUPTYwNjAKICAgICAgLSAnUEVOUE9UX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1JFQUxCQVNFNjRfNjRfUEVOUE9UfScKICAgICAgLSAnUEVOUE9UX1BVQkxJQ19VUkk9JHtTRVJWSUNFX0ZRRE5fRlJPTlRFTkRfODA4MH0nCiAgICAgIC0gJ1BFTlBPVF9CQUNLRU5EX1VSST1odHRwOi8vcGVucG90LWJhY2tlbmQnCiAgICAgIC0gJ1BFTlBPVF9FWFBPUlRFUl9VUkk9aHR0cDovL3BlbnBvdC1leHBvcnRlcicKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1VSST1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXMvJHtQT1NUR1JFU19EQjotcGVucG90fScKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BFTlBPVF9SRURJU19VUkk9cmVkaXM6Ly9wZW5wb3QtdmFsa2V5LzAnCiAgICAgIC0gJ1BFTlBPVF9URUxFTUVUUllfRU5BQkxFRD0ke1BFTlBPVF9URUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogICAgICAtIFBFTlBPVF9PQkpFQ1RTX1NUT1JBR0VfQkFDS0VORD1zMwogICAgICAtIFBFTlBPVF9PQkpFQ1RTX1NUT1JBR0VfUzNfUkVHSU9OPXVzLWVhc3QtMQogICAgICAtIFBFTlBPVF9PQkpFQ1RTX1NUT1JBR0VfUzNfQlVDS0VUPXBlbnBvdAogICAgICAtICdQRU5QT1RfT0JKRUNUU19TVE9SQUdFX1MzX0VORFBPSU5UPWh0dHA6Ly9taW5pbzo5MDAwJwogICAgICAtICdBV1NfQUNDRVNTX0tFWV9JRD0ke1NFUlZJQ0VfVVNFUl9NSU5JT30nCiAgICAgIC0gJ0FXU19TRUNSRVRfQUNDRVNTX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgICAtICdQRU5QT1RfU01UUF9ERUZBVUxUX0ZST009JHtQRU5QT1RfU01UUF9ERUZBVUxUX0ZST006LW5vLXJlcGx5QGV4YW1wbGUuY29tfScKICAgICAgLSAnUEVOUE9UX1NNVFBfREVGQVVMVF9SRVBMWV9UTz0ke1BFTlBPVF9TTVRQX0RFRkFVTFRfUkVQTFlfVE86LW5vLXJlcGx5QGV4YW1wbGUuY29tfScKICAgICAgLSAnUEVOUE9UX1NNVFBfSE9TVD0ke1BFTlBPVF9TTVRQX0hPU1Q6LW1haWxwaXR9JwogICAgICAtICdQRU5QT1RfU01UUF9QT1JUPSR7UEVOUE9UX1NNVFBfUE9SVDotMTAyNX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1VTRVJOQU1FPSR7UEVOUE9UX1NNVFBfVVNFUk5BTUU6LXBlbnBvdH0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1BBU1NXT1JEPSR7UEVOUE9UX1NNVFBfUEFTU1dPUkQ6LXBlbnBvdH0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1RMUz0ke1BFTlBPVF9TTVRQX1RMUzotZmFsc2V9JwogICAgICAtICdQRU5QT1RfU01UUF9TU0w9JHtQRU5QT1RfU01UUF9TU0w6LWZhbHNlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBub2RlCiAgICAgICAgLSAnLWUnCiAgICAgICAgLSAicmVxdWlyZSgnaHR0cCcpLmdldCh7aG9zdDonMTI3LjAuMC4xJywgcG9ydDo2MDYwLCBwYXRoOicvcmVhZHl6J30sIHJlcyA9PiBwcm9jZXNzLmV4aXQocmVzLnN0YXR1c0NvZGU9PT0yMDAgPyAwIDogMSkpLm9uKCdlcnJvcicsICgpID0+IHByb2Nlc3MuZXhpdCgxKSk7IgogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDMwcwogICAgICByZXRyaWVzOiAxNQogIHBlbnBvdC1leHBvcnRlcjoKICAgIGltYWdlOiAncGVucG90YXBwL2V4cG9ydGVyOjIuMTEuMScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQRU5QT1RfUFVCTElDX1VSST0ke1NFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwfScKICAgICAgLSAnUEVOUE9UX1JFRElTX1VSST1yZWRpczovL3BlbnBvdC12YWxrZXkvMCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2MDYxL3JlYWR5eicKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIG1pbmlvOgogICAgaW1hZ2U6ICdnaGNyLmlvL2Nvb2xsYWJzaW8vbWluaW86UkVMRUFTRS4yMDI1LTEwLTE1VDE3LTI5LTU1WicKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBtaW5pby1pbml0OgogICAgaW1hZ2U6ICdtaW5pby9tYzpsYXRlc3QnCiAgICBkZXBlbmRzX29uOgogICAgICBtaW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW50cnlwb2ludDogInNoIC1jIFwiXG4gIG1jIGFsaWFzIHNldCBsb2NhbCBodHRwOi8vbWluaW86OTAwMCAke1NFUlZJQ0VfVVNFUl9NSU5JT30gJHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfSAmJlxuICBtYyBtYiAtcCBsb2NhbC9wZW5wb3QgfHwgdHJ1ZSAmJlxuICBtYyBhbm9ueW1vdXMgc2V0IHByaXZhdGUgbG9jYWwvcGVucG90XG5cIlxuIgogICAgcmVzdGFydDogJ25vJwogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1JwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3NLine truncated
|
||||
"tags": [
|
||||
"penpot",
|
||||
"design",
|
||||
"prototyping",
|
||||
"figma",
|
||||
"open",
|
||||
"source"
|
||||
],
|
||||
"category": "productivity",
|
||||
"logo": "svgs/penpot.svg",
|
||||
"minversion": "0.0.0",
|
||||
"port": "8080"
|
||||
},
|
||||
"penpot": {
|
||||
"documentation": "https://help.penpot.app/technical-guide/getting-started/#install-with-docker?utm_source=coolify.io",
|
||||
"slogan": "Penpot is the first Open Source design and prototyping platform for product teams.",
|
||||
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9mcm9udGVuZDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtYXNzZXRzOi9vcHQvZGF0YS9hc3NldHMnCiAgICBkZXBlbmRzX29uOgogICAgICBwZW5wb3QtYmFja2VuZDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwZW5wb3QtZXhwb3J0ZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwCiAgICAgIC0gJ1BFTlBPVF9GTEFHUz0ke1BFTlBPVF9GUk9OVEVORF9GTEFHUzotZW5hYmxlLWxvZ2luLXdpdGgtcGFzc3dvcmR9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwZW5wb3QtYmFja2VuZDoKICAgIGltYWdlOiAncGVucG90YXBwL2JhY2tlbmQ6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LWFzc2V0czovb3B0L2RhdGEvYXNzZXRzJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtICdQRU5QT1RfRkxBR1M9JHtQRU5QT1RfQkFDS0VORF9GTEFHUzotZW5hYmxlLWxvZ2luLXdpdGgtcGFzc3dvcmQgZW5hYmxlLXNtdHAgZW5hYmxlLXByZXBsLXNlcnZlcn0nCiAgICAgIC0gUEVOUE9UX0hUVFBfU0VSVkVSX1BPUlQ9NjA2MAogICAgICAtIFBFTlBPVF9TRUNSRVRfS0VZPSRTRVJWSUNFX1JFQUxCQVNFNjRfNjRfUEVOUE9UCiAgICAgIC0gUEVOUE9UX1BVQkxJQ19VUkk9JFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwCiAgICAgIC0gJ1BFTlBPVF9CQUNLRU5EX1VSST1odHRwOi8vcGVucG90LWJhY2tlbmQnCiAgICAgIC0gJ1BFTlBPVF9FWFBPUlRFUl9VUkk9aHR0cDovL3BlbnBvdC1leHBvcnRlcicKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1VSST1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXMvJHtQT1NUR1JFU19EQjotcGVucG90fScKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BFTlBPVF9SRURJU19VUkk9cmVkaXM6Ly9yZWRpcy8wJwogICAgICAtIFBFTlBPVF9BU1NFVFNfU1RPUkFHRV9CQUNLRU5EPWFzc2V0cy1mcwogICAgICAtIFBFTlBPVF9TVE9SQUdFX0FTU0VUU19GU19ESVJFQ1RPUlk9L29wdC9kYXRhL2Fzc2V0cwogICAgICAtICdQRU5QT1RfVEVMRU1FVFJZX0VOQUJMRUQ9JHtQRU5QT1RfVEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnUEVOUE9UX1NNVFBfREVGQVVMVF9GUk9NPSR7UEVOUE9UX1NNVFBfREVGQVVMVF9GUk9NOi1uby1yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0RFRkFVTFRfUkVQTFlfVE89JHtQRU5QT1RfU01UUF9ERUZBVUxUX1JFUExZX1RPOi1uby1yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0hPU1Q9JHtQRU5QT1RfU01UUF9IT1NUOi1tYWlscGl0fScKICAgICAgLSAnUEVOUE9UX1NNVFBfUE9SVD0ke1BFTlBPVF9TTVRQX1BPUlQ6LTEwMjV9JwogICAgICAtICdQRU5QT1RfU01UUF9VU0VSTkFNRT0ke1BFTlBPVF9TTVRQX1VTRVJOQU1FOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfU01UUF9QQVNTV09SRD0ke1BFTlBPVF9TTVRQX1BBU1NXT1JEOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfU01UUF9UTFM9JHtQRU5QT1RfU01UUF9UTFM6LWZhbHNlfScKICAgICAgLSAnUEVOUE9UX1NNVFBfU1NMPSR7UEVOUE9UX1NNVFBfU1NMOi1mYWxzZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gInJlcXVpcmUoJ2h0dHAnKS5nZXQoe2hvc3Q6JzEyNy4wLjAuMScsIHBvcnQ6NjA2MCwgcGF0aDonL3JlYWR5eid9LCByZXMgPT4gcHJvY2Vzcy5leGl0KHJlcy5zdGF0dXNDb2RlPT09MjAwID8gMCA6IDEpKS5vbignZXJyb3InLCAoKSA9PiBwcm9jZXNzLmV4aXQoMSkpOyIKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogMTUKICBwZW5wb3QtZXhwb3J0ZXI6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9leHBvcnRlcjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQRU5QT1RfUFVCTElDX1VSST0kU0VSVklDRV9GUUROX0ZST05URU5EXzgwODAKICAgICAgLSAnUEVOUE9UX1JFRElTX1VSST1yZWRpczovL3JlZGlzLzAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NjA2MS9yZWFkeXonCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBtYWlscGl0OgogICAgaW1hZ2U6ICdheGxsZW50L21haWxwaXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01BSUxQSVRfODAyNQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9tYWlscGl0CiAgICAgICAgLSByZWFkeXoKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BlbnBvdC1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfSU5JVERCX0FSR1M9LS1kYXRhLWNoZWNrc3VtcwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXBlbnBvdH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjctYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzJwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
|
||||
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9mcm9udGVuZDoyLjExLjEnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtYXNzZXRzOi9vcHQvZGF0YS9hc3NldHMnCiAgICBkZXBlbmRzX29uOgogICAgICBwZW5wb3QtYmFja2VuZDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwZW5wb3QtZXhwb3J0ZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwCiAgICAgIC0gJ1BFTlBPVF9GTEFHUz0ke1BFTlBPVF9GUk9OVEVORF9GTEFHUzotZW5hYmxlLWxvZ2luLXdpdGgtcGFzc3dvcmR9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwZW5wb3QtYmFja2VuZDoKICAgIGltYWdlOiAncGVucG90YXBwL2JhY2tlbmQ6Mi4xMS4xJwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LWFzc2V0czovb3B0L2RhdGEvYXNzZXRzJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcGVucG90LXZhbGtleToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BFTlBPVF9GTEFHUz0ke1BFTlBPVF9CQUNLRU5EX0ZMQUdTOi1lbmFibGUtbG9naW4td2l0aC1wYXNzd29yZCBlbmFibGUtc210cH0nCiAgICAgIC0gUEVOUE9UX0hUVFBfU0VSVkVSX1BPUlQ9NjA2MAogICAgICAtIFBFTlBPVF9TRUNSRVRfS0VZPSRTRVJWSUNFX1JFQUxCQVNFNjRfNjRfUEVOUE9UCiAgICAgIC0gUEVOUE9UX1BVQkxJQ19VUkk9JFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MDgwCiAgICAgIC0gJ1BFTlBPVF9CQUNLRU5EX1VSST1odHRwOi8vcGVucG90LWJhY2tlbmQnCiAgICAgIC0gJ1BFTlBPVF9FWFBPUlRFUl9VUkk9aHR0cDovL3BlbnBvdC1leHBvcnRlcicKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1VSST1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXMvJHtQT1NUR1JFU19EQjotcGVucG90fScKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUEVOUE9UX0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BFTlBPVF9SRURJU19VUkk9cmVkaXM6Ly9wZW5wb3QtdmFsa2V5LzAnCiAgICAgIC0gUEVOUE9UX09CSkVDVFNfU1RPUkFHRV9CQUNLRU5EPWZzCiAgICAgIC0gUEVOUE9UX09CSkVDVFNfU1RPUkFHRV9GU19ESVJFQ1RPUlk9L29wdC9kYXRhL2Fzc2V0cwogICAgICAtICdQRU5QT1RfVEVMRU1FVFJZX0VOQUJMRUQ9JHtQRU5QT1RfVEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnUEVOUE9UX1NNVFBfREVGQVVMVF9GUk9NPSR7UEVOUE9UX1NNVFBfREVGQVVMVF9GUk9NOi1uby1yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0RFRkFVTFRfUkVQTFlfVE89JHtQRU5QT1RfU01UUF9ERUZBVUxUX1JFUExZX1RPOi1uby1yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX0hPU1Q9JHtQRU5QT1RfU01UUF9IT1NUOi1tYWlscGl0fScKICAgICAgLSAnUEVOUE9UX1NNVFBfUE9SVD0ke1BFTlBPVF9TTVRQX1BPUlQ6LTEwMjV9JwogICAgICAtICdQRU5QT1RfU01UUF9VU0VSTkFNRT0ke1BFTlBPVF9TTVRQX1VTRVJOQU1FOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfU01UUF9QQVNTV09SRD0ke1BFTlBPVF9TTVRQX1BBU1NXT1JEOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfU01UUF9UTFM9JHtQRU5QT1RfU01UUF9UTFM6LWZhbHNlfScKICAgICAgLSAnUEVOUE9UX1NNVFBfU1NMPSR7UEVOUE9UX1NNVFBfU1NMOi1mYWxzZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gInJlcXVpcmUoJ2h0dHAnKS5nZXQoe2hvc3Q6JzEyNy4wLjAuMScsIHBvcnQ6NjA2MCwgcGF0aDonL3JlYWR5eid9LCByZXMgPT4gcHJvY2Vzcy5leGl0KHJlcy5zdGF0dXNDb2RlPT09MjAwID8gMCA6IDEpKS5vbignZXJyb3InLCAoKSA9PiBwcm9jZXNzLmV4aXQoMSkpOyIKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogMTUKICBwZW5wb3QtZXhwb3J0ZXI6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9leHBvcnRlcjoyLjExLjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQRU5QT1RfUFVCTElDX1VSST0kU0VSVklDRV9GUUROX0ZST05URU5EXzgwODAKICAgICAgLSAnUEVOUE9UX1JFRElTX1VSST1yZWRpczovL3BlbnBvdC12YWxrZXkvMCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2MDYxL3JlYWR5eicKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIG1haWxwaXQ6CiAgICBpbWFnZTogJ2F4bGxlbnQvbWFpbHBpdDp2MS4yOCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NQUlMUElUXzgwMjUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvbWFpbHBpdAogICAgICAgIC0gcmVhZHl6CiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0lOSVREQl9BUkdTPS0tZGF0YS1jaGVja3N1bXMKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wZW5wb3R9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBlbnBvdC12YWxrZXk6CiAgICBpbWFnZTogJ3ZhbGtleS92YWxrZXk6OC4xJwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LXZhbGtleS1kYXRhOi9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1ZBTEtFWV9FWFRSQV9GTEFHUz0tLW1heG1lbW9yeSAxMjhtYiAtLW1heG1lbW9yeS1wb2xpY3kgdm9sYXRpbGUtbGZ1JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd2YWxrZXktY2xpIHBpbmcgfCBncmVwIFBPTkcnCiAgICAgIGludGVydmFsOiAxcwogICAgICB0aW1Line truncated
|
||||
"tags": [
|
||||
"penpot",
|
||||
"design",
|
||||
@@ -3469,7 +3501,7 @@
|
||||
"postiz": {
|
||||
"documentation": "https://docs.postiz.com?utm_source=coolify.io",
|
||||
"slogan": "Open source social media scheduling tool.",
|
||||
"compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOnYxLjYwLjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUE9TVElaXzUwMDAKICAgICAgLSAnTUFJTl9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafScKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX1BPU1RJWn0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0JBQ0tFTkRfVVJMPSR7U0VSVklDRV9GUUROX1BPU1RJWn0vYXBpJwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1RTRUNSRVR9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovL3Bvc3RncmVzOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotcG9zdGl6LWRifScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdDoke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9QHJlZGlzOjYzNzknCiAgICAgIC0gJ0JBQ0tFTkRfSU5URVJOQUxfVVJMPWh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICAgICAgLSAnQ0xPVURGTEFSRV9BQ0NPVU5UX0lEPSR7Q0xPVURGTEFSRV9BQ0NPVU5UX0lEfScKICAgICAgLSAnQ0xPVURGTEFSRV9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnQ0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWT0ke0NMT1VERkxBUkVfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX0JVQ0tFVE5BTUU9JHtDTE9VREZMQVJFX0JVQ0tFVE5BTUV9JwogICAgICAtICdDTE9VREZMQVJFX0JVQ0tFVF9VUkw9JHtDTE9VREZMQVJFX0JVQ0tFVF9VUkx9JwogICAgICAtICdDTE9VREZMQVJFX1JFR0lPTj0ke0NMT1VERkxBUkVfUkVHSU9OfScKICAgICAgLSAnU1RPUkFHRV9QUk9WSURFUj0ke1NUT1JBR0VfUFJPVklERVI6LWxvY2FsfScKICAgICAgLSAnVVBMT0FEX0RJUkVDVE9SWT0ke1VQTE9BRF9ESVJFQ1RPUlk6LS91cGxvYWRzfScKICAgICAgLSAnTkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9ESVJFQ1RPUlk6LS91cGxvYWRzfScKICAgICAgLSAnTkVYVF9QVUJMSUNfVVBMT0FEX1NUQVRJQ19ESVJFQ1RPUlk9JHtORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWX0nCiAgICAgIC0gJ1JFU0VORF9BUElfS0VZPSR7UkVTRU5EX0FQSV9LRVl9JwogICAgICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1N9JwogICAgICAtICdFTUFJTF9GUk9NX05BTUU9JHtFTUFJTF9GUk9NX05BTUV9JwogICAgICAtICdFTUFJTF9QUk9WSURFUj0ke0VNQUlMX1BST1ZJREVSfScKICAgICAgLSAnWF9BUElfS0VZPSR7U0VSVklDRV9YX0FQSX0nCiAgICAgIC0gJ1hfQVBJX1NFQ1JFVD0ke1NFUlZJQ0VfWF9TRUNSRVR9JwogICAgICAtICdMSU5LRURJTl9DTElFTlRfSUQ9JHtTRVJWSUNFX0xJTktFRElOX0lEfScKICAgICAgLSAnTElOS0VESU5fQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfTElOS0VESU5fU0VDUkVUfScKICAgICAgLSAnUkVERElUX0NMSUVOVF9JRD0ke1NFUlZJQ0VfUkVERElUX0FQSX0nCiAgICAgIC0gJ1JFRERJVF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9SRURESVRfU0VDUkVUfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9JRD0ke1NFUlZJQ0VfR0lUSFVCX0lEfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0dJVEhVQl9TRUNSRVR9JwogICAgICAtICdUSFJFQURTX0FQUF9JRD0ke1NFUlZJQ0VfVEhSRUFEU19JRH0nCiAgICAgIC0gJ1RIUkVBRFNfQVBQX1NFQ1JFVD0ke1NFUlZJQ0VfVEhSRUFEU19TRUNSRVR9JwogICAgICAtICdGQUNFQk9PS19BUFBfSUQ9JHtTRVJWSUNFX0ZBQ0VCT09LX0lEfScKICAgICAgLSAnRkFDRUJPT0tfQVBQX1NFQ1JFVD0ke1NFUlZJQ0VfRkFDRUJPT0tfU0VDUkVUfScKICAgICAgLSAnWU9VVFVCRV9DTElFTlRfSUQ9JHtTRVJWSUNFX1lPVVRVQkVfSUR9JwogICAgICAtICdZT1VUVUJFX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1lPVVRVQkVfU0VDUkVUfScKICAgICAgLSAnVElLVE9LX0NMSUVOVF9JRD0ke1NFUlZJQ0VfVElLVE9LX0lEfScKICAgICAgLSAnVElLVE9LX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1RJS1RPS19TRUNSRVR9JwogICAgICAtICdQSU5URVJFU1RfQ0xJRU5UX0lEPSR7U0VSVklDRV9QSU5URVJFU1RfSUR9JwogICAgICAtICdQSU5URVJFU1RfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfUElOVEVSRVNUX1NFQ1JFVH0nCiAgICAgIC0gJ0RSSUJCQkxFX0NMSUVOVF9JRD0ke1NFUlZJQ0VfRFJJQkJMRV9JRH0nCiAgICAgIC0gJ0RSSUJCQkxFX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0RSSUJCTEVfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfSUQ9JHtTRVJWSUNFX0RJU0NPUkRfSUR9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0RJU0NPUkRfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9CT1RfVE9LRU5fSUQ9JHtTRVJWSUNFX0RJU0NPUkRfVE9LRU59JwogICAgICAtICdTTEFDS19JRD0ke1NFUlZJQ0VfU0xBQ0tfSUR9JwogICAgICAtICdTTEFDS19TRUNSRVQ9JHtTRVJWSUNFX1NMQUNLX1NFQ1JFVH0nCiAgICAgIC0gJ1NMQUNLX1NJR05JTkdfU0VDUkVUPSR7U0xBQ0tfU0lHTklOR19TRUNSRVR9JwogICAgICAtICdNQVNUT0RPTl9DTElFTlRfSUQ9JHtTRVJWSUNFX01BU1RPRE9OX0lEfScKICAgICAgLSAnTUFTVE9ET05fQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfTUFTVE9ET05fU0VDUkVUfScKICAgICAgLSAnQkVFSElJVkVfQVBJX0tFWT0ke1NFUlZJQ0VfQkVFSElJVkVfS0VZfScKICAgICAgLSAnQkVFSElJVkVfUFVCTElDQVRJT05fSUQ9JHtTRVJWSUNFX0JFRUhJSVZFX1BVQklEfScKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtTRVJWSUNFX09QRU5BSV9LRVl9JwogICAgICAtICdORVhUX1BVQkxJQ19ESVNDT1JEX1NVUFBPUlQ9JHtORVhUX1BVQkxJQ19ESVNDT1JEX1NVUFBPUlR9JwogICAgICAtICdORVhUX1BVQkxJQ19QT0xPVE5PPSR7TkVYVF9QVUJMSUNfUE9MT1ROT30nCiAgICAgIC0gSVNfR0VORVJBTD10cnVlCiAgICAgIC0gJ05YX0FERF9QTFVHSU5TPSR7TlhfQUREX1BMVUdJTlM6LWZhbHNlfScKICAgICAgLSAnTk9UX1NFQ1VSRUQ9JHtOT1RfU0VDVVJFRDotZmFsc2V9JwogICAgICAtICdGRUVfQU1PVU5UPSR7RkVFX0FNT1VOVDotMC4wNX0nCiAgICAgIC0gJ1NUUklQRV9QVUJMSVNIQUJMRV9LRVk9JHtTVFJJUEVfUFVCTElTSEFCTEVfS0VZfScKICAgICAgLSAnU1RSSVBFX1NFQ1JFVF9LRVk9JHtTVFJJUEVfU0VDUkVUX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWT0ke1NUUklQRV9TSUdOSU5HX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWV9DT05ORUNUPSR7U1RSSVBFX1NJR05JTkdfS0VZX0NPTk5FQ1R9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGl6X2NvbmZpZzovY29uZmlnLycKICAgICAgLSAncG9zdGl6X3VwbG9hZHM6L3VwbG9hZHMvJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICALine truncated
|
||||
"compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOnYyLjEwLjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUE9TVElaXzUwMDAKICAgICAgLSAnTUFJTl9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafScKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX1BPU1RJWn0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0JBQ0tFTkRfVVJMPSR7U0VSVklDRV9GUUROX1BPU1RJWn0vYXBpJwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1RTRUNSRVR9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovL3Bvc3RncmVzOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotcG9zdGl6LWRifScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdDoke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9QHJlZGlzOjYzNzknCiAgICAgIC0gJ0JBQ0tFTkRfSU5URVJOQUxfVVJMPWh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICAgICAgLSAnQ0xPVURGTEFSRV9BQ0NPVU5UX0lEPSR7Q0xPVURGTEFSRV9BQ0NPVU5UX0lEfScKICAgICAgLSAnQ0xPVURGTEFSRV9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnQ0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWT0ke0NMT1VERkxBUkVfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX0JVQ0tFVE5BTUU9JHtDTE9VREZMQVJFX0JVQ0tFVE5BTUV9JwogICAgICAtICdDTE9VREZMQVJFX0JVQ0tFVF9VUkw9JHtDTE9VREZMQVJFX0JVQ0tFVF9VUkx9JwogICAgICAtICdDTE9VREZMQVJFX1JFR0lPTj0ke0NMT1VERkxBUkVfUkVHSU9OfScKICAgICAgLSAnU1RPUkFHRV9QUk9WSURFUj0ke1NUT1JBR0VfUFJPVklERVI6LWxvY2FsfScKICAgICAgLSAnVVBMT0FEX0RJUkVDVE9SWT0ke1VQTE9BRF9ESVJFQ1RPUlk6LS91cGxvYWRzfScKICAgICAgLSAnTkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9ESVJFQ1RPUlk6LS91cGxvYWRzfScKICAgICAgLSAnTkVYVF9QVUJMSUNfVVBMT0FEX1NUQVRJQ19ESVJFQ1RPUlk9JHtORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWX0nCiAgICAgIC0gJ1JFU0VORF9BUElfS0VZPSR7UkVTRU5EX0FQSV9LRVl9JwogICAgICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1N9JwogICAgICAtICdFTUFJTF9GUk9NX05BTUU9JHtFTUFJTF9GUk9NX05BTUV9JwogICAgICAtICdFTUFJTF9QUk9WSURFUj0ke0VNQUlMX1BST1ZJREVSfScKICAgICAgLSAnWF9BUElfS0VZPSR7U0VSVklDRV9YX0FQSX0nCiAgICAgIC0gJ1hfQVBJX1NFQ1JFVD0ke1NFUlZJQ0VfWF9TRUNSRVR9JwogICAgICAtICdMSU5LRURJTl9DTElFTlRfSUQ9JHtTRVJWSUNFX0xJTktFRElOX0lEfScKICAgICAgLSAnTElOS0VESU5fQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfTElOS0VESU5fU0VDUkVUfScKICAgICAgLSAnUkVERElUX0NMSUVOVF9JRD0ke1NFUlZJQ0VfUkVERElUX0FQSX0nCiAgICAgIC0gJ1JFRERJVF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9SRURESVRfU0VDUkVUfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9JRD0ke1NFUlZJQ0VfR0lUSFVCX0lEfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0dJVEhVQl9TRUNSRVR9JwogICAgICAtICdUSFJFQURTX0FQUF9JRD0ke1NFUlZJQ0VfVEhSRUFEU19JRH0nCiAgICAgIC0gJ1RIUkVBRFNfQVBQX1NFQ1JFVD0ke1NFUlZJQ0VfVEhSRUFEU19TRUNSRVR9JwogICAgICAtICdGQUNFQk9PS19BUFBfSUQ9JHtTRVJWSUNFX0ZBQ0VCT09LX0lEfScKICAgICAgLSAnRkFDRUJPT0tfQVBQX1NFQ1JFVD0ke1NFUlZJQ0VfRkFDRUJPT0tfU0VDUkVUfScKICAgICAgLSAnWU9VVFVCRV9DTElFTlRfSUQ9JHtTRVJWSUNFX1lPVVRVQkVfSUR9JwogICAgICAtICdZT1VUVUJFX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1lPVVRVQkVfU0VDUkVUfScKICAgICAgLSAnVElLVE9LX0NMSUVOVF9JRD0ke1NFUlZJQ0VfVElLVE9LX0lEfScKICAgICAgLSAnVElLVE9LX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1RJS1RPS19TRUNSRVR9JwogICAgICAtICdQSU5URVJFU1RfQ0xJRU5UX0lEPSR7U0VSVklDRV9QSU5URVJFU1RfSUR9JwogICAgICAtICdQSU5URVJFU1RfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfUElOVEVSRVNUX1NFQ1JFVH0nCiAgICAgIC0gJ0RSSUJCQkxFX0NMSUVOVF9JRD0ke1NFUlZJQ0VfRFJJQkJMRV9JRH0nCiAgICAgIC0gJ0RSSUJCQkxFX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0RSSUJCTEVfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfSUQ9JHtTRVJWSUNFX0RJU0NPUkRfSUR9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0RJU0NPUkRfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9CT1RfVE9LRU5fSUQ9JHtTRVJWSUNFX0RJU0NPUkRfVE9LRU59JwogICAgICAtICdTTEFDS19JRD0ke1NFUlZJQ0VfU0xBQ0tfSUR9JwogICAgICAtICdTTEFDS19TRUNSRVQ9JHtTRVJWSUNFX1NMQUNLX1NFQ1JFVH0nCiAgICAgIC0gJ1NMQUNLX1NJR05JTkdfU0VDUkVUPSR7U0xBQ0tfU0lHTklOR19TRUNSRVR9JwogICAgICAtICdNQVNUT0RPTl9DTElFTlRfSUQ9JHtTRVJWSUNFX01BU1RPRE9OX0lEfScKICAgICAgLSAnTUFTVE9ET05fQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfTUFTVE9ET05fU0VDUkVUfScKICAgICAgLSAnQkVFSElJVkVfQVBJX0tFWT0ke1NFUlZJQ0VfQkVFSElJVkVfS0VZfScKICAgICAgLSAnQkVFSElJVkVfUFVCTElDQVRJT05fSUQ9JHtTRVJWSUNFX0JFRUhJSVZFX1BVQklEfScKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtTRVJWSUNFX09QRU5BSV9LRVl9JwogICAgICAtICdORVhUX1BVQkxJQ19ESVNDT1JEX1NVUFBPUlQ9JHtORVhUX1BVQkxJQ19ESVNDT1JEX1NVUFBPUlR9JwogICAgICAtICdORVhUX1BVQkxJQ19QT0xPVE5PPSR7TkVYVF9QVUJMSUNfUE9MT1ROT30nCiAgICAgIC0gSVNfR0VORVJBTD10cnVlCiAgICAgIC0gJ05YX0FERF9QTFVHSU5TPSR7TlhfQUREX1BMVUdJTlM6LWZhbHNlfScKICAgICAgLSAnTk9UX1NFQ1VSRUQ9JHtOT1RfU0VDVVJFRDotZmFsc2V9JwogICAgICAtICdGRUVfQU1PVU5UPSR7RkVFX0FNT1VOVDotMC4wNX0nCiAgICAgIC0gJ1NUUklQRV9QVUJMSVNIQUJMRV9LRVk9JHtTVFJJUEVfUFVCTElTSEFCTEVfS0VZfScKICAgICAgLSAnU1RSSVBFX1NFQ1JFVF9LRVk9JHtTVFJJUEVfU0VDUkVUX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWT0ke1NUUklQRV9TSUdOSU5HX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWV9DT05ORUNUPSR7U1RSSVBFX1NJR05JTkdfS0VZX0NPTk5FQ1R9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGl6X2NvbmZpZzovY29uZmlnLycKICAgICAgLSAncG9zdGl6X3VwbG9hZHM6L3VwbG9hZHMvJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICALine truncated
|
||||
"tags": [
|
||||
"post everywhere",
|
||||
"social media",
|
||||
@@ -3564,7 +3596,7 @@
|
||||
"pterodactyl-with-wings": {
|
||||
"documentation": "https://pterodactyl.io/?utm_source=coolify.io",
|
||||
"slogan": "Pterodactyl is a free, open-source game server management panel",
|
||||
"compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMS4xMScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhbmVsLXZhcjovYXBwL3Zhci8nCiAgICAgIC0gJ3BhbmVsLW5naW54Oi9ldGMvbmdpbngvaHR0cC5kLycKICAgICAgLSAncGFuZWwtY2VydHM6L2V0Yy9sZXRzZW5jcnlwdC8nCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2V0Yy9lbnRyeXBvaW50LnNoCiAgICAgICAgdGFyZ2V0OiAvZW50cnlwb2ludC5zaAogICAgICAgIG1vZGU6ICcwNzU1JwogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vc2hcbnNldCAtZVxuXG4gZWNobyBcIlNldHRpbmcgbG9ncyBwZXJtaXNzaW9ucy4uLlwiXG4gY2hvd24gLVIgbmdpbng6IC9hcHAvc3RvcmFnZS9sb2dzL1xuXG4gVVNFUl9FWElTVFM9JChwaHAgYXJ0aXNhbiB0aW5rZXIgLS1uby1hbnNpIC0tZXhlY3V0ZT0nZWNobyBcXFB0ZXJvZGFjdHlsXFxNb2RlbHNcXFVzZXI6OndoZXJlKFwiZW1haWxcIiwgXCInXCIkQURNSU5fRU1BSUxcIidcIiktPmV4aXN0cygpID8gXCIxXCIgOiBcIjBcIjsnKVxuXG4gaWYgWyBcIiRVU0VSX0VYSVNUU1wiID0gXCIwXCIgXTsgdGhlblxuICAgZWNobyBcIkFkbWluIFVzZXIgZG9lcyBub3QgZXhpc3QsIGNyZWF0aW5nIHVzZXIgbm93LlwiXG4gICBwaHAgYXJ0aXNhbiBwOnVzZXI6bWFrZSAtLW5vLWludGVyYWN0aW9uIFxcXG4gICAgIC0tYWRtaW49MSBcXFxuICAgICAtLWVtYWlsPVwiJEFETUlOX0VNQUlMXCIgXFxcbiAgICAgLS11c2VybmFtZT1cIiRBRE1JTl9VU0VSTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1maXJzdD1cIiRBRE1JTl9GSVJTVE5BTUVcIiBcXFxuICAgICAtLW5hbWUtbGFzdD1cIiRBRE1JTl9MQVNUTkFNRVwiIFxcXG4gICAgIC0tcGFzc3dvcmQ9XCIkQURNSU5fUEFTU1dPUkRcIlxuICAgZWNobyBcIkFkbWluIHVzZXIgY3JlYXRlZCBzdWNjZXNzZnVsbHkhXCJcbiBlbHNlXG4gICBlY2hvIFwiQWRtaW4gVXNlciBhbHJlYWR5IGV4aXN0cywgc2tpcHBpbmcgY3JlYXRpb24uXCJcbiBmaVxuXG4gZXhlYyBzdXBlcnZpc29yZCAtLW5vZGFlbW9uXG4iCiAgICBjb21tYW5kOgogICAgICAtIC9lbnRyeXBvaW50LnNoCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ2N1cmwgLXNmIGh0dHA6Ly9sb2NhbGhvc3Q6ODAgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIEhBU0hJRFNfU0FMVD0kU0VSVklDRV9QQVNTV09SRF9IQVNISURTCiAgICAgIC0gSEFTSElEU19MRU5HVEg9OAogICAgICAtIFNFUlZJQ0VfRlFETl9QVEVST0RBQ1RZTF84MAogICAgICAtICdBRE1JTl9FTUFJTD0ke0FETUlOX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0FETUlOX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnQURNSU5fRklSU1ROQU1FPSR7QURNSU5fRklSU1ROQU1FOi1BZG1pbn0nCiAgICAgIC0gJ0FETUlOX0xBU1ROQU1FPSR7QURNSU5fTEFTVE5BTUU6LVVzZXJ9JwogICAgICAtICdBRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgICAtICdQVEVST0RBQ1RZTF9IVFRQUz0ke1BURVJPREFDVFlMX0hUVFBTOi1mYWxzZX0nCiAgICAgIC0gQVBQX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gQVBQX0VOVklST05NRU5UX09OTFk9ZmFsc2UKICAgICAgLSBBUFBfVVJMPSRTRVJWSUNFX0ZRRE5fUFRFUk9EQUNUWUwKICAgICAgLSAnQVBQX1RJTUVaT05FPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0FQUF9TRVJWSUNFX0FVVEhPUj0ke0FQUF9TRVJWSUNFX0FVVEhPUjotYXV0aG9yQGV4YW1wbGUuY29tfScKICAgICAgLSAnTE9HX0xFVkVMPSR7TE9HX0xFVkVMOi1kZWJ1Z30nCiAgICAgIC0gQ0FDSEVfRFJJVkVSPXJlZGlzCiAgICAgIC0gU0VTU0lPTl9EUklWRVI9cmVkaXMKICAgICAgLSBRVUVVRV9EUklWRVI9cmVkaXMKICAgICAgLSBSRURJU19IT1NUPXJlZGlzCiAgICAgIC0gREJfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBNQUlMX0ZST009JE1BSUxfRlJPTQogICAgICAtIE1BSUxfRFJJVkVSPSRNQUlMX0RSSVZFUgogICAgICAtIE1BSUxfSE9TVD0kTUFJTF9IT1NUCiAgICAgIC0gTUFJTF9QT1JUPSRNQUlMX1BPUlQKICAgICAgLSBNQUlMX1VTRVJOQU1FPSRNQUlMX1VTRVJOQU1FCiAgICAgIC0gTUFJTF9QQVNTV09SRD0kTUFJTF9QQVNTV09SRAogICAgICAtIE1BSUxfRU5DUllQVElPTj0kTUFJTF9FTkNSWVBUSU9OCiAgd2luZ3M6CiAgICBpbWFnZTogJ2doY3IuaW8vcHRlcm9kYWN0eWwvd2luZ3M6djEuMTEuMTMnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1dJTkdTXzg0NDMKICAgICAgLSAnVFo9JHtUSU1FWk9ORTotVVRDfScKICAgICAgLSBXSU5HU19VU0VSTkFNRT1wdGVyb2RhY3R5bAogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJy92YXIvbGliL2RvY2tlci9jb250YWluZXJzLzovdmFyL2xpYi9kb2NrZXIvY29udGFpbmVycy8nCiAgICAgIC0gJy92YXIvbGliL3B0ZXJvZGFjdHlsLzovdmFyL2xpYi9wdGVyb2RhY3R5bC8nCiAgICAgIC0gJy90bXAvcHRlcm9kYWN0eWwvOi90bXAvcHRlcm9kYWN0eWwvJwogICAgICAtICd3aW5ncy1sb2dzOi92YXIvbG9nL3B0ZXJvZGFjdHlsLycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2NvbmZpZy55bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvcHRlcm9kYWN0eWwvY29uZmlnLnltbAogICAgICALine truncated
|
||||
"compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMS4xMScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhbmVsLXZhcjovYXBwL3Zhci8nCiAgICAgIC0gJ3BhbmVsLW5naW54Oi9ldGMvbmdpbngvaHR0cC5kLycKICAgICAgLSAncGFuZWwtY2VydHM6L2V0Yy9sZXRzZW5jcnlwdC8nCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2V0Yy9lbnRyeXBvaW50LnNoCiAgICAgICAgdGFyZ2V0OiAvZW50cnlwb2ludC5zaAogICAgICAgIG1vZGU6ICcwNzU1JwogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vc2hcbnNldCAtZVxuXG4gZWNobyBcIlNldHRpbmcgbG9ncyBwZXJtaXNzaW9ucy4uLlwiXG4gY2hvd24gLVIgbmdpbng6IC9hcHAvc3RvcmFnZS9sb2dzL1xuXG4gVVNFUl9FWElTVFM9JChwaHAgYXJ0aXNhbiB0aW5rZXIgLS1uby1hbnNpIC0tZXhlY3V0ZT0nZWNobyBcXFB0ZXJvZGFjdHlsXFxNb2RlbHNcXFVzZXI6OndoZXJlKFwiZW1haWxcIiwgXCInXCIkQURNSU5fRU1BSUxcIidcIiktPmV4aXN0cygpID8gXCIxXCIgOiBcIjBcIjsnKVxuXG4gaWYgWyBcIiRVU0VSX0VYSVNUU1wiID0gXCIwXCIgXTsgdGhlblxuICAgZWNobyBcIkFkbWluIFVzZXIgZG9lcyBub3QgZXhpc3QsIGNyZWF0aW5nIHVzZXIgbm93LlwiXG4gICBwaHAgYXJ0aXNhbiBwOnVzZXI6bWFrZSAtLW5vLWludGVyYWN0aW9uIFxcXG4gICAgIC0tYWRtaW49MSBcXFxuICAgICAtLWVtYWlsPVwiJEFETUlOX0VNQUlMXCIgXFxcbiAgICAgLS11c2VybmFtZT1cIiRBRE1JTl9VU0VSTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1maXJzdD1cIiRBRE1JTl9GSVJTVE5BTUVcIiBcXFxuICAgICAtLW5hbWUtbGFzdD1cIiRBRE1JTl9MQVNUTkFNRVwiIFxcXG4gICAgIC0tcGFzc3dvcmQ9XCIkQURNSU5fUEFTU1dPUkRcIlxuICAgZWNobyBcIkFkbWluIHVzZXIgY3JlYXRlZCBzdWNjZXNzZnVsbHkhXCJcbiBlbHNlXG4gICBlY2hvIFwiQWRtaW4gVXNlciBhbHJlYWR5IGV4aXN0cywgc2tpcHBpbmcgY3JlYXRpb24uXCJcbiBmaVxuXG4gZXhlYyBzdXBlcnZpc29yZCAtLW5vZGFlbW9uXG4iCiAgICBjb21tYW5kOgogICAgICAtIC9lbnRyeXBvaW50LnNoCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ2N1cmwgLXNmIGh0dHA6Ly9sb2NhbGhvc3Q6ODAgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIEhBU0hJRFNfU0FMVD0kU0VSVklDRV9QQVNTV09SRF9IQVNISURTCiAgICAgIC0gSEFTSElEU19MRU5HVEg9OAogICAgICAtIFNFUlZJQ0VfRlFETl9QVEVST0RBQ1RZTF84MAogICAgICAtICdBRE1JTl9FTUFJTD0ke0FETUlOX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0FETUlOX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnQURNSU5fRklSU1ROQU1FPSR7QURNSU5fRklSU1ROQU1FOi1BZG1pbn0nCiAgICAgIC0gJ0FETUlOX0xBU1ROQU1FPSR7QURNSU5fTEFTVE5BTUU6LVVzZXJ9JwogICAgICAtICdBRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgICAtICdQVEVST0RBQ1RZTF9IVFRQUz0ke1BURVJPREFDVFlMX0hUVFBTOi1mYWxzZX0nCiAgICAgIC0gQVBQX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gQVBQX0VOVklST05NRU5UX09OTFk9ZmFsc2UKICAgICAgLSBBUFBfVVJMPSRTRVJWSUNFX0ZRRE5fUFRFUk9EQUNUWUwKICAgICAgLSAnQVBQX1RJTUVaT05FPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0FQUF9TRVJWSUNFX0FVVEhPUj0ke0FQUF9TRVJWSUNFX0FVVEhPUjotYXV0aG9yQGV4YW1wbGUuY29tfScKICAgICAgLSAnTE9HX0xFVkVMPSR7TE9HX0xFVkVMOi1kZWJ1Z30nCiAgICAgIC0gQ0FDSEVfRFJJVkVSPXJlZGlzCiAgICAgIC0gU0VTU0lPTl9EUklWRVI9cmVkaXMKICAgICAgLSBRVUVVRV9EUklWRVI9cmVkaXMKICAgICAgLSBSRURJU19IT1NUPXJlZGlzCiAgICAgIC0gREJfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBNQUlMX0ZST009JE1BSUxfRlJPTQogICAgICAtIE1BSUxfRFJJVkVSPSRNQUlMX0RSSVZFUgogICAgICAtIE1BSUxfSE9TVD0kTUFJTF9IT1NUCiAgICAgIC0gTUFJTF9QT1JUPSRNQUlMX1BPUlQKICAgICAgLSBNQUlMX1VTRVJOQU1FPSRNQUlMX1VTRVJOQU1FCiAgICAgIC0gTUFJTF9QQVNTV09SRD0kTUFJTF9QQVNTV09SRAogICAgICAtIE1BSUxfRU5DUllQVElPTj0kTUFJTF9FTkNSWVBUSU9OCiAgd2luZ3M6CiAgICBpbWFnZTogJ2doY3IuaW8vcHRlcm9kYWN0eWwvd2luZ3M6djEuMTEuMTMnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgcG9ydHM6CiAgICAgIC0gJzIwMjI6MjAyMicKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XSU5HU184NDQzCiAgICAgIC0gJ1RaPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gV0lOR1NfVVNFUk5BTUU9cHRlcm9kYWN0eWwKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgICAtICcvdmFyL2xpYi9kb2NrZXIvY29udGFpbmVycy86L3Zhci9saWIvZG9ja2VyL2NvbnRhaW5lcnMvJwogICAgICAtICcvdmFyL2xpYi9wdGVyb2RhY3R5bC86L3Zhci9saWIvcHRlcm9kYWN0eWwvJwogICAgICAtICcvdG1wL3B0ZXJvZGFjdHlsLzovdG1wL3B0ZXJvZGFjdHlsLycKICAgICAgLSAnd2luZ3MtbG9nczovdmFyL2xvZy9wdGVyb2RhY3R5bC8nCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2V0Yy9jb25maWcueW1sCiAgICAgICAgdGFyZ2V0OiAvZXRLine truncated
|
||||
"tags": [
|
||||
"game",
|
||||
"game server",
|
||||
@@ -4156,6 +4188,18 @@
|
||||
"minversion": "0.0.0",
|
||||
"port": "3000"
|
||||
},
|
||||
"terraria-server": {
|
||||
"documentation": "https://github.com/hexlo/terraria-server-docker?utm_source=coolify.io",
|
||||
"slogan": "Docker multi-arch Image for Terraria Server.",
|
||||
"compose": "c2VydmljZXM6CiAgdGVycmFyaWEtc2VydmVyOgogICAgaW1hZ2U6ICdoZXhsby90ZXJyYXJpYS1zZXJ2ZXItZG9ja2VyOjEuNC40LjknCiAgICBzdGRpbl9vcGVuOiB0cnVlCiAgICB0dHk6IHRydWUKICAgIHBvcnRzOgogICAgICAtICc3Nzc3Ojc3NzcnCiAgICB2b2x1bWVzOgogICAgICAtICd0ZXJyYXJpYS1zZXJ2ZXI6L3Jvb3QvLmxvY2FsL3NoYXJlL1RlcnJhcmlhL1dvcmxkcycKICAgIGVudmlyb25tZW50OgogICAgICAtICdXT1JMRD0ke1dPUkxEOi0vcm9vdC8ubG9jYWwvc2hhcmUvVGVycmFyaWEvV29ybGRzL3dvcmxkMS53bGR9JwogICAgICAtICdBVVRPQ1JFQVRFPSR7QVVUT0NSRUFURTotMn0nCiAgICAgIC0gJ1dPUkxETkFNRT0ke1dPUkxETkFNRTotd29ybGQxfScKICAgICAgLSAnRElGRklDVUxUWT0ke0RJRkZJQ1VMVFk6LTF9JwogICAgICAtICdNQVhQTEFZRVJTPSR7TUFYUExBWUVSUzotOH0nCiAgICAgIC0gJ1BBU1NXT1JEPSR7UEFTU1dPUkQ6LW15cGFzc3dvcmR9JwogICAgICAtICdNT1REPSR7TU9URDotV2VsY29tZSB0byB0aGUgc2VydmVyIX0nCiAgICAgIC0gJ0xBTkdVQUdFPSR7TEFOR1VBR0U6LWVuL1VTfScKICAgICAgLSAnU0VDVVJFPSR7U0VDVVJFOi0xfScK",
|
||||
"tags": [
|
||||
"terraria"
|
||||
],
|
||||
"category": "games",
|
||||
"logo": "svgs/terraria.svg",
|
||||
"minversion": "0.0.0",
|
||||
"port": "7777"
|
||||
},
|
||||
"tolgee": {
|
||||
"documentation": "https://tolgee.io/?utm_source=coolify.io",
|
||||
"slogan": "Tolgee is a localization management platform for developers and translators.",
|
||||
@@ -4272,7 +4316,7 @@
|
||||
"umami": {
|
||||
"documentation": "https://umami.is?utm_source=coolify.io",
|
||||
"slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.",
|
||||
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6cG9zdGdyZXNxbC1sYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
|
||||
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
|
||||
"tags": [
|
||||
"analytics",
|
||||
"insights",
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
use App\Services\HetznerService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
beforeEach(function () {
|
||||
Http::preventStrayRequests();
|
||||
});
|
||||
|
||||
it('getServers returns list of servers from Hetzner API', function () {
|
||||
Http::fake([
|
||||
'api.hetzner.cloud/v1/servers*' => Http::response([
|
||||
'servers' => [
|
||||
[
|
||||
'id' => 12345,
|
||||
'name' => 'test-server-1',
|
||||
'status' => 'running',
|
||||
'public_net' => [
|
||||
'ipv4' => ['ip' => '123.45.67.89'],
|
||||
'ipv6' => ['ip' => '2a01:4f8::/64'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 67890,
|
||||
'name' => 'test-server-2',
|
||||
'status' => 'off',
|
||||
'public_net' => [
|
||||
'ipv4' => ['ip' => '98.76.54.32'],
|
||||
'ipv6' => ['ip' => '2a01:4f9::/64'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$service = new HetznerService('fake-token');
|
||||
$servers = $service->getServers();
|
||||
|
||||
expect($servers)->toBeArray()
|
||||
->and(count($servers))->toBe(2)
|
||||
->and($servers[0]['id'])->toBe(12345)
|
||||
->and($servers[1]['id'])->toBe(67890);
|
||||
});
|
||||
|
||||
it('findServerByIp returns matching server by IPv4', function () {
|
||||
Http::fake([
|
||||
'api.hetzner.cloud/v1/servers*' => Http::response([
|
||||
'servers' => [
|
||||
[
|
||||
'id' => 12345,
|
||||
'name' => 'test-server',
|
||||
'status' => 'running',
|
||||
'public_net' => [
|
||||
'ipv4' => ['ip' => '123.45.67.89'],
|
||||
'ipv6' => ['ip' => '2a01:4f8::/64'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$service = new HetznerService('fake-token');
|
||||
$result = $service->findServerByIp('123.45.67.89');
|
||||
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result['id'])->toBe(12345)
|
||||
->and($result['name'])->toBe('test-server');
|
||||
});
|
||||
|
||||
it('findServerByIp returns null when no match', function () {
|
||||
Http::fake([
|
||||
'api.hetzner.cloud/v1/servers*' => Http::response([
|
||||
'servers' => [
|
||||
[
|
||||
'id' => 12345,
|
||||
'name' => 'test-server',
|
||||
'status' => 'running',
|
||||
'public_net' => [
|
||||
'ipv4' => ['ip' => '123.45.67.89'],
|
||||
'ipv6' => ['ip' => '2a01:4f8::/64'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$service = new HetznerService('fake-token');
|
||||
$result = $service->findServerByIp('1.2.3.4');
|
||||
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
it('findServerByIp returns null when server list is empty', function () {
|
||||
Http::fake([
|
||||
'api.hetzner.cloud/v1/servers*' => Http::response([
|
||||
'servers' => [],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$service = new HetznerService('fake-token');
|
||||
$result = $service->findServerByIp('123.45.67.89');
|
||||
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
it('findServerByIp matches correct server among multiple', function () {
|
||||
Http::fake([
|
||||
'api.hetzner.cloud/v1/servers*' => Http::response([
|
||||
'servers' => [
|
||||
[
|
||||
'id' => 11111,
|
||||
'name' => 'server-a',
|
||||
'status' => 'running',
|
||||
'public_net' => [
|
||||
'ipv4' => ['ip' => '10.0.0.1'],
|
||||
'ipv6' => ['ip' => '2a01:4f8::/64'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 22222,
|
||||
'name' => 'server-b',
|
||||
'status' => 'running',
|
||||
'public_net' => [
|
||||
'ipv4' => ['ip' => '10.0.0.2'],
|
||||
'ipv6' => ['ip' => '2a01:4f9::/64'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 33333,
|
||||
'name' => 'server-c',
|
||||
'status' => 'off',
|
||||
'public_net' => [
|
||||
'ipv4' => ['ip' => '10.0.0.3'],
|
||||
'ipv6' => ['ip' => '2a01:4fa::/64'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$service = new HetznerService('fake-token');
|
||||
$result = $service->findServerByIp('10.0.0.2');
|
||||
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result['id'])->toBe(22222)
|
||||
->and($result['name'])->toBe('server-b');
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Unit tests to verify LocalFileVolume::isReadOnlyVolume() correctly detects
|
||||
* read-only volumes in both short-form and long-form Docker Compose syntax.
|
||||
*
|
||||
* Related Issue: Volumes with read_only: true in long-form syntax were not
|
||||
* being detected as read-only, allowing UI edits on files that should be protected.
|
||||
*
|
||||
* Related Files:
|
||||
* - app/Models/LocalFileVolume.php
|
||||
* - app/Livewire/Project/Service/FileStorage.php
|
||||
*/
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* Helper function to parse volumes and detect read-only status.
|
||||
* This mirrors the logic in LocalFileVolume::isReadOnlyVolume()
|
||||
*
|
||||
* Note: We match on mount_path (container path) only, since fs_path gets transformed
|
||||
* from relative (./file) to absolute (/data/coolify/services/uuid/file) during parsing
|
||||
*/
|
||||
function isVolumeReadOnly(string $dockerComposeRaw, string $serviceName, string $mountPath): bool
|
||||
{
|
||||
$compose = Yaml::parse($dockerComposeRaw);
|
||||
|
||||
if (! isset($compose['services'][$serviceName]['volumes'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$volumes = $compose['services'][$serviceName]['volumes'];
|
||||
|
||||
foreach ($volumes as $volume) {
|
||||
// Volume can be string like "host:container:ro" or "host:container"
|
||||
if (is_string($volume)) {
|
||||
$parts = explode(':', $volume);
|
||||
|
||||
if (count($parts) >= 2) {
|
||||
$containerPath = $parts[1];
|
||||
$options = $parts[2] ?? null;
|
||||
|
||||
if ($containerPath === $mountPath) {
|
||||
return $options === 'ro';
|
||||
}
|
||||
}
|
||||
} elseif (is_array($volume)) {
|
||||
// Long-form syntax: { type: bind, source: ..., target: ..., read_only: true }
|
||||
$containerPath = data_get($volume, 'target');
|
||||
$readOnly = data_get($volume, 'read_only', false);
|
||||
|
||||
if ($containerPath === $mountPath) {
|
||||
return $readOnly === true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
test('detects read-only with short-form syntax using :ro', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
garage:
|
||||
image: example/image
|
||||
volumes:
|
||||
- ./config.toml:/etc/config.toml:ro
|
||||
YAML;
|
||||
|
||||
expect(isVolumeReadOnly($compose, 'garage', '/etc/config.toml'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('detects writable with short-form syntax without :ro', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
garage:
|
||||
image: example/image
|
||||
volumes:
|
||||
- ./config.toml:/etc/config.toml
|
||||
YAML;
|
||||
|
||||
expect(isVolumeReadOnly($compose, 'garage', '/etc/config.toml'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('detects read-only with long-form syntax and read_only: true', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
garage:
|
||||
image: example/image
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./garage.toml
|
||||
target: /etc/garage.toml
|
||||
read_only: true
|
||||
YAML;
|
||||
|
||||
expect(isVolumeReadOnly($compose, 'garage', '/etc/garage.toml'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('detects writable with long-form syntax and read_only: false', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
garage:
|
||||
image: example/image
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./garage.toml
|
||||
target: /etc/garage.toml
|
||||
read_only: false
|
||||
YAML;
|
||||
|
||||
expect(isVolumeReadOnly($compose, 'garage', '/etc/garage.toml'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('detects writable with long-form syntax without read_only key', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
garage:
|
||||
image: example/image
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./garage.toml
|
||||
target: /etc/garage.toml
|
||||
YAML;
|
||||
|
||||
expect(isVolumeReadOnly($compose, 'garage', '/etc/garage.toml'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('handles mixed short-form and long-form volumes in same service', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
garage:
|
||||
image: example/image
|
||||
volumes:
|
||||
- ./data:/var/data
|
||||
- type: bind
|
||||
source: ./config.toml
|
||||
target: /etc/config.toml
|
||||
read_only: true
|
||||
YAML;
|
||||
|
||||
expect(isVolumeReadOnly($compose, 'garage', '/var/data'))->toBeFalse();
|
||||
expect(isVolumeReadOnly($compose, 'garage', '/etc/config.toml'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('handles same file mounted in multiple services with different read_only settings', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
garage:
|
||||
image: example/garage
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./garage.toml
|
||||
target: /etc/garage.toml
|
||||
garage-webui:
|
||||
image: example/webui
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./garage.toml
|
||||
target: /etc/garage.toml
|
||||
read_only: true
|
||||
YAML;
|
||||
|
||||
// Same file, different services, different read_only status
|
||||
expect(isVolumeReadOnly($compose, 'garage', '/etc/garage.toml'))->toBeFalse();
|
||||
expect(isVolumeReadOnly($compose, 'garage-webui', '/etc/garage.toml'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('handles volume mount type', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
app:
|
||||
image: example/app
|
||||
volumes:
|
||||
- type: volume
|
||||
source: mydata
|
||||
target: /data
|
||||
read_only: true
|
||||
YAML;
|
||||
|
||||
expect(isVolumeReadOnly($compose, 'app', '/data'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('returns false when service has no volumes', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
garage:
|
||||
image: example/image
|
||||
YAML;
|
||||
|
||||
expect(isVolumeReadOnly($compose, 'garage', '/etc/config.toml'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('returns false when service does not exist', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
garage:
|
||||
image: example/image
|
||||
volumes:
|
||||
- ./config.toml:/etc/config.toml:ro
|
||||
YAML;
|
||||
|
||||
expect(isVolumeReadOnly($compose, 'nonexistent', '/etc/config.toml'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('returns false when mount path does not match', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
garage:
|
||||
image: example/image
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./other.toml
|
||||
target: /etc/other.toml
|
||||
read_only: true
|
||||
YAML;
|
||||
|
||||
expect(isVolumeReadOnly($compose, 'garage', '/etc/config.toml'))->toBeFalse();
|
||||
});
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.454"
|
||||
"version": "4.0.0-beta.455"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.455"
|
||||
"version": "4.0.0-beta.456"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.12"
|
||||
|
||||
Reference in new issue
Block a user