Merge branch 'next' into fix-volume-variable-with-default
This commit is contained in:
@@ -13,6 +13,7 @@ Coolify is an open-source, self-hostable platform for deploying applications and
|
||||
- `npm run build` - Build frontend assets for production
|
||||
|
||||
### Backend Development
|
||||
Only run artisan commands inside "coolify" container when in development.
|
||||
- `php artisan serve` - Start Laravel development server
|
||||
- `php artisan migrate` - Run database migrations
|
||||
- `php artisan queue:work` - Start queue worker for background jobs
|
||||
|
@@ -53,6 +53,7 @@ Thank you so much!
|
||||
|
||||
## Big Sponsors
|
||||
|
||||
* [CubePath](https://cubepath.com/?ref=coolify.io) - Dedicated Servers & Instant Deploy
|
||||
* [GlueOps](https://www.glueops.dev?ref=coolify.io) - DevOps automation and infrastructure management
|
||||
* [Algora](https://algora.io?ref=coolify.io) - Open source contribution platform
|
||||
* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform
|
||||
@@ -87,8 +88,11 @@ Thank you so much!
|
||||
* [Gozunga](https://gozunga.com?ref=coolify.io) - Seriously Simple Cloud Infrastructure
|
||||
* [Macarne](https://macarne.com?ref=coolify.io) - Best IP Transit & Carrier Ethernet Solutions for Simplified Network Connectivity
|
||||
|
||||
|
||||
## Small Sponsors
|
||||
|
||||
<a href="https://open-elements.com/?utm_source=coolify.io"><img width="60px" alt="OpenElements" src="https://github.com/OpenElements.png"/></a>
|
||||
<a href="https://xaman.app/?utm_source=coolify.io"><img width="60px" alt="XamanApp" src="https://github.com/XamanApp.png"/></a>
|
||||
<a href="https://www.uxwizz.com/?utm_source=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a>
|
||||
<a href="https://evercam.io/?utm_source=coolify.io"><img width="60px" alt="Evercam" src="https://github.com/evercam.png"/></a>
|
||||
<a href="https://github.com/iujlaki"><img width="60px" alt="Imre Ujlaki" src="https://github.com/iujlaki.png"/></a>
|
||||
|
@@ -185,6 +185,8 @@ class StartPostgresql
|
||||
}
|
||||
}
|
||||
|
||||
$command = ['postgres'];
|
||||
|
||||
if (filled($this->database->postgres_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
@@ -195,29 +197,25 @@ class StartPostgresql
|
||||
'read_only' => true,
|
||||
]]
|
||||
);
|
||||
$docker_compose['services'][$container_name]['command'] = [
|
||||
'postgres',
|
||||
'-c',
|
||||
'config_file=/etc/postgresql/postgresql.conf',
|
||||
];
|
||||
$command = array_merge($command, ['-c', 'config_file=/etc/postgresql/postgresql.conf']);
|
||||
}
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$docker_compose['services'][$container_name]['command'] = [
|
||||
'postgres',
|
||||
'-c',
|
||||
'ssl=on',
|
||||
'-c',
|
||||
'ssl_cert_file=/var/lib/postgresql/certs/server.crt',
|
||||
'-c',
|
||||
'ssl_key_file=/var/lib/postgresql/certs/server.key',
|
||||
];
|
||||
$command = array_merge($command, [
|
||||
'-c', 'ssl=on',
|
||||
'-c', 'ssl_cert_file=/var/lib/postgresql/certs/server.crt',
|
||||
'-c', 'ssl_key_file=/var/lib/postgresql/certs/server.key',
|
||||
]);
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
if (count($command) > 1) {
|
||||
$docker_compose['services'][$container_name]['command'] = $command;
|
||||
}
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
|
@@ -16,7 +16,7 @@ class Services extends Command
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $description = 'Generate service-templates.yaml based on /templates/compose directory';
|
||||
protected $description = 'Generates service-templates json file based on /templates/compose directory';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
@@ -33,7 +33,10 @@ class Services extends Command
|
||||
];
|
||||
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL);
|
||||
file_put_contents(base_path('templates/'.config('constants.services.file_name')), $serviceTemplatesJson.PHP_EOL);
|
||||
|
||||
// Generate service-templates.json with SERVICE_URL changed to SERVICE_FQDN
|
||||
$this->generateServiceTemplatesWithFqdn();
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
@@ -86,4 +89,143 @@ class Services extends Command
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function generateServiceTemplatesWithFqdn(): void
|
||||
{
|
||||
$serviceTemplatesWithFqdn = collect(array_merge(
|
||||
glob(base_path('templates/compose/*.yaml')),
|
||||
glob(base_path('templates/compose/*.yml'))
|
||||
))
|
||||
->mapWithKeys(function ($file): array {
|
||||
$file = basename($file);
|
||||
$parsed = $this->processFileWithFqdn($file);
|
||||
|
||||
return $parsed === false ? [] : [
|
||||
Arr::pull($parsed, 'name') => $parsed,
|
||||
];
|
||||
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesWithFqdn.PHP_EOL);
|
||||
|
||||
// Generate service-templates-raw.json with non-base64 encoded compose content
|
||||
// $this->generateServiceTemplatesRaw();
|
||||
}
|
||||
|
||||
private function processFileWithFqdn(string $file): false|array
|
||||
{
|
||||
$content = file_get_contents(base_path("templates/compose/$file"));
|
||||
|
||||
$data = collect(explode(PHP_EOL, $content))->mapWithKeys(function ($line): array {
|
||||
preg_match('/^#(?<key>.*):(?<value>.*)$/U', $line, $m);
|
||||
|
||||
return $m ? [trim($m['key']) => trim($m['value'])] : [];
|
||||
});
|
||||
|
||||
if (str($data->get('ignore'))->toBoolean()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$documentation = $data->get('documentation');
|
||||
$documentation = $documentation ? $documentation.'?utm_source=coolify.io' : 'https://coolify.io/docs';
|
||||
|
||||
// Replace SERVICE_URL with SERVICE_FQDN in the content
|
||||
$modifiedContent = str_replace('SERVICE_URL', 'SERVICE_FQDN', $content);
|
||||
|
||||
$json = Yaml::parse($modifiedContent);
|
||||
$compose = base64_encode(Yaml::dump($json, 10, 2));
|
||||
|
||||
$tags = str($data->get('tags'))->lower()->explode(',')->map(fn ($tag) => trim($tag))->filter();
|
||||
$tags = $tags->isEmpty() ? null : $tags->all();
|
||||
|
||||
$payload = [
|
||||
'name' => pathinfo($file, PATHINFO_FILENAME),
|
||||
'documentation' => $documentation,
|
||||
'slogan' => $data->get('slogan', str($file)->headline()),
|
||||
'compose' => $compose,
|
||||
'tags' => $tags,
|
||||
'logo' => $data->get('logo', 'svgs/default.webp'),
|
||||
'minversion' => $data->get('minversion', '0.0.0'),
|
||||
];
|
||||
|
||||
if ($port = $data->get('port')) {
|
||||
$payload['port'] = $port;
|
||||
}
|
||||
|
||||
if ($envFile = $data->get('env_file')) {
|
||||
$envFileContent = file_get_contents(base_path("templates/compose/$envFile"));
|
||||
// Also replace SERVICE_URL with SERVICE_FQDN in env file content
|
||||
$modifiedEnvContent = str_replace('SERVICE_URL', 'SERVICE_FQDN', $envFileContent);
|
||||
$payload['envs'] = base64_encode($modifiedEnvContent);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function generateServiceTemplatesRaw(): void
|
||||
{
|
||||
$serviceTemplatesRaw = collect(array_merge(
|
||||
glob(base_path('templates/compose/*.yaml')),
|
||||
glob(base_path('templates/compose/*.yml'))
|
||||
))
|
||||
->mapWithKeys(function ($file): array {
|
||||
$file = basename($file);
|
||||
$parsed = $this->processFileWithFqdnRaw($file);
|
||||
|
||||
return $parsed === false ? [] : [
|
||||
Arr::pull($parsed, 'name') => $parsed,
|
||||
];
|
||||
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
file_put_contents(base_path('templates/service-templates-raw.json'), $serviceTemplatesRaw.PHP_EOL);
|
||||
}
|
||||
|
||||
private function processFileWithFqdnRaw(string $file): false|array
|
||||
{
|
||||
$content = file_get_contents(base_path("templates/compose/$file"));
|
||||
|
||||
$data = collect(explode(PHP_EOL, $content))->mapWithKeys(function ($line): array {
|
||||
preg_match('/^#(?<key>.*):(?<value>.*)$/U', $line, $m);
|
||||
|
||||
return $m ? [trim($m['key']) => trim($m['value'])] : [];
|
||||
});
|
||||
|
||||
if (str($data->get('ignore'))->toBoolean()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$documentation = $data->get('documentation');
|
||||
$documentation = $documentation ? $documentation.'?utm_source=coolify.io' : 'https://coolify.io/docs';
|
||||
|
||||
// Replace SERVICE_URL with SERVICE_FQDN in the content
|
||||
$modifiedContent = str_replace('SERVICE_URL', 'SERVICE_FQDN', $content);
|
||||
|
||||
$json = Yaml::parse($modifiedContent);
|
||||
$compose = Yaml::dump($json, 10, 2); // Not base64 encoded
|
||||
|
||||
$tags = str($data->get('tags'))->lower()->explode(',')->map(fn ($tag) => trim($tag))->filter();
|
||||
$tags = $tags->isEmpty() ? null : $tags->all();
|
||||
|
||||
$payload = [
|
||||
'name' => pathinfo($file, PATHINFO_FILENAME),
|
||||
'documentation' => $documentation,
|
||||
'slogan' => $data->get('slogan', str($file)->headline()),
|
||||
'compose' => $compose,
|
||||
'tags' => $tags,
|
||||
'logo' => $data->get('logo', 'svgs/default.webp'),
|
||||
'minversion' => $data->get('minversion', '0.0.0'),
|
||||
];
|
||||
|
||||
if ($port = $data->get('port')) {
|
||||
$payload['port'] = $port;
|
||||
}
|
||||
|
||||
if ($envFile = $data->get('env_file')) {
|
||||
$envFileContent = file_get_contents(base_path("templates/compose/$envFile"));
|
||||
// Also replace SERVICE_URL with SERVICE_FQDN in env file content (not base64 encoded)
|
||||
$modifiedEnvContent = str_replace('SERVICE_URL', 'SERVICE_FQDN', $envFileContent);
|
||||
$payload['envs'] = $modifiedEnvContent;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ namespace App\Console\Commands;
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\PullChangelogFromGitHub;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
@@ -64,6 +65,7 @@ class Init extends Command
|
||||
try {
|
||||
$this->cleanupUnnecessaryDynamicProxyConfiguration();
|
||||
$this->pullTemplatesFromCDN();
|
||||
$this->pullChangelogFromGitHub();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
|
||||
}
|
||||
@@ -74,6 +76,7 @@ class Init extends Command
|
||||
try {
|
||||
$this->cleanupInProgressApplicationDeployments();
|
||||
$this->pullTemplatesFromCDN();
|
||||
$this->pullChangelogFromGitHub();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
|
||||
}
|
||||
@@ -105,7 +108,17 @@ class Init extends Command
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
if ($response->successful()) {
|
||||
$services = $response->json();
|
||||
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
||||
File::put(base_path('templates/'.config('constants.services.file_name')), json_encode($services));
|
||||
}
|
||||
}
|
||||
|
||||
private function pullChangelogFromGitHub()
|
||||
{
|
||||
try {
|
||||
PullChangelogFromGitHub::dispatch();
|
||||
echo "Changelog fetch initiated\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not fetch changelog from GitHub: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
98
app/Console/Commands/InitChangelog.php
Normal file
98
app/Console/Commands/InitChangelog.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class InitChangelog extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'changelog:init {month? : Month in YYYY-MM format (defaults to current month)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Initialize a new monthly changelog file with example structure';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$month = $this->argument('month') ?: Carbon::now()->format('Y-m');
|
||||
|
||||
// Validate month format
|
||||
if (! preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) {
|
||||
$this->error('Invalid month format. Use YYYY-MM format with valid months 01-12 (e.g., 2025-08)');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$changelogsDir = base_path('changelogs');
|
||||
$filePath = $changelogsDir."/{$month}.json";
|
||||
|
||||
// Create changelogs directory if it doesn't exist
|
||||
if (! is_dir($changelogsDir)) {
|
||||
mkdir($changelogsDir, 0755, true);
|
||||
$this->info("Created changelogs directory: {$changelogsDir}");
|
||||
}
|
||||
|
||||
// Check if file already exists
|
||||
if (file_exists($filePath)) {
|
||||
if (! $this->confirm("File {$month}.json already exists. Overwrite?")) {
|
||||
$this->info('Operation cancelled');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the month for example data
|
||||
$carbonMonth = Carbon::createFromFormat('Y-m', $month);
|
||||
$monthName = $carbonMonth->format('F Y');
|
||||
$sampleDate = $carbonMonth->addDays(14)->toISOString(); // Mid-month
|
||||
|
||||
// Get version from config
|
||||
$version = 'v'.config('constants.coolify.version');
|
||||
|
||||
// Create example changelog structure
|
||||
$exampleData = [
|
||||
'entries' => [
|
||||
[
|
||||
'version' => $version,
|
||||
'title' => 'Example Feature Release',
|
||||
'content' => "This is an example changelog entry for {$monthName}. Replace this with your actual release notes. Include details about new features, improvements, bug fixes, and any breaking changes.",
|
||||
'published_at' => $sampleDate,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Write the file
|
||||
$jsonContent = json_encode($exampleData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if (file_put_contents($filePath, $jsonContent) === false) {
|
||||
$this->error("Failed to create changelog file: {$filePath}");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("✅ Created changelog file: changelogs/{$month}.json");
|
||||
$this->line(" Example entry created for {$monthName}");
|
||||
$this->line(' Edit the file to add your actual changelog entries');
|
||||
|
||||
// Show the file contents
|
||||
if ($this->option('verbose')) {
|
||||
$this->newLine();
|
||||
$this->line('File contents:');
|
||||
$this->line($jsonContent);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@@ -45,7 +45,7 @@ class SyncBunny extends Command
|
||||
$install_script = 'install.sh';
|
||||
$upgrade_script = 'upgrade.sh';
|
||||
$production_env = '.env.production';
|
||||
$service_template = 'service-templates.json';
|
||||
$service_template = config('constants.services.file_name');
|
||||
$versions = 'versions.json';
|
||||
|
||||
$compose_file_location = "$parent_dir/$compose_file";
|
||||
@@ -102,7 +102,7 @@ class SyncBunny extends Command
|
||||
}
|
||||
}
|
||||
if ($only_template) {
|
||||
$this->info('About to sync service-templates.json to BunnyCDN.');
|
||||
$this->info('About to sync '.config('constants.services.file_name').' to BunnyCDN.');
|
||||
$confirmed = confirm('Are you sure you want to sync?');
|
||||
if (! $confirmed) {
|
||||
return;
|
||||
|
@@ -6,6 +6,7 @@ use App\Jobs\CheckAndStartSentinelJob;
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\PullChangelogFromGitHub;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\RegenerateSslCertJob;
|
||||
use App\Jobs\ScheduledJobManager;
|
||||
@@ -67,6 +68,7 @@ class Kernel extends ConsoleKernel
|
||||
$this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer();
|
||||
|
||||
$this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
$this->scheduleInstance->job(new PullChangelogFromGitHub)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
|
||||
$this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
|
||||
$this->scheduleUpdates();
|
||||
|
@@ -447,4 +447,255 @@ class ProjectController extends Controller
|
||||
|
||||
return response()->json(['message' => 'Project deleted.']);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Environments',
|
||||
description: 'List all environments in a project.',
|
||||
path: '/projects/{uuid}/environments',
|
||||
operationId: 'get-environments',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Projects'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'List of environments',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/Environment')
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
description: 'Project not found.',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function get_environments(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'Project UUID is required.'], 422);
|
||||
}
|
||||
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $project) {
|
||||
return response()->json(['message' => 'Project not found.'], 404);
|
||||
}
|
||||
|
||||
$environments = $project->environments()->select('id', 'name', 'uuid')->get();
|
||||
|
||||
return response()->json(serializeApiResponse($environments));
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Environment',
|
||||
description: 'Create environment in project.',
|
||||
path: '/projects/{uuid}/environments',
|
||||
operationId: 'create-environment',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Projects'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
description: 'Environment created.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the environment.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Environment created.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'example' => 'env123', 'description' => 'The UUID of the environment.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
description: 'Project not found.',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 409,
|
||||
description: 'Environment with this name already exists.',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_environment(Request $request)
|
||||
{
|
||||
$allowedFields = ['name'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'string|max:255|required',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'Project UUID is required.'], 422);
|
||||
}
|
||||
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $project) {
|
||||
return response()->json(['message' => 'Project not found.'], 404);
|
||||
}
|
||||
|
||||
$existingEnvironment = $project->environments()->where('name', $request->name)->first();
|
||||
if ($existingEnvironment) {
|
||||
return response()->json(['message' => 'Environment with this name already exists.'], 409);
|
||||
}
|
||||
|
||||
$environment = $project->environments()->create([
|
||||
'name' => $request->name,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $environment->uuid,
|
||||
])->setStatusCode(201);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete Environment',
|
||||
description: 'Delete environment by name or UUID. Environment must be empty.',
|
||||
path: '/projects/{uuid}/environments/{environment_name_or_uuid}',
|
||||
operationId: 'delete-environment',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Projects'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'environment_name_or_uuid', in: 'path', required: true, description: 'Environment name or UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Environment deleted.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Environment deleted.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
description: 'Environment has resources, so it cannot be deleted.',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
description: 'Project or environment not found.',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_environment(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'Project UUID is required.'], 422);
|
||||
}
|
||||
if (! $request->environment_name_or_uuid) {
|
||||
return response()->json(['message' => 'Environment name or UUID is required.'], 422);
|
||||
}
|
||||
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $project) {
|
||||
return response()->json(['message' => 'Project not found.'], 404);
|
||||
}
|
||||
|
||||
$environment = $project->environments()->whereName($request->environment_name_or_uuid)->first();
|
||||
if (! $environment) {
|
||||
$environment = $project->environments()->whereUuid($request->environment_name_or_uuid)->first();
|
||||
}
|
||||
if (! $environment) {
|
||||
return response()->json(['message' => 'Environment not found.'], 404);
|
||||
}
|
||||
|
||||
if (! $environment->isEmpty()) {
|
||||
return response()->json(['message' => 'Environment has resources, so it cannot be deleted.'], 400);
|
||||
}
|
||||
|
||||
$environment->delete();
|
||||
|
||||
return response()->json(['message' => 'Environment deleted.']);
|
||||
}
|
||||
}
|
||||
|
@@ -1428,6 +1428,19 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$local_branch = "pull/{$this->pull_request_id}/head";
|
||||
}
|
||||
// Build an exact refspec for ls-remote so we don't match similarly named branches (e.g., changeset-release/main)
|
||||
if ($this->pull_request_id === 0) {
|
||||
$lsRemoteRef = "refs/heads/{$local_branch}";
|
||||
} else {
|
||||
if ($this->git_type === 'github' || $this->git_type === 'gitea') {
|
||||
$lsRemoteRef = "refs/pull/{$this->pull_request_id}/head";
|
||||
} elseif ($this->git_type === 'gitlab') {
|
||||
$lsRemoteRef = "refs/merge-requests/{$this->pull_request_id}/head";
|
||||
} else {
|
||||
// Fallback to the original value if provider-specific ref is unknown
|
||||
$lsRemoteRef = $local_branch;
|
||||
}
|
||||
}
|
||||
$private_key = data_get($this->application, 'private_key.private_key');
|
||||
if ($private_key) {
|
||||
$private_key = base64_encode($private_key);
|
||||
@@ -1442,7 +1455,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
|
||||
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$lsRemoteRef}"),
|
||||
'hidden' => true,
|
||||
'save' => 'git_commit_sha',
|
||||
]
|
||||
@@ -1450,7 +1463,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
|
||||
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$lsRemoteRef}"),
|
||||
'hidden' => true,
|
||||
'save' => 'git_commit_sha',
|
||||
],
|
||||
|
@@ -351,6 +351,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$size = $this->calculate_size();
|
||||
if ($this->backup->save_s3) {
|
||||
$this->upload_to_s3();
|
||||
|
||||
// If local backup is disabled, delete the local file immediately after S3 upload
|
||||
if ($this->backup->disable_local_backup) {
|
||||
deleteBackupsLocally($this->backup_location, $this->server);
|
||||
$this->add_to_backup_output('Local backup file deleted after S3 upload (disable_local_backup enabled).');
|
||||
}
|
||||
}
|
||||
|
||||
$this->team->notify(new BackupSuccess($this->backup, $this->database, $database));
|
||||
|
110
app/Jobs/PullChangelogFromGitHub.php
Normal file
110
app/Jobs/PullChangelogFromGitHub.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PullChangelogFromGitHub implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 30;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$response = Http::retry(3, 1000)
|
||||
->timeout(30)
|
||||
->get('https://api.github.com/repos/coollabsio/coolify/releases?per_page=10');
|
||||
|
||||
if ($response->successful()) {
|
||||
$releases = $response->json();
|
||||
$changelog = $this->transformReleasesToChangelog($releases);
|
||||
|
||||
// Group entries by month and save them
|
||||
$this->saveChangelogEntries($changelog);
|
||||
} else {
|
||||
send_internal_notification('PullChangelogFromGitHub failed with: '.$response->status().' '.$response->body());
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('PullChangelogFromGitHub failed with: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function transformReleasesToChangelog(array $releases): array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
foreach ($releases as $release) {
|
||||
// Skip drafts and pre-releases if desired
|
||||
if ($release['draft']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$publishedAt = Carbon::parse($release['published_at']);
|
||||
|
||||
$entry = [
|
||||
'tag_name' => $release['tag_name'],
|
||||
'title' => $release['name'] ?: $release['tag_name'],
|
||||
'content' => $release['body'] ?: 'No release notes available.',
|
||||
'published_at' => $publishedAt->toISOString(),
|
||||
];
|
||||
|
||||
$entries[] = $entry;
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
private function saveChangelogEntries(array $entries): void
|
||||
{
|
||||
// Create changelogs directory if it doesn't exist
|
||||
$changelogsDir = base_path('changelogs');
|
||||
if (! File::exists($changelogsDir)) {
|
||||
File::makeDirectory($changelogsDir, 0755, true);
|
||||
}
|
||||
|
||||
// Group entries by year-month
|
||||
$groupedEntries = [];
|
||||
foreach ($entries as $entry) {
|
||||
$date = Carbon::parse($entry['published_at']);
|
||||
$monthKey = $date->format('Y-m');
|
||||
|
||||
if (! isset($groupedEntries[$monthKey])) {
|
||||
$groupedEntries[$monthKey] = [];
|
||||
}
|
||||
|
||||
$groupedEntries[$monthKey][] = $entry;
|
||||
}
|
||||
|
||||
// Save each month's entries to separate files
|
||||
foreach ($groupedEntries as $month => $monthEntries) {
|
||||
// Sort entries by published date (newest first)
|
||||
usort($monthEntries, function ($a, $b) {
|
||||
return Carbon::parse($b['published_at'])->timestamp - Carbon::parse($a['published_at'])->timestamp;
|
||||
});
|
||||
|
||||
$monthData = [
|
||||
'entries' => $monthEntries,
|
||||
'last_updated' => now()->toISOString(),
|
||||
];
|
||||
|
||||
$filePath = base_path("changelogs/{$month}.json");
|
||||
File::put($filePath, json_encode($monthData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -31,7 +31,7 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
if ($response->successful()) {
|
||||
$services = $response->json();
|
||||
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
||||
File::put(base_path('templates/'.config('constants.services.file_name')), json_encode($services));
|
||||
} else {
|
||||
send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body());
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Actions\Application\GenerateConfig;
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
@@ -228,7 +227,18 @@ class General extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
$this->application->parse();
|
||||
|
||||
// Refresh parsedServiceDomains to reflect any changes in docker_compose_domains
|
||||
$this->application->refresh();
|
||||
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
|
||||
// Convert service names with dots to use underscores for HTML form binding
|
||||
$sanitizedDomains = [];
|
||||
foreach ($this->parsedServiceDomains as $serviceName => $domain) {
|
||||
$sanitizedKey = str($serviceName)->slug('_')->toString();
|
||||
$sanitizedDomains[$sanitizedKey] = $domain;
|
||||
}
|
||||
$this->parsedServiceDomains = $sanitizedDomains;
|
||||
|
||||
$showToast && $this->dispatch('success', 'Docker compose file loaded.');
|
||||
$this->dispatch('compose_loaded');
|
||||
$this->dispatch('refreshStorages');
|
||||
@@ -246,7 +256,7 @@ class General extends Component
|
||||
public function generateDomain(string $serviceName)
|
||||
{
|
||||
$uuid = new Cuid2;
|
||||
$domain = generateFqdn($this->application->destination->server, $uuid);
|
||||
$domain = generateUrl(server: $this->application->destination->server, random: $uuid);
|
||||
$sanitizedKey = str($serviceName)->slug('_')->toString();
|
||||
$this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain;
|
||||
|
||||
@@ -270,7 +280,6 @@ class General extends Component
|
||||
$this->application->save();
|
||||
$this->dispatch('success', 'Domain generated.');
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->updateServiceEnvironmentVariables();
|
||||
$this->loadComposeFile(showToast: false);
|
||||
}
|
||||
|
||||
@@ -317,7 +326,7 @@ class General extends Component
|
||||
{
|
||||
$server = data_get($this->application, 'destination.server');
|
||||
if ($server) {
|
||||
$fqdn = generateFqdn($server, $this->application->uuid);
|
||||
$fqdn = generateFqdn(server: $server, random: $this->application->uuid, parserVersion: $this->application->compose_parsing_version);
|
||||
$this->application->fqdn = $fqdn;
|
||||
$this->application->save();
|
||||
$this->resetDefaultLabels();
|
||||
@@ -344,7 +353,7 @@ class General extends Component
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->loadComposeFile();
|
||||
$this->loadComposeFile(showToast: false);
|
||||
}
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -421,7 +430,7 @@ class General extends Component
|
||||
}
|
||||
|
||||
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
|
||||
$compose_return = $this->loadComposeFile();
|
||||
$compose_return = $this->loadComposeFile(showToast: false);
|
||||
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
||||
return;
|
||||
}
|
||||
@@ -453,45 +462,23 @@ class General extends Component
|
||||
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
|
||||
}
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
// Convert sanitized service names back to original names for storage
|
||||
$originalDomains = [];
|
||||
foreach ($this->parsedServiceDomains as $key => $value) {
|
||||
// Find the original service name by checking parsed services
|
||||
$originalServiceName = $key;
|
||||
if (isset($this->parsedServices['services'])) {
|
||||
foreach ($this->parsedServices['services'] as $originalName => $service) {
|
||||
if (str($originalName)->slug('_')->toString() === $key) {
|
||||
$originalServiceName = $originalName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$originalDomains[$originalServiceName] = $value;
|
||||
}
|
||||
|
||||
$this->application->docker_compose_domains = json_encode($originalDomains);
|
||||
|
||||
foreach ($originalDomains as $serviceName => $service) {
|
||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||
if ($this->application->isDirty('docker_compose_domains')) {
|
||||
foreach ($this->parsedServiceDomains as $service) {
|
||||
$domain = data_get($service, 'domain');
|
||||
if ($domain) {
|
||||
if (! validate_dns_entry($domain, $this->application->destination->server)) {
|
||||
$showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
}
|
||||
}
|
||||
}
|
||||
check_domain_usage(resource: $this->application);
|
||||
}
|
||||
}
|
||||
if ($this->application->isDirty('docker_compose_domains')) {
|
||||
$this->application->save();
|
||||
$this->resetDefaultLabels();
|
||||
}
|
||||
}
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
|
||||
// Update SERVICE_FQDN_ and SERVICE_URL_ environment variables for Docker Compose applications
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->updateServiceEnvironmentVariables();
|
||||
}
|
||||
|
||||
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
|
||||
} catch (\Throwable $e) {
|
||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
@@ -525,25 +512,33 @@ class General extends Component
|
||||
foreach ($domains as $serviceName => $service) {
|
||||
$serviceNameFormatted = str($serviceName)->upper()->replace('-', '_');
|
||||
$domain = data_get($service, 'domain');
|
||||
// Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed
|
||||
$this->application->environment_variables()->where('resourceable_type', Application::class)
|
||||
->where('resourceable_id', $this->application->id)
|
||||
->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%")
|
||||
->delete();
|
||||
|
||||
$this->application->environment_variables()->where('resourceable_type', Application::class)
|
||||
->where('resourceable_id', $this->application->id)
|
||||
->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%")
|
||||
->delete();
|
||||
|
||||
if ($domain) {
|
||||
// Create or update SERVICE_FQDN_ and SERVICE_URL_ variables
|
||||
$fqdn = Url::fromString($domain);
|
||||
$port = $fqdn->getPort();
|
||||
$path = $fqdn->getPath();
|
||||
$fqdnValue = $fqdn->getScheme().'://'.$fqdn->getHost();
|
||||
if ($path !== '/') {
|
||||
$fqdnValue = $fqdnValue.$path;
|
||||
}
|
||||
$urlValue = str($domain)->after('://');
|
||||
$urlValue = $fqdn->getScheme().'://'.$fqdn->getHost();
|
||||
if ($path !== '/') {
|
||||
$urlValue = $urlValue.$path;
|
||||
}
|
||||
$fqdnValue = str($domain)->after('://');
|
||||
if ($path !== '/') {
|
||||
$fqdnValue = $fqdnValue.$path;
|
||||
}
|
||||
|
||||
// Create/update SERVICE_FQDN_
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
$this->application->environment_variables()->updateOrCreate([
|
||||
'key' => "SERVICE_FQDN_{$serviceNameFormatted}",
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
@@ -552,21 +547,16 @@ class General extends Component
|
||||
]);
|
||||
|
||||
// Create/update SERVICE_URL_
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
$this->application->environment_variables()->updateOrCreate([
|
||||
'key' => "SERVICE_URL_{$serviceNameFormatted}",
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
// Create/update port-specific variables if port exists
|
||||
if ($port) {
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
if (filled($port)) {
|
||||
$this->application->environment_variables()->updateOrCreate([
|
||||
'key' => "SERVICE_FQDN_{$serviceNameFormatted}_{$port}",
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
@@ -574,9 +564,7 @@ class General extends Component
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
$this->application->environment_variables()->updateOrCreate([
|
||||
'key' => "SERVICE_URL_{$serviceNameFormatted}_{$port}",
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
@@ -584,17 +572,6 @@ class General extends Component
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed
|
||||
EnvironmentVariable::where('resourceable_type', Application::class)
|
||||
->where('resourceable_id', $this->application->id)
|
||||
->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%")
|
||||
->delete();
|
||||
|
||||
EnvironmentVariable::where('resourceable_type', Application::class)
|
||||
->where('resourceable_id', $this->application->id)
|
||||
->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%")
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -48,7 +48,7 @@ class PreviewsCompose extends Component
|
||||
$random = new Cuid2;
|
||||
|
||||
// Generate a unique domain like main app services do
|
||||
$generated_fqdn = generateFqdn($server, $random);
|
||||
$generated_fqdn = generateFqdn(server: $server, random: $random, parserVersion: $this->preview->application->compose_parsing_version);
|
||||
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', str($generated_fqdn)->after('://'), $preview_fqdn);
|
||||
|
@@ -129,7 +129,7 @@ class CloneMe extends Component
|
||||
$uuid = (string) new Cuid2;
|
||||
$url = $application->fqdn;
|
||||
if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
|
||||
$url = generateFqdn($this->server, $uuid);
|
||||
$url = generateFqdn(server: $this->server, random: $uuid, parserVersion: $application->compose_parsing_version);
|
||||
}
|
||||
|
||||
$newApplication = $application->replicate([
|
||||
|
@@ -64,6 +64,9 @@ class BackupEdit extends Component
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $saveS3 = false;
|
||||
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $disableLocalBackup = false;
|
||||
|
||||
#[Validate(['nullable', 'integer'])]
|
||||
public ?int $s3StorageId = 1;
|
||||
|
||||
@@ -98,6 +101,7 @@ class BackupEdit extends Component
|
||||
$this->backup->database_backup_retention_days_s3 = $this->databaseBackupRetentionDaysS3;
|
||||
$this->backup->database_backup_retention_max_storage_s3 = $this->databaseBackupRetentionMaxStorageS3;
|
||||
$this->backup->save_s3 = $this->saveS3;
|
||||
$this->backup->disable_local_backup = $this->disableLocalBackup;
|
||||
$this->backup->s3_storage_id = $this->s3StorageId;
|
||||
$this->backup->databases_to_backup = $this->databasesToBackup;
|
||||
$this->backup->dump_all = $this->dumpAll;
|
||||
@@ -115,6 +119,7 @@ class BackupEdit extends Component
|
||||
$this->databaseBackupRetentionDaysS3 = $this->backup->database_backup_retention_days_s3;
|
||||
$this->databaseBackupRetentionMaxStorageS3 = $this->backup->database_backup_retention_max_storage_s3;
|
||||
$this->saveS3 = $this->backup->save_s3;
|
||||
$this->disableLocalBackup = $this->backup->disable_local_backup ?? false;
|
||||
$this->s3StorageId = $this->backup->s3_storage_id;
|
||||
$this->databasesToBackup = $this->backup->databases_to_backup;
|
||||
$this->dumpAll = $this->backup->dump_all;
|
||||
@@ -193,6 +198,12 @@ class BackupEdit extends Component
|
||||
if (! is_numeric($this->backup->s3_storage_id)) {
|
||||
$this->backup->s3_storage_id = null;
|
||||
}
|
||||
|
||||
// Validate that disable_local_backup can only be true when S3 backup is enabled
|
||||
if ($this->backup->disable_local_backup && ! $this->backup->save_s3) {
|
||||
throw new \Exception('Local backup can only be disabled when S3 backup is enabled.');
|
||||
}
|
||||
|
||||
$isValid = validate_cron_expression($this->backup->frequency);
|
||||
if (! $isValid) {
|
||||
throw new \Exception('Invalid Cron / Human expression');
|
||||
|
@@ -132,6 +132,12 @@ class ExecuteContainerCommand extends Component
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort containers alphabetically by name
|
||||
$this->containers = $this->containers->sortBy(function ($container) {
|
||||
return data_get($container, 'container.Names');
|
||||
});
|
||||
|
||||
if ($this->containers->count() === 1) {
|
||||
$this->selected_container = data_get($this->containers->first(), 'container.Names');
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ class ResourceOperations extends Component
|
||||
$url = $this->resource->fqdn;
|
||||
|
||||
if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
|
||||
$url = generateFqdn($server, $uuid);
|
||||
$url = generateFqdn(server: $server, random: $uuid, parserVersion: $this->resource->compose_parsing_version);
|
||||
}
|
||||
|
||||
$new_resource = $this->resource->replicate([
|
||||
|
@@ -6,6 +6,7 @@ use App\Actions\Server\CheckUpdates;
|
||||
use App\Actions\Server\UpdatePackage;
|
||||
use App\Events\ServerPackageUpdated;
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Server\ServerPatchCheck;
|
||||
use Livewire\Component;
|
||||
|
||||
class Patches extends Component
|
||||
@@ -96,6 +97,89 @@ class Patches extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function sendTestEmail()
|
||||
{
|
||||
if (! isDev()) {
|
||||
$this->dispatch('error', message: 'Test email functionality is only available in development mode.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current patch data or create test data if none exists
|
||||
$testPatchData = $this->createTestPatchData();
|
||||
|
||||
// Send test notification
|
||||
$this->server->team->notify(new ServerPatchCheck($this->server, $testPatchData));
|
||||
|
||||
$this->dispatch('success', 'Test email sent successfully! Check your email inbox.');
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', message: 'Failed to send test email: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function createTestPatchData(): array
|
||||
{
|
||||
// If we have real patch data, use it
|
||||
if (isset($this->updates) && is_array($this->updates) && count($this->updates) > 0) {
|
||||
return [
|
||||
'total_updates' => $this->totalUpdates,
|
||||
'updates' => $this->updates,
|
||||
'osId' => $this->osId,
|
||||
'package_manager' => $this->packageManager,
|
||||
];
|
||||
}
|
||||
|
||||
// Otherwise create realistic test data
|
||||
return [
|
||||
'total_updates' => 8,
|
||||
'updates' => [
|
||||
[
|
||||
'package' => 'docker-ce',
|
||||
'current_version' => '24.0.7-1',
|
||||
'new_version' => '25.0.1-1',
|
||||
],
|
||||
[
|
||||
'package' => 'nginx',
|
||||
'current_version' => '1.20.2-1',
|
||||
'new_version' => '1.22.1-1',
|
||||
],
|
||||
[
|
||||
'package' => 'kernel-generic',
|
||||
'current_version' => '5.15.0-89',
|
||||
'new_version' => '5.15.0-91',
|
||||
],
|
||||
[
|
||||
'package' => 'openssh-server',
|
||||
'current_version' => '8.9p1-3',
|
||||
'new_version' => '9.0p1-1',
|
||||
],
|
||||
[
|
||||
'package' => 'curl',
|
||||
'current_version' => '7.81.0-1',
|
||||
'new_version' => '7.85.0-1',
|
||||
],
|
||||
[
|
||||
'package' => 'git',
|
||||
'current_version' => '2.34.1-1',
|
||||
'new_version' => '2.39.1-1',
|
||||
],
|
||||
[
|
||||
'package' => 'python3',
|
||||
'current_version' => '3.10.6-1',
|
||||
'new_version' => '3.11.0-1',
|
||||
],
|
||||
[
|
||||
'package' => 'htop',
|
||||
'current_version' => '3.2.1-1',
|
||||
'new_version' => '3.2.2-1',
|
||||
],
|
||||
],
|
||||
'osId' => $this->osId ?? 'ubuntu',
|
||||
'package_manager' => $this->packageManager ?? 'apt',
|
||||
];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.security.patches');
|
||||
|
67
app/Livewire/SettingsDropdown.php
Normal file
67
app/Livewire/SettingsDropdown.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Jobs\PullChangelogFromGitHub;
|
||||
use App\Services\ChangelogService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class SettingsDropdown extends Component
|
||||
{
|
||||
public $showWhatsNewModal = false;
|
||||
|
||||
public function getUnreadCountProperty()
|
||||
{
|
||||
return Auth::user()->getUnreadChangelogCount();
|
||||
}
|
||||
|
||||
public function getEntriesProperty()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
return app(ChangelogService::class)->getEntriesForUser($user);
|
||||
}
|
||||
|
||||
public function openWhatsNewModal()
|
||||
{
|
||||
$this->showWhatsNewModal = true;
|
||||
}
|
||||
|
||||
public function closeWhatsNewModal()
|
||||
{
|
||||
$this->showWhatsNewModal = false;
|
||||
}
|
||||
|
||||
public function markAsRead($identifier)
|
||||
{
|
||||
app(ChangelogService::class)->markAsReadForUser($identifier, Auth::user());
|
||||
}
|
||||
|
||||
public function markAllAsRead()
|
||||
{
|
||||
app(ChangelogService::class)->markAllAsReadForUser(Auth::user());
|
||||
}
|
||||
|
||||
public function manualFetchChangelog()
|
||||
{
|
||||
if (! isDev()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
PullChangelogFromGitHub::dispatch();
|
||||
$this->dispatch('success', 'Changelog fetch initiated! Check back in a few moments.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->dispatch('error', 'Failed to fetch changelog: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings-dropdown', [
|
||||
'entries' => $this->entries,
|
||||
'unreadCount' => $this->unreadCount,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -75,7 +75,7 @@ class Index extends Component
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Log the error
|
||||
logger()->error('Stripe API error: ' . $e->getMessage());
|
||||
logger()->error('Stripe API error: '.$e->getMessage());
|
||||
// Set a flag to show an error message to the user
|
||||
$this->addError('stripe', 'Could not retrieve subscription information. Please try again later.');
|
||||
} finally {
|
||||
|
@@ -59,7 +59,7 @@ class Index extends Component
|
||||
|
||||
return null;
|
||||
})->filter();
|
||||
});
|
||||
})->sortBy('name');
|
||||
}
|
||||
|
||||
public function updatedSelectedUuid()
|
||||
|
@@ -111,7 +111,7 @@ class Application extends BaseModel
|
||||
{
|
||||
use HasConfiguration, HasFactory, SoftDeletes;
|
||||
|
||||
private static $parserVersion = '4';
|
||||
private static $parserVersion = '5';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -1353,7 +1353,7 @@ class Application extends BaseModel
|
||||
public function parse(int $pull_request_id = 0, ?int $preview_id = null)
|
||||
{
|
||||
if ((int) $this->compose_parsing_version >= 3) {
|
||||
return newParser($this, $pull_request_id, $preview_id);
|
||||
return applicationParser($this, $pull_request_id, $preview_id);
|
||||
} elseif ($this->docker_compose_raw) {
|
||||
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
|
||||
} else {
|
||||
@@ -1442,7 +1442,21 @@ class Application extends BaseModel
|
||||
$parsedServices = $this->parse();
|
||||
if ($this->docker_compose_domains) {
|
||||
$json = collect(json_decode($this->docker_compose_domains));
|
||||
$names = collect(data_get($parsedServices, 'services'))->keys()->toArray();
|
||||
foreach ($json as $key => $value) {
|
||||
if (str($key)->contains('-')) {
|
||||
$key = str($key)->replace('-', '_');
|
||||
}
|
||||
$json->put((string) $key, $value);
|
||||
}
|
||||
$services = collect(data_get($parsedServices, 'services', []));
|
||||
foreach ($services as $name => $service) {
|
||||
if (str($name)->contains('-')) {
|
||||
$replacedName = str($name)->replace('-', '_');
|
||||
$services->put((string) $replacedName, $service);
|
||||
$services->forget((string) $name);
|
||||
}
|
||||
}
|
||||
$names = collect($services)->keys()->toArray();
|
||||
$jsonNames = $json->keys()->toArray();
|
||||
$diff = array_diff($jsonNames, $names);
|
||||
$json = $json->filter(function ($value, $key) use ($diff) {
|
||||
|
@@ -74,7 +74,7 @@ class ApplicationPreview extends BaseModel
|
||||
|
||||
public function generate_preview_fqdn()
|
||||
{
|
||||
if (is_null($this->fqdn) && $this->application->fqdn) {
|
||||
if (empty($this->fqdn) && $this->application->fqdn) {
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
|
||||
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
|
||||
|
@@ -42,7 +42,7 @@ class Service extends BaseModel
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
private static $parserVersion = '4';
|
||||
private static $parserVersion = '5';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -255,6 +255,19 @@ class Service extends BaseModel
|
||||
continue;
|
||||
}
|
||||
switch ($image) {
|
||||
case $image->contains('drizzle-team/gateway'):
|
||||
$data = collect([]);
|
||||
$masterpass = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_DRIZZLE')->first();
|
||||
$data = $data->merge([
|
||||
'Master Password' => [
|
||||
'key' => data_get($masterpass, 'key'),
|
||||
'value' => data_get($masterpass, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
$fields->put('Drizzle', $data->toArray());
|
||||
break;
|
||||
case $image->contains('castopod'):
|
||||
$data = collect([]);
|
||||
$disable_https = $this->environment_variables()->where('key', 'CP_DISABLE_HTTPS')->first();
|
||||
@@ -1277,7 +1290,7 @@ class Service extends BaseModel
|
||||
public function parse(bool $isNew = false): Collection
|
||||
{
|
||||
if ((int) $this->compose_parsing_version >= 3) {
|
||||
return newParser($this);
|
||||
return serviceParser($this);
|
||||
} elseif ($this->docker_compose_raw) {
|
||||
return parseDockerComposeFile($this, $isNew);
|
||||
} else {
|
||||
|
@@ -203,6 +203,16 @@ class User extends Authenticatable implements SendsEmail
|
||||
return $this->belongsToMany(Team::class)->withPivot('role');
|
||||
}
|
||||
|
||||
public function changelogReads()
|
||||
{
|
||||
return $this->hasMany(UserChangelogRead::class);
|
||||
}
|
||||
|
||||
public function getUnreadChangelogCount(): int
|
||||
{
|
||||
return app(\App\Services\ChangelogService::class)->getUnreadCountForUser($this);
|
||||
}
|
||||
|
||||
public function getRecipients(): array
|
||||
{
|
||||
return [$this->email];
|
||||
|
48
app/Models/UserChangelogRead.php
Normal file
48
app/Models/UserChangelogRead.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserChangelogRead extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'release_tag',
|
||||
'read_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'read_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public static function markAsRead(int $userId, string $identifier): void
|
||||
{
|
||||
self::firstOrCreate([
|
||||
'user_id' => $userId,
|
||||
'release_tag' => $identifier,
|
||||
], [
|
||||
'read_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function isReadByUser(int $userId, string $identifier): bool
|
||||
{
|
||||
return self::where('user_id', $userId)
|
||||
->where('release_tag', $identifier)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public static function getReadIdentifiersForUser(int $userId): array
|
||||
{
|
||||
return self::where('user_id', $userId)
|
||||
->pluck('release_tag')
|
||||
->toArray();
|
||||
}
|
||||
}
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Notifications\Channels;
|
||||
|
||||
use App\Models\Team;
|
||||
use Exception;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Resend;
|
||||
|
||||
@@ -11,21 +13,53 @@ class EmailChannel
|
||||
|
||||
public function send(SendsEmail $notifiable, Notification $notification): void
|
||||
{
|
||||
try {
|
||||
// Get team and validate membership before proceeding
|
||||
$team = data_get($notifiable, 'id');
|
||||
$members = Team::find($team)->members;
|
||||
|
||||
$useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings;
|
||||
$isTransactionalEmail = data_get($notification, 'isTransactionalEmail', false);
|
||||
$customEmails = data_get($notification, 'emails', null);
|
||||
|
||||
if ($useInstanceEmailSettings || $isTransactionalEmail) {
|
||||
$settings = instanceSettings();
|
||||
} else {
|
||||
$settings = $notifiable->emailNotificationSettings;
|
||||
}
|
||||
|
||||
$isResendEnabled = $settings->resend_enabled;
|
||||
$isSmtpEnabled = $settings->smtp_enabled;
|
||||
|
||||
if ($customEmails) {
|
||||
$recipients = [$customEmails];
|
||||
} else {
|
||||
$recipients = $notifiable->getRecipients();
|
||||
}
|
||||
|
||||
// Validate team membership for all recipients
|
||||
if (count($recipients) === 0) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
$mailMessage = $notification->toMail($notifiable);
|
||||
|
||||
if ($isResendEnabled) {
|
||||
@@ -66,5 +100,15 @@ class EmailChannel
|
||||
|
||||
$mailer->send($email);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Illuminate\Support\Facades\Log::error('EmailChannel failed: '.$e->getMessage(), [
|
||||
'notification' => get_class($notification),
|
||||
'notifiable' => get_class($notifiable),
|
||||
'team_id' => data_get($notifiable, 'id'),
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ class S3StoragePolicy
|
||||
*/
|
||||
public function view(User $user, S3Storage $storage): bool
|
||||
{
|
||||
return $user->teams()->where('id', $storage->team_id)->exists();
|
||||
return $user->teams()->get()->firstWhere('id', $storage->team_id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ class S3StoragePolicy
|
||||
*/
|
||||
public function update(User $user, Server $server): bool
|
||||
{
|
||||
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
|
||||
return $user->teams()->get()->firstWhere('id', $server->team_id)->exists() && $user->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +45,7 @@ class S3StoragePolicy
|
||||
*/
|
||||
public function delete(User $user, S3Storage $storage): bool
|
||||
{
|
||||
return $user->teams()->where('id', $storage->team_id)->exists();
|
||||
return $user->teams()->get()->firstWhere('id', $storage->team_id)->exists() && $user->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
|
300
app/Services/ChangelogService.php
Normal file
300
app/Services/ChangelogService.php
Normal file
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserChangelogRead;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Spatie\LaravelMarkdown\MarkdownRenderer;
|
||||
|
||||
class ChangelogService
|
||||
{
|
||||
public function getEntries(int $recentMonths = 3): Collection
|
||||
{
|
||||
// For backward compatibility, check if old changelog.json exists
|
||||
if (file_exists(base_path('changelog.json'))) {
|
||||
$data = $this->fetchChangelogData();
|
||||
|
||||
if (! $data || ! isset($data['entries'])) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return collect($data['entries'])
|
||||
->filter(fn ($entry) => $this->validateEntryData($entry))
|
||||
->map(function ($entry) {
|
||||
$entry['published_at'] = Carbon::parse($entry['published_at']);
|
||||
$entry['content_html'] = $this->parseMarkdown($entry['content']);
|
||||
|
||||
return (object) $entry;
|
||||
})
|
||||
->filter(fn ($entry) => $entry->published_at <= now())
|
||||
->sortBy('published_at')
|
||||
->reverse()
|
||||
->values();
|
||||
}
|
||||
|
||||
// Load entries from recent months for performance
|
||||
$availableMonths = $this->getAvailableMonths();
|
||||
$monthsToLoad = $availableMonths->take($recentMonths);
|
||||
|
||||
return $monthsToLoad
|
||||
->flatMap(fn ($month) => $this->getEntriesForMonth($month))
|
||||
->sortBy('published_at')
|
||||
->reverse()
|
||||
->values();
|
||||
}
|
||||
|
||||
public function getAllEntries(): Collection
|
||||
{
|
||||
$availableMonths = $this->getAvailableMonths();
|
||||
|
||||
return $availableMonths
|
||||
->flatMap(fn ($month) => $this->getEntriesForMonth($month))
|
||||
->sortBy('published_at')
|
||||
->reverse()
|
||||
->values();
|
||||
}
|
||||
|
||||
public function getEntriesForUser(User $user): Collection
|
||||
{
|
||||
$entries = $this->getEntries();
|
||||
$readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id);
|
||||
|
||||
return $entries->map(function ($entry) use ($readIdentifiers) {
|
||||
$entry->is_read = in_array($entry->tag_name, $readIdentifiers);
|
||||
|
||||
return $entry;
|
||||
})->sortBy([
|
||||
['is_read', 'asc'], // unread first
|
||||
['published_at', 'desc'], // then by date
|
||||
])->values();
|
||||
}
|
||||
|
||||
public function getUnreadCountForUser(User $user): int
|
||||
{
|
||||
if (isDev()) {
|
||||
$entries = $this->getEntries();
|
||||
$readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id);
|
||||
|
||||
return $entries->reject(fn ($entry) => in_array($entry->tag_name, $readIdentifiers))->count();
|
||||
} else {
|
||||
return Cache::remember(
|
||||
'user_unread_changelog_count_'.$user->id,
|
||||
now()->addHour(),
|
||||
function () use ($user) {
|
||||
$entries = $this->getEntries();
|
||||
$readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id);
|
||||
|
||||
return $entries->reject(fn ($entry) => in_array($entry->tag_name, $readIdentifiers))->count();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getAvailableMonths(): Collection
|
||||
{
|
||||
$pattern = base_path('changelogs/*.json');
|
||||
$files = glob($pattern);
|
||||
|
||||
if ($files === false) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return collect($files)
|
||||
->map(fn ($file) => basename($file, '.json'))
|
||||
->filter(fn ($name) => preg_match('/^\d{4}-\d{2}$/', $name))
|
||||
->sort()
|
||||
->reverse()
|
||||
->values();
|
||||
}
|
||||
|
||||
public function getEntriesForMonth(string $month): Collection
|
||||
{
|
||||
$path = base_path("changelogs/{$month}.json");
|
||||
|
||||
if (! file_exists($path)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$content = file_get_contents($path);
|
||||
|
||||
if ($content === false) {
|
||||
Log::error("Failed to read changelog file: {$month}.json");
|
||||
|
||||
return collect();
|
||||
}
|
||||
|
||||
$data = json_decode($content, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
Log::error("Invalid JSON in {$month}.json: ".json_last_error_msg());
|
||||
|
||||
return collect();
|
||||
}
|
||||
|
||||
if (! isset($data['entries']) || ! is_array($data['entries'])) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return collect($data['entries'])
|
||||
->filter(fn ($entry) => $this->validateEntryData($entry))
|
||||
->map(function ($entry) {
|
||||
$entry['published_at'] = Carbon::parse($entry['published_at']);
|
||||
$entry['content_html'] = $this->parseMarkdown($entry['content']);
|
||||
|
||||
return (object) $entry;
|
||||
})
|
||||
->filter(fn ($entry) => $entry->published_at <= now())
|
||||
->sortBy('published_at')
|
||||
->reverse()
|
||||
->values();
|
||||
}
|
||||
|
||||
private function fetchChangelogData(): ?array
|
||||
{
|
||||
// Legacy support for old changelog.json
|
||||
$path = base_path('changelog.json');
|
||||
|
||||
if (file_exists($path)) {
|
||||
$content = file_get_contents($path);
|
||||
|
||||
if ($content === false) {
|
||||
Log::error('Failed to read changelog.json file');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($content, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
Log::error('Invalid JSON in changelog.json: '.json_last_error_msg());
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
// New monthly structure - combine all months
|
||||
$allEntries = [];
|
||||
foreach ($this->getAvailableMonths() as $month) {
|
||||
$monthEntries = $this->getEntriesForMonth($month);
|
||||
foreach ($monthEntries as $entry) {
|
||||
$allEntries[] = (array) $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return ['entries' => $allEntries];
|
||||
}
|
||||
|
||||
public function markAsReadForUser(string $version, User $user): void
|
||||
{
|
||||
UserChangelogRead::markAsRead($user->id, $version);
|
||||
Cache::forget('user_unread_changelog_count_'.$user->id);
|
||||
}
|
||||
|
||||
public function markAllAsReadForUser(User $user): void
|
||||
{
|
||||
$entries = $this->getEntries();
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
UserChangelogRead::markAsRead($user->id, $entry->tag_name);
|
||||
}
|
||||
|
||||
Cache::forget('user_unread_changelog_count_'.$user->id);
|
||||
}
|
||||
|
||||
private function validateEntryData(array $data): bool
|
||||
{
|
||||
$required = ['tag_name', 'title', 'content', 'published_at'];
|
||||
|
||||
foreach ($required as $field) {
|
||||
if (! isset($data[$field]) || empty($data[$field])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clearAllReadStatus(): array
|
||||
{
|
||||
try {
|
||||
$count = UserChangelogRead::count();
|
||||
UserChangelogRead::truncate();
|
||||
|
||||
// Clear all user caches
|
||||
$this->clearAllUserCaches();
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Successfully cleared {$count} read status records",
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to clear read status: '.$e->getMessage());
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to clear read status: '.$e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function clearAllUserCaches(): void
|
||||
{
|
||||
$users = User::select('id')->get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
Cache::forget('user_unread_changelog_count_'.$user->id);
|
||||
}
|
||||
}
|
||||
|
||||
private function parseMarkdown(string $content): string
|
||||
{
|
||||
$renderer = app(MarkdownRenderer::class);
|
||||
|
||||
$html = $renderer->toHtml($content);
|
||||
|
||||
// Apply custom Tailwind CSS classes for dark mode compatibility
|
||||
$html = $this->applyCustomStyling($html);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function applyCustomStyling(string $html): string
|
||||
{
|
||||
// Headers
|
||||
$html = preg_replace('/<h1[^>]*>/', '<h1 class="text-xl font-bold dark:text-white mb-2">', $html);
|
||||
$html = preg_replace('/<h2[^>]*>/', '<h2 class="text-lg font-semibold dark:text-white mb-2">', $html);
|
||||
$html = preg_replace('/<h3[^>]*>/', '<h3 class="text-md font-semibold dark:text-white mb-1">', $html);
|
||||
|
||||
// Paragraphs
|
||||
$html = preg_replace('/<p[^>]*>/', '<p class="mb-2 dark:text-neutral-300">', $html);
|
||||
|
||||
// Lists
|
||||
$html = preg_replace('/<ul[^>]*>/', '<ul class="mb-2 ml-4 list-disc">', $html);
|
||||
$html = preg_replace('/<ol[^>]*>/', '<ol class="mb-2 ml-4 list-decimal">', $html);
|
||||
$html = preg_replace('/<li[^>]*>/', '<li class="dark:text-neutral-300">', $html);
|
||||
|
||||
// Code blocks and inline code
|
||||
$html = preg_replace('/<pre[^>]*>/', '<pre class="bg-gray-100 dark:bg-coolgray-300 p-2 rounded text-sm overflow-x-auto my-2">', $html);
|
||||
$html = preg_replace('/<code[^>]*>/', '<code class="bg-gray-100 dark:bg-coolgray-300 px-1 py-0.5 rounded text-sm">', $html);
|
||||
|
||||
// Links - Apply styling to existing markdown links
|
||||
$html = preg_replace('/<a([^>]*)>/', '<a$1 class="text-blue-500 hover:text-blue-600 underline" target="_blank" rel="noopener">', $html);
|
||||
|
||||
// Convert plain URLs to clickable links (that aren't already in <a> tags)
|
||||
$html = preg_replace('/(?<!href="|href=\')(?<!>)(?<!\/)(https?:\/\/[^\s<>"]+)(?![^<]*<\/a>)/', '<a href="$1" class="text-blue-500 hover:text-blue-600 underline" target="_blank" rel="noopener">$1</a>', $html);
|
||||
|
||||
// Strong/bold text
|
||||
$html = preg_replace('/<strong[^>]*>/', '<strong class="font-semibold dark:text-white">', $html);
|
||||
|
||||
// Emphasis/italic text
|
||||
$html = preg_replace('/<em[^>]*>/', '<em class="italic dark:text-neutral-300">', $html);
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
@@ -25,6 +25,7 @@ class Input extends Component
|
||||
public string $autocomplete = 'off',
|
||||
public ?int $minlength = null,
|
||||
public ?int $maxlength = null,
|
||||
public bool $autofocus = false,
|
||||
) {}
|
||||
|
||||
public function render(): View|Closure|string
|
||||
|
@@ -237,6 +237,12 @@ function removeOldBackups($backup): void
|
||||
{
|
||||
try {
|
||||
if ($backup->executions) {
|
||||
// If local backup is disabled, mark all executions as having local storage deleted
|
||||
if ($backup->disable_local_backup && $backup->save_s3) {
|
||||
$backup->executions()
|
||||
->where('local_storage_deleted', false)
|
||||
->update(['local_storage_deleted' => true]);
|
||||
} else {
|
||||
$localBackupsToDelete = deleteOldBackupsLocally($backup);
|
||||
if ($localBackupsToDelete->isNotEmpty()) {
|
||||
$backup->executions()
|
||||
@@ -244,6 +250,7 @@ function removeOldBackups($backup): void
|
||||
->update(['local_storage_deleted' => true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($backup->save_s3 && $backup->executions) {
|
||||
$s3BackupsToDelete = deleteOldBackupsFromS3($backup);
|
||||
@@ -254,10 +261,18 @@ function removeOldBackups($backup): void
|
||||
}
|
||||
}
|
||||
|
||||
// Delete executions where both local and S3 storage are marked as deleted
|
||||
// or where only S3 is enabled and S3 storage is deleted
|
||||
if ($backup->disable_local_backup && $backup->save_s3) {
|
||||
$backup->executions()
|
||||
->where('s3_storage_deleted', true)
|
||||
->delete();
|
||||
} else {
|
||||
$backup->executions()
|
||||
->where('local_storage_deleted', true)
|
||||
->where('s3_storage_deleted', true)
|
||||
->delete();
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
|
@@ -256,12 +256,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
|
||||
|
||||
if (str($MINIO_BROWSER_REDIRECT_URL->value ?? '')->isEmpty()) {
|
||||
$MINIO_BROWSER_REDIRECT_URL->update([
|
||||
'value' => generateFqdn($server, 'console-'.$uuid, true),
|
||||
'value' => generateFqdn(server: $server, random: 'console-'.$uuid, parserVersion: $resource->compose_parsing_version, forceHttps: true),
|
||||
]);
|
||||
}
|
||||
if (str($MINIO_SERVER_URL->value ?? '')->isEmpty()) {
|
||||
$MINIO_SERVER_URL->update([
|
||||
'value' => generateFqdn($server, 'minio-'.$uuid, true),
|
||||
'value' => generateFqdn(server: $server, random: 'minio-'.$uuid, parserVersion: $resource->compose_parsing_version, forceHttps: true),
|
||||
]);
|
||||
}
|
||||
$payload = collect([
|
||||
@@ -279,12 +279,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
|
||||
|
||||
if (str($LOGTO_ENDPOINT->value ?? '')->isEmpty()) {
|
||||
$LOGTO_ENDPOINT->update([
|
||||
'value' => generateFqdn($server, 'logto-'.$uuid),
|
||||
'value' => generateFqdn(server: $server, random: 'logto-'.$uuid, parserVersion: $resource->compose_parsing_version),
|
||||
]);
|
||||
}
|
||||
if (str($LOGTO_ADMIN_ENDPOINT->value ?? '')->isEmpty()) {
|
||||
$LOGTO_ADMIN_ENDPOINT->update([
|
||||
'value' => generateFqdn($server, 'logto-admin-'.$uuid),
|
||||
'value' => generateFqdn(server: $server, random: 'logto-admin-'.$uuid, parserVersion: $resource->compose_parsing_version),
|
||||
]);
|
||||
}
|
||||
$payload = collect([
|
||||
@@ -1101,7 +1101,7 @@ function getContainerLogs(Server $server, string $container_id, int $lines = 100
|
||||
], $server);
|
||||
}
|
||||
|
||||
$output .= removeAnsiColors($output);
|
||||
$output = removeAnsiColors($output);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
1767
bootstrap/helpers/parsers.php
Normal file
1767
bootstrap/helpers/parsers.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
@@ -115,163 +114,71 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
|
||||
$resource->image = $updatedImage;
|
||||
$resource->save();
|
||||
}
|
||||
|
||||
$serviceName = str($resource->name)->upper()->replace('-', '_');
|
||||
$resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%")->delete();
|
||||
$resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%")->delete();
|
||||
|
||||
if ($resource->fqdn) {
|
||||
$resourceFqdns = str($resource->fqdn)->explode(',');
|
||||
if ($resourceFqdns->count() === 1) {
|
||||
$resourceFqdns = $resourceFqdns->first();
|
||||
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_');
|
||||
$url = Url::fromString($resourceFqdns);
|
||||
$port = $url->getPort();
|
||||
$path = $url->getPath();
|
||||
$urlValue = $url->getScheme().'://'.$url->getHost();
|
||||
$urlValue = ($path === '/') ? $urlValue : $urlValue.$path;
|
||||
$resource->service->environment_variables()->updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
if ($port) {
|
||||
$variableName = $variableName."_$port";
|
||||
$resource->service->environment_variables()->updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
$variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_');
|
||||
$fqdn = Url::fromString($resourceFqdns);
|
||||
$port = $fqdn->getPort();
|
||||
$path = $fqdn->getPath();
|
||||
$fqdn = $fqdn->getScheme().'://'.$fqdn->getHost();
|
||||
$fqdnValue = ($path === '/') ? $fqdn : $fqdn.$path;
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
if ($port) {
|
||||
$variableName = $variableName."_$port";
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_');
|
||||
$url = Url::fromString($fqdn);
|
||||
$port = $url->getPort();
|
||||
$path = $url->getPath();
|
||||
$url = $url->getHost();
|
||||
$urlValue = str($fqdn)->after('://');
|
||||
$fqdn = $fqdn->getHost();
|
||||
$fqdnValue = str($fqdn)->after('://');
|
||||
if ($path !== '/') {
|
||||
$urlValue = $urlValue.$path;
|
||||
$fqdnValue = $fqdnValue.$path;
|
||||
}
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
$resource->service->environment_variables()->updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'value' => $fqdnValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
if ($port) {
|
||||
$variableName = $variableName."_$port";
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
$resource->service->environment_variables()->updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'value' => $fqdnValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
} elseif ($resourceFqdns->count() > 1) {
|
||||
foreach ($resourceFqdns as $fqdn) {
|
||||
$host = Url::fromString($fqdn);
|
||||
$port = $host->getPort();
|
||||
$url = $host->getHost();
|
||||
$path = $host->getPath();
|
||||
$host = $host->getScheme().'://'.$host->getHost();
|
||||
if ($port) {
|
||||
$port_envs = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', 'like', "SERVICE_FQDN_%_$port")
|
||||
->get();
|
||||
foreach ($port_envs as $port_env) {
|
||||
$service_fqdn = str($port_env->key)->beforeLast('_')->after('SERVICE_FQDN_');
|
||||
$env = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', 'SERVICE_FQDN_'.$service_fqdn)
|
||||
->first();
|
||||
if ($env) {
|
||||
if ($path === '/') {
|
||||
$env->value = $host;
|
||||
} else {
|
||||
$env->value = $host.$path;
|
||||
}
|
||||
$env->save();
|
||||
}
|
||||
if ($path === '/') {
|
||||
$port_env->value = $host;
|
||||
} else {
|
||||
$port_env->value = $host.$path;
|
||||
}
|
||||
$port_env->save();
|
||||
}
|
||||
$port_envs_url = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', 'like', "SERVICE_URL_%_$port")
|
||||
->get();
|
||||
foreach ($port_envs_url as $port_env_url) {
|
||||
$service_url = str($port_env_url->key)->beforeLast('_')->after('SERVICE_URL_');
|
||||
$env = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', 'SERVICE_URL_'.$service_url)
|
||||
->first();
|
||||
if ($env) {
|
||||
if ($path === '/') {
|
||||
$env->value = $url;
|
||||
} else {
|
||||
$env->value = $url.$path;
|
||||
}
|
||||
$env->save();
|
||||
}
|
||||
if ($path === '/') {
|
||||
$port_env_url->value = $url;
|
||||
} else {
|
||||
$port_env_url->value = $url.$path;
|
||||
}
|
||||
$port_env_url->save();
|
||||
}
|
||||
} else {
|
||||
$variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_');
|
||||
$generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', $variableName)
|
||||
->first();
|
||||
$fqdn = Url::fromString($fqdn);
|
||||
$fqdn = $fqdn->getScheme().'://'.$fqdn->getHost().$fqdn->getPath();
|
||||
if ($generatedEnv) {
|
||||
$generatedEnv->value = $fqdn;
|
||||
$generatedEnv->save();
|
||||
}
|
||||
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_');
|
||||
$generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', $variableName)
|
||||
->first();
|
||||
$url = Url::fromString($fqdn);
|
||||
$url = $url->getHost().$url->getPath();
|
||||
if ($generatedEnv) {
|
||||
$url = str($fqdn)->after('://');
|
||||
$generatedEnv->value = $url;
|
||||
$generatedEnv->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If FQDN is removed, delete the corresponding environment variables
|
||||
$serviceName = str($resource->name)->upper()->replace('-', '_');
|
||||
EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%")
|
||||
->delete();
|
||||
EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%")
|
||||
->delete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
|
File diff suppressed because it is too large
Load Diff
2
changelogs/.gitignore
vendored
Normal file
2
changelogs/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
@@ -47,6 +47,7 @@
|
||||
"socialiteproviders/zitadel": "^4.2",
|
||||
"spatie/laravel-activitylog": "^4.10.2",
|
||||
"spatie/laravel-data": "^4.17.0",
|
||||
"spatie/laravel-markdown": "^2.7",
|
||||
"spatie/laravel-ray": "^1.40.2",
|
||||
"spatie/laravel-schemaless-attributes": "^2.5.1",
|
||||
"spatie/url": "^2.4",
|
||||
|
203
composer.lock
generated
203
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "52a680a0eb446dcaa74bc35e158aca57",
|
||||
"content-hash": "a78cf8fdfec25eac43de77c05640dc91",
|
||||
"packages": [
|
||||
{
|
||||
"name": "amphp/amp",
|
||||
@@ -7902,6 +7902,66 @@
|
||||
],
|
||||
"time": "2025-05-08T15:41:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/commonmark-shiki-highlighter",
|
||||
"version": "2.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/commonmark-shiki-highlighter.git",
|
||||
"reference": "595c7e0b45d4a63b17dfc1ccbd13532d431ec351"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/commonmark-shiki-highlighter/zipball/595c7e0b45d4a63b17dfc1ccbd13532d431ec351",
|
||||
"reference": "595c7e0b45d4a63b17dfc1ccbd13532d431ec351",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"league/commonmark": "^2.4.2",
|
||||
"php": "^8.0",
|
||||
"spatie/shiki-php": "^2.2.2",
|
||||
"symfony/process": "^5.4|^6.4|^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.19|^v3.49.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.2.7",
|
||||
"spatie/ray": "^1.28"
|
||||
},
|
||||
"type": "commonmark-extension",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\CommonMarkShikiHighlighter\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Highlight code blocks with league/commonmark and Shiki",
|
||||
"homepage": "https://github.com/spatie/commonmark-shiki-highlighter",
|
||||
"keywords": [
|
||||
"commonmark-shiki-highlighter",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/spatie/commonmark-shiki-highlighter/tree/2.5.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-13T11:25:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-activitylog",
|
||||
"version": "4.10.2",
|
||||
@@ -8076,6 +8136,82 @@
|
||||
],
|
||||
"time": "2025-06-25T11:36:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-markdown",
|
||||
"version": "2.7.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-markdown.git",
|
||||
"reference": "353e7f9fae62826e26cbadef58a12ecf39685280"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-markdown/zipball/353e7f9fae62826e26cbadef58a12ecf39685280",
|
||||
"reference": "353e7f9fae62826e26cbadef58a12ecf39685280",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/cache": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/view": "^9.0|^10.0|^11.0|^12.0",
|
||||
"league/commonmark": "^2.6.0",
|
||||
"php": "^8.1",
|
||||
"spatie/commonmark-shiki-highlighter": "^2.5",
|
||||
"spatie/laravel-package-tools": "^1.4.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^6.2|^7.8",
|
||||
"nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0",
|
||||
"orchestra/testbench": "^6.15|^7.0|^8.0|^10.0",
|
||||
"pestphp/pest": "^1.22|^2.0|^3.7",
|
||||
"phpunit/phpunit": "^9.3|^11.5.3",
|
||||
"spatie/laravel-ray": "^1.23",
|
||||
"spatie/pest-plugin-snapshots": "^1.1|^2.2|^3.0",
|
||||
"vimeo/psalm": "^4.8|^6.7"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Spatie\\LaravelMarkdown\\MarkdownServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\LaravelMarkdown\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A highly configurable markdown renderer and Blade component for Laravel",
|
||||
"homepage": "https://github.com/spatie/laravel-markdown",
|
||||
"keywords": [
|
||||
"Laravel-Markdown",
|
||||
"laravel",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/spatie/laravel-markdown/tree/2.7.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-21T13:43:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-package-tools",
|
||||
"version": "1.92.7",
|
||||
@@ -8515,6 +8651,71 @@
|
||||
],
|
||||
"time": "2025-04-18T08:17:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/shiki-php",
|
||||
"version": "2.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/shiki-php.git",
|
||||
"reference": "a2e78a9ff8a1290b25d550be8fbf8285c13175c5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/shiki-php/zipball/a2e78a9ff8a1290b25d550be8fbf8285c13175c5",
|
||||
"reference": "a2e78a9ff8a1290b25d550be8fbf8285c13175c5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": "^8.0",
|
||||
"symfony/process": "^5.4|^6.4|^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^v3.0",
|
||||
"pestphp/pest": "^1.8",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"spatie/pest-plugin-snapshots": "^1.1",
|
||||
"spatie/ray": "^1.10"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\ShikiPhp\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rias Van der Veken",
|
||||
"email": "rias@spatie.be",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Highlight code using Shiki in PHP",
|
||||
"homepage": "https://github.com/spatie/shiki-php",
|
||||
"keywords": [
|
||||
"shiki",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/spatie/shiki-php/tree/2.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-21T14:16:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/url",
|
||||
"version": "2.4.0",
|
||||
|
@@ -22,7 +22,8 @@ return [
|
||||
'services' => [
|
||||
// Temporary disabled until cache is implemented
|
||||
// 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
|
||||
'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json',
|
||||
'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/v4.x/templates/service-templates-latest.json',
|
||||
'file_name' => 'service-templates-latest.json',
|
||||
],
|
||||
|
||||
'terminal' => [
|
||||
|
@@ -65,6 +65,6 @@ return [
|
||||
'client_secret' => env('ZITADEL_CLIENT_SECRET'),
|
||||
'redirect' => env('ZITADEL_REDIRECT_URI'),
|
||||
'base_url' => env('ZITADEL_BASE_URL'),
|
||||
]
|
||||
],
|
||||
|
||||
];
|
||||
|
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_changelog_reads', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('release_tag'); // GitHub tag_name (e.g., "v4.0.0-beta.420.6")
|
||||
$table->timestamp('read_at');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'release_tag']);
|
||||
$table->index('user_id');
|
||||
$table->index('release_tag');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_changelog_reads');
|
||||
}
|
||||
};
|
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('scheduled_database_backups', function (Blueprint $table) {
|
||||
$table->boolean('disable_local_backup')->default(false)->after('save_s3');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('scheduled_database_backups', function (Blueprint $table) {
|
||||
$table->dropColumn('disable_local_backup');
|
||||
});
|
||||
}
|
||||
};
|
@@ -120,6 +120,7 @@ COPY --chown=www-data:www-data templates ./templates
|
||||
COPY --chown=www-data:www-data resources/views ./resources/views
|
||||
COPY --chown=www-data:www-data artisan artisan
|
||||
COPY --chown=www-data:www-data openapi.yaml ./openapi.yaml
|
||||
COPY --chown=www-data:www-data changelogs/ ./changelogs/
|
||||
|
||||
RUN composer dump-autoload
|
||||
|
||||
|
3
public/svgs/bluesky.svg
Normal file
3
public/svgs/bluesky.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="600" height="530" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M135.72 44.03C202.216 93.951 273.74 195.17 300 249.49c26.262-54.316 97.782-155.54 164.28-205.46C512.26 8.009 590-19.862 590 68.825c0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.38-3.69-10.832-3.708-7.896-.017-2.936-1.193.516-3.707 7.896-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.45-163.25-81.433C20.15 217.613 9.997 86.535 9.997 68.825c0-88.687 77.742-60.816 125.72-24.795z" fill="#1185fe"/>
|
||||
</svg>
|
After Width: | Height: | Size: 665 B |
BIN
public/svgs/drizzle.jpeg
Normal file
BIN
public/svgs/drizzle.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
11
public/svgs/homebox.svg
Normal file
11
public/svgs/homebox.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg viewBox="0 0 10817 9730" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:5.42683">
|
||||
<path d="M9310.16 2560.9c245.302 249.894 419.711 539.916 565.373 845.231 47.039 98.872 36.229 215.514-28.2 304.05-64.391 88.536-172.099 134.676-280.631 120.28 0 .053-.039.053-.039.053" style="fill:gray;stroke:#000;stroke-width:206.41px"/>
|
||||
<path d="M5401.56 487.044c-127.958 6.227-254.855 40.77-370.992 103.628-765.271 414.225-2397.45 1297.68-3193.03 1728.32-137.966 74.669-250.327 183.605-328.791 313.046l3963.09 2122.43s-249.048 416.428-470.593 786.926c-189.24 316.445-592.833 429.831-919.198 258.219l-2699.36-1419.32v2215.59c0 226.273 128.751 435.33 337.755 548.466 764.649 413.885 2620.97 1418.66 3385.59 1832.51 209.018 113.137 466.496 113.137 675.514 0 764.623-413.857 2620.94-1418.63 3385.59-1832.51 208.989-113.136 337.743-322.193 337.743-548.466v-3513.48c0-318.684-174.59-611.722-454.853-763.409-795.543-430.632-2427.75-1314.09-3193.02-1728.32-141.693-76.684-299.364-111.227-455.442-103.628" style="fill:#dadada;stroke:#000;stroke-width:206.42px"/>
|
||||
<path d="M5471.83 4754.46V504.71c-127.958 6.226-325.127 23.1-441.264 85.958-765.271 414.225-2397.45 1297.68-3193.03 1728.32-137.966 74.669-250.327 183.605-328.791 313.046l3963.09 2122.43Z" style="fill:gray;stroke:#000;stroke-width:206.42px"/>
|
||||
<path d="m1459.34 2725.96-373.791 715.667c-177.166 339.292-46.417 758 292.375 936.167l4.75 2.5m0 0 2699.37 1419.29c326.374 171.625 729.916 58.25 919.165-258.208 221.542-370.5 470.583-786.917 470.583-786.917l-3963.04-2122.42-2.167 3.458-47.25 90.458" style="fill:#dadada;stroke:#000;stroke-width:206.42px"/>
|
||||
<path d="M5443.74 520.879v4149.79" style="fill:none;stroke:#000;stroke-width:153.5px"/>
|
||||
<path d="M8951.41 4102.72c0-41.65-22.221-80.136-58.291-100.961-36.069-20.825-80.51-20.825-116.58 0l-2439.92 1408.69c-36.07 20.825-58.29 59.311-58.29 100.961V7058c0 41.65 22.22 80.136 58.29 100.961 36.07 20.825 80.51 20.825 116.58 0l2439.92-1408.69c36.07-20.825 58.291-59.312 58.291-100.962v-1546.59Z" style="fill:#567f67"/>
|
||||
<path d="M8951.41 4102.72c0-41.65-22.221-80.136-58.291-100.961-36.069-20.825-80.51-20.825-116.58 0l-2439.92 1408.69c-36.07 20.825-58.29 59.311-58.29 100.961V7058c0 41.65 22.22 80.136 58.29 100.961 36.07 20.825 80.51 20.825 116.58 0l2439.92-1408.69c36.07-20.825 58.291-59.312 58.291-100.962v-1546.59ZM6463.98 5551.29v1387.06l2301.77-1328.92V4222.37L6463.98 5551.29Z"/>
|
||||
<path d="M5443.76 9041.74v-4278.4" style="fill:none;stroke:#000;stroke-width:206.44px;stroke-linejoin:miter"/>
|
||||
<path d="m5471.79 4773.86 3829.35-2188.22" style="fill:none;stroke:#000;stroke-width:206.43px;stroke-linejoin:miter"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
32
public/svgs/librechat.svg
Normal file
32
public/svgs/librechat.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg width="512" height="512" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="linearGradient22708">
|
||||
<stop stop-color="#21facf" offset="0"/>
|
||||
<stop stop-color="#0970ef" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient6949" x1="68.454" x2="198.59" y1="246.73" y2="96.35" gradientTransform="translate(-5.754,-56.594)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#72004e" offset="0"/>
|
||||
<stop stop-color="#0015b1" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient22718" x1="56.735" x2="155.2" y1="246.96" y2="58.575" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4f00da" offset="0"/>
|
||||
<stop stop-color="#e5311b" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient23463" x1="68.454" x2="198.59" y1="246.73" y2="96.35" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient22708"/>
|
||||
<linearGradient id="linearGradient903" x1="54.478" x2="192.1" y1="247.56" y2="9.8095" gradientTransform="matrix(.87923 0 0 .87923 -9.551 48.787)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#dc180d" offset="0"/>
|
||||
<stop stop-color="#f96e20" offset=".5"/>
|
||||
<stop stop-color="#f4ce41" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient918" x1="39.468" x2="154.99" y1="204.22" y2="124.47" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient22708"/>
|
||||
</defs>
|
||||
<g transform="matrix(2.473 0 0 2.473 -4.8978 -4.8812)">
|
||||
<path transform="translate(-5.5496,-57.412)" d="m148.16 59.393c-7.7098 9.3985-19.951 42.888-20.696 49.204-0.16994 4.6737 1.3731 14.231 0.67182 15.805-0.71909 1.6134-5.117-9.4461-7.2151-6.3266-12.219 18.168-10.7 17.731-15.582 31.378-1.8357 5.1315-0.42447 21.99-1.5666 23.773-1.273 1.9866-3.962-12.31-6.8063-9.236-11.603 12.54-16.279 20.379-22.336 30.607-3.3589 5.6725-2.1817 23.33-3.506 24.674-1.3023 1.3215-3.8566-18.326-7.6437-14.309-8.5193 9.038-14.054 13.441-18.946 19.252-5.1981 6.1739-0.78251 17.584-5.0672 35.383l0.1448 0.22073c77.447-50.308 101.52-127.16 107.61-181.19-0.68051 63.93-29.41 142.78-105.33 184.65l0.1127 0.17141c20.241-2.181 22.307 10.458 44.562-4.2837 55.792-48.277 81.856-124.29 61.593-199.78z" display="none" fill="url(#linearGradient903)"/>
|
||||
<path transform="translate(-5.5498,-57.412)" d="m148.16 59.393c-7.7098 9.3985-19.951 42.888-20.696 49.204-0.16994 4.6737 1.3731 14.231 0.67182 15.805-0.71909 1.6134-5.117-9.4461-7.2151-6.3266-12.219 18.168-10.7 17.731-15.582 31.378-1.8357 5.1315-0.42447 21.99-1.5666 23.773-1.273 1.9866-3.962-12.31-6.8063-9.236-11.603 12.54-16.279 20.379-22.336 30.607-3.3589 5.6725-2.1817 23.33-3.506 24.674-1.3023 1.3215-3.8566-18.326-7.6437-14.309-8.5193 9.038-14.054 13.441-18.946 19.252-5.1981 6.1739-0.78251 17.584-5.0672 35.383l0.1448 0.22073c77.447-50.308 101.52-127.16 107.61-181.19-0.68051 63.93-29.41 142.78-105.33 184.65l0.1127 0.17141c20.241-2.181 22.307 10.458 44.562-4.2837 55.792-48.277 81.856-124.29 61.593-199.78z" fill="url(#linearGradient918)"/>
|
||||
<g transform="translate(0 2.0218e-5)">
|
||||
<path transform="translate(-5.7543,-56.594)" d="m111.25 81.024c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" display="none" fill="url(#linearGradient22718)"/>
|
||||
<path transform="translate(-5.754,-56.594)" d="m111.25 81.024c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" display="none" fill="url(#linearGradient23463)"/>
|
||||
<path d="m105.5 24.43c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" fill="url(#linearGradient6949)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.6 KiB |
@@ -8,6 +8,7 @@
|
||||
data_get($application, 'previews', collect([]))->count() > 0 ||
|
||||
data_get($application, 'ports_mappings_array')) &&
|
||||
data_get($application, 'settings.is_raw_compose_deployment_enabled') !== true)
|
||||
<div class="flex flex-col gap-1">
|
||||
@if (data_get($application, 'gitBrancLocation'))
|
||||
<a target="_blank" class="dropdown-item" href="{{ $application->gitBranchLocation }}">
|
||||
<x-git-icon git="{{ $application->source?->getMorphClass() }}" />
|
||||
@@ -19,14 +20,7 @@
|
||||
@if (data_get($fqdn, 'domain'))
|
||||
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
|
||||
<a class="dropdown-item" target="_blank" href="{{ getFqdnWithoutPort($domain) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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 d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>{{ getFqdnWithoutPort($domain) }}
|
||||
<x-external-link class="size-4" />{{ getFqdnWithoutPort($domain) }}
|
||||
</a>
|
||||
@endforeach
|
||||
@endif
|
||||
@@ -35,13 +29,7 @@
|
||||
@if (data_get($application, 'fqdn'))
|
||||
@foreach (str(data_get($application, 'fqdn'))->explode(',') as $fqdn)
|
||||
<a class="dropdown-item" target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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 d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>{{ getFqdnWithoutPort($fqdn) }}
|
||||
<x-external-link class="size-4" />{{ getFqdnWithoutPort($fqdn) }}
|
||||
</a>
|
||||
@endforeach
|
||||
@endif
|
||||
@@ -52,15 +40,8 @@
|
||||
@if (data_get($fqdn, 'domain'))
|
||||
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
|
||||
<a class="dropdown-item" target="_blank" href="{{ getFqdnWithoutPort($domain) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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 d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>PR{{ data_get($preview, 'pull_request_id') }} |
|
||||
<x-external-link class="size-4" />PR{{ data_get($preview, 'pull_request_id') }}
|
||||
|
|
||||
{{ getFqdnWithoutPort($domain) }}
|
||||
</a>
|
||||
@endforeach
|
||||
@@ -72,15 +53,7 @@
|
||||
@if (data_get($preview, 'fqdn'))
|
||||
<a class="dropdown-item" target="_blank"
|
||||
href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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 d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>
|
||||
<x-external-link class="size-4" />
|
||||
PR{{ data_get($preview, 'pull_request_id') }} |
|
||||
{{ data_get($preview, 'fqdn') }}
|
||||
</a>
|
||||
@@ -92,42 +65,20 @@
|
||||
@foreach ($application->ports_mappings_array as $port)
|
||||
@if ($application->destination->server->id === 0)
|
||||
<a class="dropdown-item" target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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 d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>
|
||||
<x-external-link class="size-4" />
|
||||
Port {{ $port }}
|
||||
</a>
|
||||
@else
|
||||
<a class="dropdown-item" target="_blank"
|
||||
href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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 d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>
|
||||
<x-external-link class="size-4" />
|
||||
{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}
|
||||
</a>
|
||||
@if (count($application->additional_servers) > 0)
|
||||
@foreach ($application->additional_servers as $server)
|
||||
<a class="dropdown-item" target="_blank"
|
||||
href="http://{{ $server->ip }}:{{ explode(':', $port)[0] }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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 d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>
|
||||
<x-external-link class="size-4" />
|
||||
{{ $server->ip }}:{{ explode(':', $port)[0] }}
|
||||
</a>
|
||||
@endforeach
|
||||
@@ -135,6 +86,7 @@
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="px-2 py-1.5 text-xs">No links available</div>
|
||||
@endif
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<svg class="inline-flex w-3 h-3 ml-1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"
|
||||
focusable="false" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none">
|
||||
</path>
|
||||
<path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z" fill="currentColor">
|
||||
</path>
|
||||
@props(['class' => 'inline-flex w-3 h-3 dark:text-neutral-400 text-black'])
|
||||
|
||||
<svg {{ $attributes->merge(['class' => $class]) }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 356 B |
@@ -32,7 +32,7 @@
|
||||
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}">
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif>
|
||||
|
||||
</div>
|
||||
@else
|
||||
@@ -45,7 +45,7 @@
|
||||
max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}"
|
||||
maxlength="{{ $attributes->get('maxlength') }}"
|
||||
@if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}"
|
||||
placeholder="{{ $attributes->get('placeholder') }}">
|
||||
placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif>
|
||||
@endif
|
||||
@if (!$label && $helper)
|
||||
<x-helper :helper="$helper" />
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<nav class="flex flex-col flex-1 px-2 bg-white border-r dark:border-coolgray-200 dark:bg-base" x-data="{
|
||||
<nav class="flex flex-col flex-1 px-2 bg-white border-r dark:border-coolgray-200 border-neutral-300 dark:bg-base"
|
||||
x-data="{
|
||||
switchWidth() {
|
||||
if (this.full === 'full') {
|
||||
localStorage.setItem('pageWidth', 'center');
|
||||
@@ -75,45 +76,13 @@
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}">
|
||||
}">
|
||||
<div class="flex pt-6 pb-4 pl-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div>
|
||||
<x-version />
|
||||
</div>
|
||||
<div class="pt-1">
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
<div class="flex justify-end w-8" x-show="theme === 'dark' || theme === 'system'">
|
||||
<svg class="w-5 h-5 rounded-sm dark:fill-white" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex justify-end w-8" x-show="theme === 'light'">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</x-slot:title>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="font-bold border-b dark:border-coolgray-500 dark:text-white text-md">Color</div>
|
||||
<button @click="setTheme('dark')" class="px-1 dropdown-item-no-padding">Dark</button>
|
||||
<button @click="setTheme('light')" class="px-1 dropdown-item-no-padding">Light</button>
|
||||
<button @click="setTheme('system')" class="px-1 dropdown-item-no-padding">System</button>
|
||||
<div class="my-1 font-bold border-b dark:border-coolgray-500 dark:text-white text-md">Width</div>
|
||||
<button @click="switchWidth()" class="px-1 dropdown-item-no-padding"
|
||||
x-show="full === 'full'">Center</button>
|
||||
<button @click="switchWidth()" class="px-1 dropdown-item-no-padding"
|
||||
x-show="full === 'center'">Full</button>
|
||||
<div class="my-1 font-bold border-b dark:border-coolgray-500 dark:text-white text-md">Zoom</div>
|
||||
<button @click="setZoom(100)" class="px-1 dropdown-item-no-padding">100%</button>
|
||||
<button @click="setZoom(90)" class="px-1 dropdown-item-no-padding">90%</button>
|
||||
</div>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
<livewire:settings-dropdown />
|
||||
</div>
|
||||
<div class="px-2 pt-2 pb-7">
|
||||
<livewire:switch-team />
|
||||
@@ -196,8 +165,8 @@
|
||||
class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('storage.index') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M4 6a8 3 0 1 0 16 0A8 3 0 1 0 4 6" />
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0V6" />
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||
@@ -211,8 +180,8 @@
|
||||
class="{{ request()->is('shared-variables*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('shared-variables.index') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path
|
||||
d="M5 4C2.5 9 2.5 14 5 20M19 4c2.5 5 2.5 10 0 16M9 9h1c1 0 1 1 2.016 3.527C13 15 13 16 14 16h1" />
|
||||
<path d="M8 16c1.5 0 3-2 4-3.5S14.5 9 16 9" />
|
||||
|
@@ -5,13 +5,7 @@
|
||||
</x-slot>
|
||||
@foreach ($links as $link)
|
||||
<a class="dropdown-item" target="_blank" href="{{ $link }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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 d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>{{ $link }}
|
||||
<x-external-link class="size-4" />{{ $link }}
|
||||
</a>
|
||||
@endforeach
|
||||
</x-dropdown>
|
||||
|
@@ -6,7 +6,8 @@
|
||||
@if ($exception->getMessage())
|
||||
<p class="text-base leading-7 text-red-500">{{ $exception->getMessage() }}</p>
|
||||
@else
|
||||
<p class="text-base leading-7 text-neutral-300">The request could not be understood by the server due to
|
||||
<p class="text-base leading-7 dark:text-neutral-400 text-black">The request could not be understood by the
|
||||
server due to
|
||||
malformed syntax.
|
||||
</p>
|
||||
@endif
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div>
|
||||
<p class="font-mono font-semibold text-7xl dark:text-warning">401</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">You shall not pass!</h1>
|
||||
<p class="text-base leading-7 text-neutral-300">You don't have permission to access this page.
|
||||
<p class="text-base leading-7 dark:text-neutral-400 text-black">You don't have permission to access this page.
|
||||
</p>
|
||||
<div class="flex items-center mt-10 gap-x-6">
|
||||
<a href="/">
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div>
|
||||
<p class="font-mono font-semibold text-7xl dark:text-warning">403</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">You shall not pass!</h1>
|
||||
<p class="text-base leading-7 text-neutral-300">You don't have permission to access this page.
|
||||
<p class="text-base leading-7 dark:text-neutral-400 text-black">You don't have permission to access this page.
|
||||
</p>
|
||||
<div class="flex items-center mt-10 gap-x-6">
|
||||
<a href="/">
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div>
|
||||
<p class="font-mono font-semibold text-7xl dark:text-warning">404</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">How did you get here?</h1>
|
||||
<p class="text-base leading-7 text-neutral-300">Sorry, we couldn’t find the page you’re looking
|
||||
<p class="text-base leading-7 dark:text-neutral-400 text-black">Sorry, we couldn’t find the page you’re looking
|
||||
for.
|
||||
</p>
|
||||
<div class="flex items-center mt-10 gap-x-6">
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div>
|
||||
<p class="font-mono font-semibold text-7xl dark:text-warning">419</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">This page is definitely old, not like you!</h1>
|
||||
<p class="text-base leading-7 text-neutral-300">Sorry, we couldn’t find the page you’re looking
|
||||
<p class="text-base leading-7 dark:text-neutral-300 text-black">Sorry, we couldn’t find the page you’re looking
|
||||
for.
|
||||
</p>
|
||||
<div class="flex items-center mt-10 gap-x-6">
|
||||
|
@@ -3,7 +3,8 @@
|
||||
<div>
|
||||
<p class="font-mono font-semibold text-7xl dark:text-warning">429</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">Woah, slow down there!</h1>
|
||||
<p class="text-base leading-7 text-neutral-300">You're making too many requests. Please wait a few
|
||||
<p class="text-base leading-7 dark:text-neutral-400 text-black">You're making too many requests. Please wait a
|
||||
few
|
||||
seconds before trying again.
|
||||
</p>
|
||||
<div class="flex items-center mt-10 gap-x-6">
|
||||
|
@@ -3,7 +3,8 @@
|
||||
<div class="w-full max-w-3xl px-8">
|
||||
<p class="font-mono font-semibold text-red-500 text-[200px] leading-none">500</p>
|
||||
<h1 class="text-3xl font-bold tracking-tight dark:text-white">Wait, this is not cool...</h1>
|
||||
<p class="mt-2 text-lg leading-7 text-neutral-300">There has been an error with the following error message:</p>
|
||||
<p class="mt-2 text-lg leading-7 dark:text-neutral-400 text-black">There has been an error with the following
|
||||
error message:</p>
|
||||
@if ($exception->getMessage() !== '')
|
||||
<div class="mt-6 text-sm text-red-500">
|
||||
{!! Purify::clean($exception->getMessage()) !!}
|
||||
@@ -13,8 +14,8 @@
|
||||
<a href="/">
|
||||
<x-forms.button>Go back home</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-sm hover:text-neutral-300 flex items-center gap-1" href="{{ config('constants.urls.contact') }}">
|
||||
Contact support
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
support
|
||||
<x-external-link />
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -3,14 +3,13 @@
|
||||
<div>
|
||||
<p class="font-mono font-semibold text-7xl dark:text-warning">503</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">We are working on serious things.</h1>
|
||||
<p class="text-base leading-7 text-black dark:text-neutral-300">Service Unavailable. Be right back. Thanks for your
|
||||
<p class="text-base leading-7 dark:text-neutral-400 text-black">Service Unavailable. Be right back. Thanks for
|
||||
your
|
||||
patience.
|
||||
</p>
|
||||
<div class="flex items-center mt-10 gap-x-6">
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
support
|
||||
<x-external-link />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -66,7 +66,8 @@
|
||||
<h3 class="py-4">Deployments</h3>
|
||||
<div class="flex flex-wrap w-full gap-4">
|
||||
@foreach (data_get($application, 'previews') as $previewName => $preview)
|
||||
<div class="flex flex-col w-full p-4 border dark:border-coolgray-200" wire:key="preview-container-{{ $preview->pull_request_id }}">
|
||||
<div class="flex flex-col w-full p-4 border dark:border-coolgray-200"
|
||||
wire:key="preview-container-{{ $preview->pull_request_id }}">
|
||||
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
|
||||
@if (str(data_get($preview, 'status'))->startsWith('running'))
|
||||
<x-status.running :status="data_get($preview, 'status')" />
|
||||
@@ -85,6 +86,18 @@
|
||||
PR on Git
|
||||
<x-external-link />
|
||||
</a>
|
||||
@if (count($parameters) > 0)
|
||||
|
|
||||
<a
|
||||
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
|
||||
Deployment Logs
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
|
||||
Application Logs
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
@@ -100,7 +113,8 @@
|
||||
</form>
|
||||
@else
|
||||
@foreach (collect(json_decode($preview->docker_compose_domains)) as $serviceName => $service)
|
||||
<livewire:project.application.previews-compose wire:key="preview-{{ $preview->pull_request_id }}-{{ $serviceName }}"
|
||||
<livewire:project.application.previews-compose
|
||||
wire:key="preview-{{ $preview->pull_request_id }}-{{ $serviceName }}"
|
||||
:service="$service" :serviceName="$serviceName" :preview="$preview" />
|
||||
@endforeach
|
||||
@endif
|
||||
@@ -114,21 +128,7 @@
|
||||
Domain</x-forms.button>
|
||||
</form>
|
||||
@endif
|
||||
<div class="flex items-center gap-2 pt-6">
|
||||
@if (count($parameters) > 0)
|
||||
<a
|
||||
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
|
||||
<x-forms.button>
|
||||
Deployment Logs
|
||||
</x-forms.button>
|
||||
</a>
|
||||
<a
|
||||
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
|
||||
<x-forms.button>
|
||||
Application Logs
|
||||
</x-forms.button>
|
||||
</a>
|
||||
@endif
|
||||
<div class="flex flex-col xl:flex-row xl:items-center gap-2 pt-6">
|
||||
<div class="flex-1"></div>
|
||||
<x-forms.button
|
||||
wire:click="force_deploy_without_cache({{ data_get($preview, 'pull_request_id') }})">
|
||||
|
@@ -18,7 +18,7 @@
|
||||
shortConfirmationLabel="Database Name" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="w-48 pb-2">
|
||||
<div class="w-64 pb-2">
|
||||
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
|
||||
@if ($s3s->count() > 0)
|
||||
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" />
|
||||
@@ -26,11 +26,18 @@
|
||||
<x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3"
|
||||
disabled />
|
||||
@endif
|
||||
@if ($backup->save_s3)
|
||||
<x-forms.checkbox instantSave label="Disable Local Backup" id="disableLocalBackup"
|
||||
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
|
||||
@else
|
||||
<x-forms.checkbox disabled label="Disable Local Backup" id="disableLocalBackup"
|
||||
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
|
||||
@endif
|
||||
</div>
|
||||
@if ($backup->save_s3)
|
||||
<div class="pb-6">
|
||||
<x-forms.select id="s3StorageId" label="S3 Storage" required>
|
||||
<option value="default">Select a S3 storage</option>
|
||||
<option value="default" disabled>Select a S3 storage</option>
|
||||
@foreach ($s3s as $s3)
|
||||
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
|
||||
@endforeach
|
||||
@@ -77,7 +84,7 @@
|
||||
<x-forms.input label="Frequency" id="frequency" />
|
||||
<x-forms.input label="Timezone" id="timezone" disabled
|
||||
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" />
|
||||
<x-forms.input label="Timeout" id="timeout" helper="The timeout of the backup job in seconds."/>
|
||||
<x-forms.input label="Timeout" id="timeout" helper="The timeout of the backup job in seconds." />
|
||||
</div>
|
||||
|
||||
<h3 class="mt-6 mb-2 text-lg font-medium">Backup Retention Settings</h3>
|
||||
|
@@ -132,8 +132,16 @@
|
||||
id="database.public_port" label="Public Port" />
|
||||
</div>
|
||||
<x-forms.textarea
|
||||
helper="<a target='_blank' class='underline dark:text-white' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>Redis Default Configuration</a>"
|
||||
placeholder="# maxmemory 256mb
|
||||
# maxmemory-policy allkeys-lru
|
||||
# timeout 300"
|
||||
helper="You only need to provide the Redis directives you want to override — Redis will use default values for everything else. <br/><br/>
|
||||
⚠️ <strong>Important:</strong> Coolify automatically applies the requirepass directive using the password shown in the Password field above. If you override requirepass in your custom configuration, make sure it matches the password field to avoid authentication issues. <br/><br/>
|
||||
🔗 <strong>Tip:</strong> <a target='_blank' class='underline dark:text-white' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>View the full Redis default configuration</a> to see what options are available."
|
||||
label="Custom Redis Configuration" rows="10" id="database.redis_conf" />
|
||||
|
||||
|
||||
|
||||
<h3 class="pt-4">Advanced</h3>
|
||||
<div class="flex flex-col">
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
|
@@ -1,12 +1,13 @@
|
||||
<div>
|
||||
<div x-data x-init="$nextTick(() => { if ($refs.autofocusInput) $refs.autofocusInput.focus(); })">
|
||||
<h1>Create a new Application</h1>
|
||||
<div class="pb-4">Deploy any public Git repositories.</div>
|
||||
|
||||
<!-- Repository URL Form -->
|
||||
<form class="flex flex-col gap-2" wire:submit='loadBranch'>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2 items-end">
|
||||
<x-forms.input required id="repository_url" label="Repository URL (https://)"
|
||||
helper="{!! __('repository.url') !!}" />
|
||||
helper="{!! __('repository.url') !!}" autofocus />
|
||||
<x-forms.button type="submit">
|
||||
Check repository
|
||||
</x-forms.button>
|
||||
@@ -16,6 +17,9 @@
|
||||
href="https://github.com/coollabsio/coolify-examples/" target="_blank">Coolify
|
||||
Examples</a>.
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if ($branchFound)
|
||||
@if ($rate_limit_remaining && $rate_limit_reset)
|
||||
<div class="flex gap-2 py-2">
|
||||
@@ -24,6 +28,9 @@
|
||||
helper="Rate limit remaining: {{ $rate_limit_remaining }}<br>Rate limit reset at: {{ $rate_limit_reset }} UTC" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Application Configuration Form -->
|
||||
<form class="flex flex-col gap-2 pt-4" wire:submit='submit'>
|
||||
<div class="flex flex-col gap-2 pb-6">
|
||||
<div class="flex gap-2">
|
||||
@if ($git_source === 'other')
|
||||
@@ -45,13 +52,18 @@
|
||||
@endif
|
||||
</div>
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col">
|
||||
<x-forms.input placeholder="/" wire:model.blur-sm="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." />
|
||||
helper="Directory to use as root. Useful for monorepos." x-model="baseDir" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml" wire:model.blur-sm="docker_compose_location"
|
||||
label="Docker Compose Location"
|
||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($base_directory . $docker_compose_location, '/') }}</span>" />
|
||||
Compose file location in your repository:<span
|
||||
class='dark:text-warning'>{{ Str::start($base_directory . $docker_compose_location, '/') }}</span>
|
||||
label="Docker Compose Location" helper="It is calculated together with the Base Directory."
|
||||
x-model="composeLocation" />
|
||||
<div class="pt-2">
|
||||
<span>
|
||||
Compose file location in your repository: </span><span class='dark:text-warning'
|
||||
x-text='(baseDir === "/" ? "" : baseDir) + (composeLocation.startsWith("/") ? composeLocation : "/" + composeLocation)'></span>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<x-forms.input wire:model="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." />
|
||||
@@ -64,21 +76,10 @@
|
||||
helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
|
||||
</div>
|
||||
@endif
|
||||
{{-- <div class="w-64">
|
||||
<x-forms.checkbox helper="If your repository contains a coolify.json file, it will be used to configure your application." instantSave id="checkCoolifyConfig" label="Use coolify.json if exists?" />
|
||||
</div> --}}
|
||||
{{-- @if ($build_pack === 'dockercompose' && isDev())
|
||||
<div class="dark:text-warning">If you choose Docker Compose based deployments, you cannot
|
||||
change it afterwards.</div>
|
||||
<x-forms.checkbox instantSave label="New Compose Services (only in dev mode)"
|
||||
id="new_compose_services"></x-forms.checkbox>
|
||||
@endif --}}
|
||||
</div>
|
||||
<x-forms.button wire:click.prevent='submit'>
|
||||
<x-forms.button type="submit">
|
||||
Continue
|
||||
</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
@@ -2,6 +2,9 @@
|
||||
<div>
|
||||
<div class="flex gap-2">
|
||||
<h2>Service Stack</h2>
|
||||
@if (isDev())
|
||||
<div>{{ $service->compose_parsing_version }}</div>
|
||||
@endif
|
||||
<x-forms.button wire:target='submit' type="submit">Save</x-forms.button>
|
||||
<x-modal-input buttonTitle="Edit Compose File" title="Edit Docker Compose" :closeOutside="false">
|
||||
<livewire:project.service.edit-compose serviceId="{{ $service->id }}" />
|
||||
|
@@ -9,7 +9,8 @@
|
||||
</svg>
|
||||
<div class="text-center">
|
||||
<h3 class="text-lg font-medium">Terminal Not Available</h3>
|
||||
<p class="mt-2 text-sm text-gray-500">No shell (bash/sh) is available in this container. Please
|
||||
<p class="mt-2 text-sm text-neutral-300">No shell (bash/sh) is available in this container.
|
||||
Please
|
||||
ensure either bash or sh is installed to use the terminal.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -22,6 +22,10 @@
|
||||
soon.<br/>Status notifications sent every week.<br/>You can disable notifications in the <a class='dark:text-white underline' href='{{ route('notifications.email') }}'>notification settings</a>." />
|
||||
<x-forms.button type="button" wire:click="$dispatch('checkForUpdatesDispatch')">
|
||||
Check Now</x-forms.button>
|
||||
@if (isDev())
|
||||
<x-forms.button type="button" wire:click="sendTestEmail">
|
||||
Send Test Email (dev only)</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
<div>Update your servers semi-automatically.</div>
|
||||
<div>
|
||||
|
337
resources/views/livewire/settings-dropdown.blade.php
Normal file
337
resources/views/livewire/settings-dropdown.blade.php
Normal file
@@ -0,0 +1,337 @@
|
||||
<div x-data="{
|
||||
dropdownOpen: false,
|
||||
search: '',
|
||||
allEntries: [],
|
||||
init() {
|
||||
this.mounted();
|
||||
// Load all entries when component initializes
|
||||
this.allEntries = @js($entries->toArray());
|
||||
},
|
||||
markEntryAsRead(tagName) {
|
||||
// Update the entry in our local Alpine data
|
||||
const entry = this.allEntries.find(e => e.tag_name === tagName);
|
||||
if (entry) {
|
||||
entry.is_read = true;
|
||||
}
|
||||
// Call Livewire to update server-side
|
||||
$wire.markAsRead(tagName);
|
||||
},
|
||||
markAllEntriesAsRead() {
|
||||
// Update all entries in our local Alpine data
|
||||
this.allEntries.forEach(entry => {
|
||||
entry.is_read = true;
|
||||
});
|
||||
// Call Livewire to update server-side
|
||||
$wire.markAllAsRead();
|
||||
},
|
||||
switchWidth() {
|
||||
if (this.full === 'full') {
|
||||
localStorage.setItem('pageWidth', 'center');
|
||||
} else {
|
||||
localStorage.setItem('pageWidth', 'full');
|
||||
}
|
||||
window.location.reload();
|
||||
},
|
||||
setZoom(zoom) {
|
||||
localStorage.setItem('zoom', zoom);
|
||||
window.location.reload();
|
||||
},
|
||||
setTheme(type) {
|
||||
this.theme = type;
|
||||
localStorage.setItem('theme', type);
|
||||
this.queryTheme();
|
||||
},
|
||||
queryTheme() {
|
||||
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const userSettings = localStorage.getItem('theme') || 'dark';
|
||||
localStorage.setItem('theme', userSettings);
|
||||
if (userSettings === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
this.theme = 'dark';
|
||||
} else if (userSettings === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
this.theme = 'light';
|
||||
} else if (darkModePreference) {
|
||||
this.theme = 'system';
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (!darkModePreference) {
|
||||
this.theme = 'system';
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.full = localStorage.getItem('pageWidth');
|
||||
this.zoom = localStorage.getItem('zoom');
|
||||
this.queryTheme();
|
||||
},
|
||||
get filteredEntries() {
|
||||
let entries = this.allEntries;
|
||||
|
||||
// Apply search filter if search term exists
|
||||
if (this.search && this.search.trim() !== '') {
|
||||
const searchLower = this.search.trim().toLowerCase();
|
||||
entries = entries.filter(entry => {
|
||||
return (entry.title?.toLowerCase().includes(searchLower) ||
|
||||
entry.content?.toLowerCase().includes(searchLower) ||
|
||||
entry.tag_name?.toLowerCase().includes(searchLower));
|
||||
});
|
||||
}
|
||||
|
||||
// Always sort: unread first, then by published date (newest first)
|
||||
return entries.sort((a, b) => {
|
||||
// First sort by read status (unread first)
|
||||
if (a.is_read !== b.is_read) {
|
||||
return a.is_read ? 1 : -1; // unread (false) comes before read (true)
|
||||
}
|
||||
// Then sort by published date (newest first)
|
||||
return new Date(b.published_at) - new Date(a.published_at);
|
||||
});
|
||||
}
|
||||
}" @click.outside="dropdownOpen = false">
|
||||
<!-- Custom Dropdown without arrow -->
|
||||
<div class="relative">
|
||||
<button @click="dropdownOpen = !dropdownOpen"
|
||||
class="relative p-2 dark:text-neutral-400 hover:dark:text-white transition-colors cursor-pointer"
|
||||
title="Settings">
|
||||
<!-- Gear Icon -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="Settings">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
|
||||
<!-- Unread Count Badge -->
|
||||
@if ($unreadCount > 0)
|
||||
<span
|
||||
class="absolute -top-1 -right-1 bg-error text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">
|
||||
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
|
||||
</span>
|
||||
@endif
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div x-show="dropdownOpen" x-transition:enter="ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="ease-in duration-150" x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2" class="absolute right-0 top-full mt-1 z-50 w-48" x-cloak>
|
||||
<div
|
||||
class="p-1 bg-white border rounded-sm shadow-lg dark:bg-coolgray-200 dark:border-black border-neutral-300">
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- What's New Section -->
|
||||
@if ($unreadCount > 0)
|
||||
<button wire:click="openWhatsNewModal" @click="dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>What's New</span>
|
||||
</div>
|
||||
<span
|
||||
class="bg-error text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
|
||||
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
|
||||
</span>
|
||||
</button>
|
||||
@else
|
||||
<button wire:click="openWhatsNewModal" @click="dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span>Changelog</span>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="border-b dark:border-coolgray-500 border-neutral-300"></div>
|
||||
|
||||
<!-- Theme Section -->
|
||||
<div class="font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white pb-1">
|
||||
Appearance</div>
|
||||
<button @click="setTheme('dark'); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<span>Dark</span>
|
||||
</button>
|
||||
<button @click="setTheme('light'); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<span>Light</span>
|
||||
</button>
|
||||
<button @click="setTheme('system'); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>System</span>
|
||||
</button>
|
||||
|
||||
<!-- Width Section -->
|
||||
<div
|
||||
class="my-1 font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white text-md">
|
||||
Width</div>
|
||||
<button @click="switchWidth(); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2" x-show="full === 'full'">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h7" />
|
||||
</svg>
|
||||
<span>Center</span>
|
||||
</button>
|
||||
<button @click="switchWidth(); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2" x-show="full === 'center'">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<span>Full</span>
|
||||
</button>
|
||||
|
||||
<!-- Zoom Section -->
|
||||
<div
|
||||
class="my-1 font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white text-md">
|
||||
Zoom</div>
|
||||
<button @click="setZoom(100); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<span>100%</span>
|
||||
</button>
|
||||
<button @click="setZoom(90); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 10h4v4h-4v-4z" />
|
||||
</svg>
|
||||
<span>90%</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- What's New Modal -->
|
||||
@if ($showWhatsNewModal)
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center py-6 px-4"
|
||||
@keydown.escape.window="$wire.closeWhatsNewModal()">
|
||||
<!-- Background overlay -->
|
||||
<div class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs" wire:click="closeWhatsNewModal">
|
||||
</div>
|
||||
|
||||
<!-- Modal panel -->
|
||||
<div
|
||||
class="relative w-full h-full max-w-7xl py-6 border rounded-sm drop-shadow-sm bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300 flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between pb-3">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold dark:text-white">
|
||||
Changelog
|
||||
</h3>
|
||||
<p class="mt-1 text-sm dark:text-neutral-400">
|
||||
Stay up to date with the latest features and improvements.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@if (isDev())
|
||||
<x-forms.button wire:click="manualFetchChangelog"
|
||||
class="bg-coolgray-200 hover:bg-coolgray-300">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Fetch Latest
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if ($unreadCount > 0)
|
||||
<x-forms.button @click="markAllEntriesAsRead">
|
||||
Mark all as read
|
||||
</x-forms.button>
|
||||
@endif
|
||||
<button wire:click="closeWhatsNewModal"
|
||||
class="flex items-center justify-center w-8 h-8 rounded-full dark:text-white hover:bg-neutral-100 dark:hover:bg-coolgray-300 outline-0 cursor-pointer">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="pb-4 border-b dark:border-coolgray-200 flex-shrink-0">
|
||||
<div class="relative">
|
||||
<input x-model="search" placeholder="Search updates..." class="input pl-10" />
|
||||
<svg class="absolute left-3 top-2 w-4 h-4 dark:text-neutral-400" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="py-4 flex-1 overflow-y-auto scrollbar">
|
||||
<div x-show="filteredEntries.length > 0">
|
||||
<div class="space-y-4">
|
||||
<template x-for="entry in filteredEntries" :key="entry.tag_name">
|
||||
<div class="relative p-4 border dark:border-coolgray-300 rounded-sm"
|
||||
:class="!entry.is_read ? 'dark:bg-coolgray-200 border-warning' : 'dark:bg-coolgray-100'">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span x-show="entry.title"
|
||||
class="px-2 py-1 text-xs font-semibold dark:bg-coolgray-300 dark:text-neutral-200 rounded-sm"><a
|
||||
:href="`https://github.com/coollabsio/coolify/releases/tag/${entry.tag_name}`"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-1 hover:text-coolgray-500">
|
||||
<span x-text="entry.title"></span>
|
||||
<x-external-link />
|
||||
</a></span>
|
||||
<span class="text-xs dark:text-neutral-400"
|
||||
x-text="new Date(entry.published_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></span>
|
||||
</div>
|
||||
<div class="dark:text-neutral-300 leading-relaxed max-w-none"
|
||||
x-html="entry.content_html">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button x-show="!entry.is_read" @click="markEntryAsRead(entry.tag_name)"
|
||||
class="ml-4 px-3 py-1 text-xs dark:text-neutral-400 hover:dark:text-white border dark:border-neutral-600 rounded hover:dark:bg-neutral-700 transition-colors cursor-pointer"
|
||||
title="Mark as read">
|
||||
mark as read
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="filteredEntries.length === 0" class="text-center py-8">
|
||||
<svg class="mx-auto h-12 w-12 dark:text-neutral-400" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium dark:text-white">No updates found</h3>
|
||||
<p class="mt-1 text-sm dark:text-neutral-400">
|
||||
<span x-show="search.trim() !== ''">No updates match your search criteria.</span>
|
||||
<span x-show="search.trim() === ''">There are no updates available at the moment.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
@@ -45,7 +45,10 @@ Route::group([
|
||||
|
||||
Route::get('/projects', [ProjectController::class, 'projects'])->middleware(['api.ability:read']);
|
||||
Route::get('/projects/{uuid}', [ProjectController::class, 'project_by_uuid'])->middleware(['api.ability:read']);
|
||||
Route::get('/projects/{uuid}/environments', [ProjectController::class, 'get_environments'])->middleware(['api.ability:read']);
|
||||
Route::get('/projects/{uuid}/{environment_name_or_uuid}', [ProjectController::class, 'environment_details'])->middleware(['api.ability:read']);
|
||||
Route::post('/projects/{uuid}/environments', [ProjectController::class, 'create_environment'])->middleware(['api.ability:write']);
|
||||
Route::delete('/projects/{uuid}/environments/{environment_name_or_uuid}', [ProjectController::class, 'delete_environment'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::post('/projects', [ProjectController::class, 'create_project'])->middleware(['api.ability:read']);
|
||||
Route::patch('/projects/{uuid}', [ProjectController::class, 'update_project'])->middleware(['api.ability:write']);
|
||||
|
@@ -8,13 +8,13 @@ services:
|
||||
activepieces:
|
||||
image: "ghcr.io/activepieces/activepieces:latest"
|
||||
environment:
|
||||
- SERVICE_FQDN_ACTIVEPIECES
|
||||
- SERVICE_URL_ACTIVEPIECES
|
||||
- AP_API_KEY=$SERVICE_PASSWORD_64_APIKEY
|
||||
- AP_ENCRYPTION_KEY=$SERVICE_PASSWORD_ENCRYPTIONKEY
|
||||
- AP_ENGINE_EXECUTABLE_PATH=${AP_ENGINE_EXECUTABLE_PATH:-dist/packages/engine/main.js}
|
||||
- AP_ENVIRONMENT=${AP_ENVIRONMENT:-prod}
|
||||
- AP_EXECUTION_MODE=${AP_EXECUTION_MODE:-UNSANDBOXED}
|
||||
- AP_FRONTEND_URL=${SERVICE_FQDN_ACTIVEPIECES}
|
||||
- AP_FRONTEND_URL=${SERVICE_URL_ACTIVEPIECES}
|
||||
- AP_JWT_SECRET=$SERVICE_PASSWORD_64_JWT
|
||||
- AP_POSTGRES_DATABASE=${POSTGRES_DB:-activepieces}
|
||||
- AP_POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
||||
|
@@ -8,7 +8,7 @@ services:
|
||||
actual_server:
|
||||
image: actualbudget/actual-server:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_ACTUAL_5006
|
||||
- SERVICE_URL_ACTUAL_5006
|
||||
- ACTUAL_LOGIN_METHOD=password
|
||||
volumes:
|
||||
- actual_data:/data
|
||||
|
@@ -20,13 +20,13 @@ services:
|
||||
options:
|
||||
max-size: 1000m
|
||||
environment:
|
||||
- SERVICE_FQDN_AFFINE_3010
|
||||
- SERVICE_URL_AFFINE_3010
|
||||
- AFFINE_CONFIG_PATH=/root/.affine/config
|
||||
- REDIS_SERVER_HOST=redis
|
||||
- DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-affine}
|
||||
- NODE_ENV=production
|
||||
- AFFINE_SERVER_HOST=$SERVICE_FQDN_AFFINE
|
||||
- AFFINE_SERVER_EXTERNAL_URL=$SERVICE_FQDN_AFFINE
|
||||
- AFFINE_SERVER_HOST=$SERVICE_URL_AFFINE
|
||||
- AFFINE_SERVER_EXTERNAL_URL=$SERVICE_URL_AFFINE
|
||||
- MAILER_HOST=${MAILER_HOST}
|
||||
- MAILER_PORT=${MAILER_PORT}
|
||||
- MAILER_USER=${MAILER_USER}
|
||||
|
@@ -8,7 +8,7 @@ services:
|
||||
anything-llm:
|
||||
image: mintplexlabs/anythingllm
|
||||
environment:
|
||||
- SERVICE_FQDN_ANYTHINGLLM_3001
|
||||
- SERVICE_URL_ANYTHINGLLM_3001
|
||||
- STORAGE_DIR=/app/server/storage
|
||||
- DISABLE_TELEMETRY=${DISABLE_TELEMETRY:-true}
|
||||
- PASSWORDLOWERCASE=${PASSWORDLOWERCASE:-1}
|
||||
|
@@ -8,7 +8,7 @@ services:
|
||||
apprise-api:
|
||||
image: linuxserver/apprise-api:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_APPRISE_8000
|
||||
- SERVICE_URL_APPRISE_8000
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=UTC
|
||||
|
@@ -7,7 +7,7 @@ services:
|
||||
appsmith:
|
||||
image: index.docker.io/appsmith/appsmith-ce:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_APPSMITH
|
||||
- SERVICE_URL_APPSMITH
|
||||
- APPSMITH_MAIL_ENABLED=${APPSMITH_MAIL_ENABLED:-false}
|
||||
- APPSMITH_DISABLE_TELEMETRY=${APPSMITH_DISABLE_TELEMETRY:-false}
|
||||
- APPSMITH_DISABLE_INTERCOM=${APPSMITH_DISABLE_INTERCOM:-true}
|
||||
|
@@ -20,7 +20,7 @@ services:
|
||||
- appwrite-mariadb
|
||||
- appwrite-redis
|
||||
environment:
|
||||
- SERVICE_FQDN_APPWRITE=/
|
||||
- SERVICE_URL_APPWRITE=/
|
||||
- _APP_ENV=${_APP_ENV:-production}
|
||||
- _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6}
|
||||
- _APP_LOCALE=${_APP_LOCALE:-en}
|
||||
@@ -128,7 +128,7 @@ services:
|
||||
image: appwrite/console:6.0.13
|
||||
container_name: appwrite-console
|
||||
environment:
|
||||
- SERVICE_FQDN_APPWRITE=/console
|
||||
- SERVICE_URL_APPWRITE=/console
|
||||
|
||||
appwrite-realtime:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -138,7 +138,7 @@ services:
|
||||
- appwrite-mariadb
|
||||
- appwrite-redis
|
||||
environment:
|
||||
- SERVICE_FQDN_APPWRITE=/v1/realtime
|
||||
- SERVICE_URL_APPWRITE=/v1/realtime
|
||||
- _APP_ENV=${_APP_ENV:-production}
|
||||
- _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6}
|
||||
- _APP_OPTIONS_ABUSE=${_APP_OPTIONS_ABUSE:-enabled}
|
||||
|
@@ -8,7 +8,7 @@ services:
|
||||
argilla:
|
||||
image: "argilla/argilla-server:v2.2.0"
|
||||
environment:
|
||||
- SERVICE_FQDN_ARGILLA_6900
|
||||
- SERVICE_URL_ARGILLA_6900
|
||||
- ARGILLA_HOME_PATH=/var/lib/argilla
|
||||
- ARGILLA_ELASTICSEARCH=http://elasticsearch:9200
|
||||
- ARGILLA_DATABASE_URL=postgresql+asyncpg://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB}
|
||||
|
@@ -8,7 +8,7 @@ services:
|
||||
audiobookshelf:
|
||||
image: ghcr.io/advplyr/audiobookshelf:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_AUDIOBOOKSHELF_80
|
||||
- SERVICE_URL_AUDIOBOOKSHELF_80
|
||||
- TZ=${TIMEZONE:-America/Toronto}
|
||||
volumes:
|
||||
- audiobookshelf-audiobooks:/audiobooks
|
||||
|
@@ -10,7 +10,7 @@ services:
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
- SERVICE_FQDN_AUTHENTIKSERVER_9000
|
||||
- SERVICE_URL_AUTHENTIKSERVER_9000
|
||||
- AUTHENTIK_REDIS__HOST=${REDIS_HOST:-redis}
|
||||
- AUTHENTIK_POSTGRESQL__HOST=${POSTGRES_HOST:-postgresql}
|
||||
- AUTHENTIK_POSTGRESQL__USER=${SERVICE_USER_POSTGRESQL}
|
||||
|
@@ -54,8 +54,8 @@ services:
|
||||
image: bytemark/smtp:latest
|
||||
platform: linux/amd64
|
||||
environment:
|
||||
- SERVICE_FQDN_SMTP
|
||||
- RELAY_HOST=$SERVICE_FQDN_SMTP
|
||||
- SERVICE_URL_SMTP
|
||||
- RELAY_HOST=$SERVICE_URL_SMTP
|
||||
- RELAY_PORT=${RELAY_PORT:-587}
|
||||
- RELAY_USERNAME=$SERVICE_EMAIL_SMTP
|
||||
- RELAY_PASSWORD=$SERVICE_PASSWORD_SMTP
|
||||
@@ -75,7 +75,7 @@ services:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- SERVICE_FQDN_AZIMUTT_4000
|
||||
- SERVICE_URL_AZIMUTT_4000
|
||||
- SENTRY=false
|
||||
- PHX_SERVER=true
|
||||
- PHX_HOST=$SERVICE_URL_AZIMUTT
|
||||
@@ -92,7 +92,7 @@ services:
|
||||
- S3_KEY_ID=${S3_KEY_ID}
|
||||
- S3_KEY_SECRET=${S3_KEY_SECRET}
|
||||
- EMAIL_ADAPTER=${EMAIL_ADAPTER:-smtp}
|
||||
- SMTP_RELAY=$SERVICE_FQDN_SMTP
|
||||
- SMTP_RELAY=$SERVICE_URL_SMTP
|
||||
- SMTP_USERNAME=$SERVICE_EMAIL_SMTP
|
||||
- SMTP_PASSWORD=$SERVICE_PASSWORD_SMTP
|
||||
- SMTP_PORT=${SMTP_PORT:-587}
|
||||
|
@@ -7,11 +7,11 @@ services:
|
||||
babybuddy:
|
||||
image: lscr.io/linuxserver/babybuddy:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_BABYBUDDY
|
||||
- SERVICE_URL_BABYBUDDY
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Madrid
|
||||
- CSRF_TRUSTED_ORIGINS=$SERVICE_FQDN_BABYBUDDY
|
||||
- CSRF_TRUSTED_ORIGINS=$SERVICE_URL_BABYBUDDY
|
||||
volumes:
|
||||
- babybuddy-config:/config
|
||||
healthcheck:
|
||||
|
@@ -10,7 +10,7 @@ services:
|
||||
beszel:
|
||||
image: henrygd/beszel:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_BESZEL_8090
|
||||
- SERVICE_URL_BESZEL_8090
|
||||
volumes:
|
||||
- beszel_data:/beszel_data
|
||||
|
||||
|
@@ -1,34 +1,64 @@
|
||||
# ignore: true
|
||||
# documentation: https://github.com/bluesky-social/pds
|
||||
# slogan: A social network for the decentralized web
|
||||
# tags: pds, bluesky, social, network, decentralized
|
||||
# logo:
|
||||
# slogan: Bluesky PDS (Personal Data Server)
|
||||
# tags: bluesky, pds, platform
|
||||
# logo: svgs/bluesky.svg
|
||||
# port: 3000
|
||||
|
||||
services:
|
||||
pds:
|
||||
image: ghcr.io/bluesky-social/pds:0.4
|
||||
image: 'ghcr.io/bluesky-social/pds:latest'
|
||||
volumes:
|
||||
- pds-data:/pds
|
||||
- ./pds-data:/pds
|
||||
environment:
|
||||
- SERVICE_FQDN_PDS_3000
|
||||
- PDS_JWT_SECRET=${SERVICE_BASE64_PDS}
|
||||
- PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_PDS}
|
||||
- PDS_ADMIN_EMAIL=${PDS_ADMIN_EMAIL:-admin@example.com}
|
||||
- PDS_DATADIR=${PDS_DATADIR:-/pds}
|
||||
- PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATADIR:-/pds}/blocks
|
||||
- PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-52428800}
|
||||
- SERVICE_URL_PDS_3000
|
||||
- PDS_HOSTNAME=${SERVICE_URL_PDS}
|
||||
- PDS_DID_PLC_URL=https://plc.directory
|
||||
- PDS_BSKY_APP_VIEW_URL=https://api.bsky.app
|
||||
- PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app
|
||||
- PDS_REPORT_SERVICE_URL=https://mod.bsky.app
|
||||
- PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
|
||||
- PDS_CRAWLERS=https://bsky.network
|
||||
- PDS_JWT_SECRET=${SERVICE_PASSWORD_JWT_SECRET}
|
||||
- PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}
|
||||
- PDS_ADMIN_EMAIL=${SERVICE_EMAIL_ADMIN}
|
||||
- PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX}
|
||||
- PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}
|
||||
- PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATA_DIRECTORY:-/pds}/blocks
|
||||
- PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-52428800}
|
||||
- PDS_DID_PLC_URL=${PDS_DID_PLC_URL:-https://plc.directory}
|
||||
- PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL:-https://api.bsky.app}
|
||||
- PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID:-did:web:api.bsky.app}
|
||||
- PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL:-https://mod.bsky.app/xrpc/com.atproto.moderation.createReport}
|
||||
- PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID:-did:plc:ar7c4by46qjdydhdevvrndac}
|
||||
- PDS_CRAWLERS=${PDS_CRAWLERS:-https://bsky.network}
|
||||
- LOG_ENABLED=${LOG_ENABLED:-true}
|
||||
- PDS_EMAIL_SMTP_URL=${PDS_EMAIL_SMTP_URL:-smtp://localhost:8025}
|
||||
- PDS_EMAIL_FROM_ADDRESS=${PDS_EMAIL_FROM_ADDRESS:-admin@example.com}
|
||||
- PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${SERVICE_HEX_32_ROTATIONKEY}
|
||||
|
||||
command: >
|
||||
sh -c '
|
||||
echo "Installing curl, bash, and pdsadmin..."
|
||||
apk add --no-cache curl bash && \
|
||||
curl -o /usr/local/bin/pdsadmin.sh https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin.sh && \
|
||||
chmod +x /usr/local/bin/pdsadmin.sh && \
|
||||
ln -sf /usr/local/bin/pdsadmin.sh /usr/local/bin/pdsadmin
|
||||
|
||||
echo "Generating /pds/pds.env..."
|
||||
printf "%s\n" \
|
||||
"SERVICE_FQDN_PDS_3000=$${SERVICE_FQDN_PDS_3000}" \
|
||||
"PDS_HOSTNAME=$${PDS_HOSTNAME}" \
|
||||
"PDS_JWT_SECRET=$${PDS_JWT_SECRET}" \
|
||||
"PDS_ADMIN_PASSWORD=$${PDS_ADMIN_PASSWORD}" \
|
||||
"PDS_ADMIN_EMAIL=$${PDS_ADMIN_EMAIL}" \
|
||||
"PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX}" \
|
||||
"PDS_DATA_DIRECTORY=$${PDS_DATA_DIRECTORY}" \
|
||||
"PDS_BLOBSTORE_DISK_LOCATION=$${PDS_DATA_DIRECTORY}/blocks" \
|
||||
"PDS_BLOB_UPLOAD_LIMIT=$${PDS_BLOB_UPLOAD_LIMIT}" \
|
||||
"PDS_DID_PLC_URL=$${PDS_DID_PLC_URL}" \
|
||||
"PDS_BSKY_APP_VIEW_URL=$${PDS_BSKY_APP_VIEW_URL}" \
|
||||
"PDS_BSKY_APP_VIEW_DID=$${PDS_BSKY_APP_VIEW_DID}" \
|
||||
"PDS_REPORT_SERVICE_URL=$${PDS_REPORT_SERVICE_URL}" \
|
||||
"PDS_REPORT_SERVICE_DID=$${PDS_REPORT_SERVICE_DID}" \
|
||||
"PDS_CRAWLERS=$${PDS_CRAWLERS}" \
|
||||
"LOG_ENABLED=$${LOG_ENABLED}" \
|
||||
> /pds/pds.env
|
||||
|
||||
echo "Launching PDS..."
|
||||
exec node --enable-source-maps index.js
|
||||
'
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "http://127.0.0.1:3000/xrpc/_health"]
|
||||
interval: 2s
|
||||
|
@@ -8,8 +8,8 @@ services:
|
||||
bookstack:
|
||||
image: lscr.io/linuxserver/bookstack:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_BOOKSTACK_80
|
||||
- APP_URL=${SERVICE_FQDN_BOOKSTACK}
|
||||
- SERVICE_URL_BOOKSTACK_80
|
||||
- APP_URL=${SERVICE_URL_BOOKSTACK}
|
||||
- APP_KEY=${SERVICE_PASSWORD_APPKEY}
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
|
@@ -8,7 +8,7 @@ services:
|
||||
browserless:
|
||||
image: ghcr.io/browserless/chromium
|
||||
environment:
|
||||
- SERVICE_FQDN_BROWSERLESS_3000
|
||||
- SERVICE_URL_BROWSERLESS_3000
|
||||
- TOKEN=$SERVICE_PASSWORD_BROWSERLESS
|
||||
expose:
|
||||
- 3000
|
||||
|
@@ -7,7 +7,7 @@ services:
|
||||
budge:
|
||||
image: lscr.io/linuxserver/budge:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_BUDGE
|
||||
- SERVICE_URL_BUDGE
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Madrid
|
||||
|
@@ -82,7 +82,7 @@ services:
|
||||
proxy-service:
|
||||
image: budibase/proxy
|
||||
environment:
|
||||
- SERVICE_FQDN_BUDIBASE_10000
|
||||
- SERVICE_URL_BUDIBASE_10000
|
||||
- PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
|
||||
- PROXY_RATE_LIMIT_API_PER_SECOND=20
|
||||
- APPS_UPSTREAM_URL=http://app-service:4002
|
||||
|
@@ -31,8 +31,8 @@ services:
|
||||
environment:
|
||||
- SECRET_KEY=$SERVICE_PASSWORD_64_BUGSINK
|
||||
- CREATE_SUPERUSER=admin:$SERVICE_PASSWORD_BUGSINK
|
||||
- SERVICE_FQDN_BUGSINK_8000
|
||||
- BASE_URL=$SERVICE_FQDN_BUGSINK_8000
|
||||
- SERVICE_URL_BUGSINK_8000
|
||||
- BASE_URL=$SERVICE_URL_BUGSINK_8000
|
||||
- DATABASE_URL=mysql://${SERVICE_USER_BUGSINK}:$SERVICE_PASSWORD_BUGSINK@mysql:3306/${MYSQL_DATABASE:-bugsink}
|
||||
depends_on:
|
||||
mysql:
|
||||
|
@@ -12,14 +12,14 @@ services:
|
||||
# Some variables still uses Calcom previous name, Calendso
|
||||
#
|
||||
# Full list https://github.com/calcom/cal.com/blob/main/.env.example
|
||||
- SERVICE_FQDN_CALCOM_3000
|
||||
- SERVICE_URL_CALCOM_3000
|
||||
- NEXT_PUBLIC_LICENSE_CONSENT=agree
|
||||
- NODE_ENV=production
|
||||
- NEXT_PUBLIC_WEBAPP_URL=${SERVICE_FQDN_CALCOM}
|
||||
- NEXT_PUBLIC_API_V2_URL=${SERVICE_FQDN_CALCOM}/api/v2
|
||||
- NEXT_PUBLIC_WEBAPP_URL=${SERVICE_URL_CALCOM}
|
||||
- NEXT_PUBLIC_API_V2_URL=${SERVICE_URL_CALCOM}/api/v2
|
||||
# https://next-auth.js.org/configuration/options#nextauth_url
|
||||
# From https://github.com/calcom/docker?tab=readme-ov-file#important-run-time-variables, it should be ${NEXT_PUBLIC_WEBAPP_URL}/api/auth
|
||||
- NEXTAUTH_URL=${SERVICE_FQDN_CALCOM}/api/auth
|
||||
- NEXTAUTH_URL=${SERVICE_URL_CALCOM}/api/auth
|
||||
# It is highly recommended that the NEXTAUTH_SECRET must be overridden and very unique
|
||||
# Use `openssl rand -base64 32` to generate a key
|
||||
- NEXTAUTH_SECRET=${SERVICE_BASE64_CALCOMSECRET}
|
||||
@@ -47,7 +47,7 @@ services:
|
||||
- EMAIL_SERVER_PASSWORD=${EMAIL_SERVER_PASSWORD}
|
||||
- NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||
# More info on ALLOWED_HOSTNAMES https://github.com/calcom/cal.com/issues/12201
|
||||
- ALLOWED_HOSTNAMES=["${SERVICE_FQDN_CALCOM}"]
|
||||
- ALLOWED_HOSTNAMES=["${SERVICE_URL_CALCOM}"]
|
||||
depends_on:
|
||||
- postgresql
|
||||
postgresql:
|
||||
|
@@ -8,7 +8,7 @@ services:
|
||||
calibre-web:
|
||||
image: lscr.io/linuxserver/calibre-web:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_CALIBRE_8083
|
||||
- SERVICE_URL_CALIBRE_8083
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${TZ:-Etc/UTC}
|
||||
|
@@ -10,12 +10,12 @@ services:
|
||||
volumes:
|
||||
- castopod-media:/var/www/castopod/public/media
|
||||
environment:
|
||||
- SERVICE_FQDN_CASTOPOD_8000
|
||||
- SERVICE_URL_CASTOPOD_8000
|
||||
- MYSQL_DATABASE=castopod
|
||||
- MYSQL_USER=$SERVICE_USER_MYSQL
|
||||
- MYSQL_PASSWORD=$SERVICE_PASSWORD_MYSQL
|
||||
- CP_DISABLE_HTTPS=${CP_DISABLE_HTTPS:-1}
|
||||
- CP_BASEURL=$SERVICE_FQDN_CASTOPOD
|
||||
- CP_BASEURL=$SERVICE_URL_CASTOPOD
|
||||
- CP_ANALYTICS_SALT=$SERVICE_REALBASE64_64_SALT
|
||||
- CP_CACHE_HANDLER=redis
|
||||
- CP_REDIS_HOST=redis
|
||||
|
@@ -10,8 +10,8 @@ services:
|
||||
volumes:
|
||||
- changedetection-data:/datastore
|
||||
environment:
|
||||
- SERVICE_FQDN_CHANGEDETECTION_5000
|
||||
- BASE_URL=${SERVICE_FQDN_CHANGEDETECTION}
|
||||
- SERVICE_URL_CHANGEDETECTION_5000
|
||||
- BASE_URL=${SERVICE_URL_CHANGEDETECTION}
|
||||
- PUID=${PUID:-1000}
|
||||
- PGID=${PGID:-1000}
|
||||
- PLAYWRIGHT_DRIVER_URL=${PLAYWRIGHT_DRIVER_URL:-ws://browser-sockpuppet-chrome:3000}
|
||||
|
@@ -8,13 +8,13 @@ services:
|
||||
chaskiq:
|
||||
image: chaskiq/chaskiq:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_CHASKIQ_3000
|
||||
- SERVICE_URL_CHASKIQ_3000
|
||||
- REDIS_URL=redis://redis:6379/
|
||||
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/${POSTGRES_DB:-chaskiq}
|
||||
- POSTGRES_USER=$SERVICE_USER_POSTGRES
|
||||
- SERVICE_URL=${SERVICE_URL_CHASKIQ}
|
||||
- HOST=${SERVICE_FQDN_CHASKIQ_3000}
|
||||
- ASSET_HOST=${SERVICE_FQDN_CHASKIQ_3000}
|
||||
- HOST=${SERVICE_URL_CHASKIQ_3000}
|
||||
- ASSET_HOST=${SERVICE_URL_CHASKIQ_3000}
|
||||
- WS=wss://${SERVICE_URL_CHASKIQ}/cable
|
||||
- SNS_CONFIGURATION_SET=metrics
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
|
||||
@@ -80,8 +80,8 @@ services:
|
||||
- REDIS_URL=redis://redis:6379/
|
||||
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/${POSTGRES_DB:-chaskiq}
|
||||
- POSTGRES_USER=$SERVICE_USER_POSTGRES
|
||||
- HOST=${SERVICE_FQDN_CHASKIQ_3000}
|
||||
- ASSET_HOST=${SERVICE_FQDN_CHASKIQ_3000}
|
||||
- HOST=${SERVICE_URL_CHASKIQ_3000}
|
||||
- ASSET_HOST=${SERVICE_URL_CHASKIQ_3000}
|
||||
- WS=wss://${SERVICE_URL_CHASKIQ}/cable
|
||||
- SNS_CONFIGURATION_SET=metrics
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
|
||||
|
@@ -11,9 +11,9 @@ services:
|
||||
- postgres
|
||||
- redis
|
||||
environment:
|
||||
- SERVICE_FQDN_CHATWOOT_3000
|
||||
- SERVICE_URL_CHATWOOT_3000
|
||||
- SECRET_KEY_BASE=$SERVICE_PASSWORD_CHATWOOT
|
||||
- FRONTEND_URL=${SERVICE_FQDN_CHATWOOT}
|
||||
- FRONTEND_URL=${SERVICE_URL_CHATWOOT}
|
||||
- DEFAULT_LOCALE=${CHATWOOT_DEFAULT_LOCALE}
|
||||
- FORCE_SSL=${FORCE_SSL:-false}
|
||||
- ENABLE_ACCOUNT_SIGNUP=${ENABLE_ACCOUNT_SIGNUP:-false}
|
||||
@@ -54,7 +54,7 @@ services:
|
||||
- redis
|
||||
environment:
|
||||
- SECRET_KEY_BASE=$SERVICE_PASSWORD_CHATWOOT
|
||||
- FRONTEND_URL=${SERVICE_FQDN_CHATWOOT}
|
||||
- FRONTEND_URL=${SERVICE_URL_CHATWOOT}
|
||||
- DEFAULT_LOCALE=${CHATWOOT_DEFAULT_LOCALE}
|
||||
- FORCE_SSL=${FORCE_SSL:-false}
|
||||
- ENABLE_ACCOUNT_SIGNUP=${ENABLE_ACCOUNT_SIGNUP:-false}
|
||||
|
@@ -8,14 +8,14 @@ services:
|
||||
client:
|
||||
image: bluewaveuptime/uptime_client:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_CHECKMATE_80
|
||||
- UPTIME_APP_API_BASE_URL=${SERVICE_FQDN_CHECKMATESERVER_5000}/api/v1
|
||||
- SERVICE_URL_CHECKMATE_80
|
||||
- UPTIME_APP_API_BASE_URL=${SERVICE_URL_CHECKMATESERVER_5000}/api/v1
|
||||
depends_on:
|
||||
- server
|
||||
server:
|
||||
image: bluewaveuptime/uptime_server:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_CHECKMATESERVER_5000
|
||||
- SERVICE_URL_CHECKMATESERVER_5000
|
||||
- JWT_SECRET=${SERVICE_PASSWORD_64_JWT}
|
||||
- REFRESH_TOKEN_SECRET=${SERVICE_PASSWORD_64_REFRESH}
|
||||
- SYSTEM_EMAIL_ADDRESS=${SYSTEM_EMAIL_ADDRESS:-test@example.com}
|
||||
|
@@ -10,7 +10,7 @@ services:
|
||||
volumes:
|
||||
- chroma-data:/data
|
||||
environment:
|
||||
- SERVICE_FQDN_CHROMA_8000
|
||||
- SERVICE_URL_CHROMA_8000
|
||||
- IS_PERSISTENT=TRUE
|
||||
- PERSIST_DIRECTORY=/data
|
||||
healthcheck:
|
||||
|
@@ -9,7 +9,7 @@ services:
|
||||
volumes:
|
||||
- classicpress-files:/var/www/html
|
||||
environment:
|
||||
- SERVICE_FQDN_CLASSICPRESS
|
||||
- SERVICE_URL_CLASSICPRESS
|
||||
- CLASSICPRESS_DB_HOST=mariadb
|
||||
- CLASSICPRESS_DB_USER=$SERVICE_USER_CLASSICPRESS
|
||||
- CLASSICPRESS_DB_PASSWORD=$SERVICE_PASSWORD_CLASSICPRESS
|
||||
|
@@ -9,7 +9,7 @@ services:
|
||||
volumes:
|
||||
- classicpress-files:/var/www/html
|
||||
environment:
|
||||
- SERVICE_FQDN_CLASSICPRESS
|
||||
- SERVICE_URL_CLASSICPRESS
|
||||
- CLASSICPRESS_DB_HOST=mysql
|
||||
- CLASSICPRESS_DB_USER=$SERVICE_USER_CLASSICPRESS
|
||||
- CLASSICPRESS_DB_PASSWORD=$SERVICE_PASSWORD_CLASSICPRESS
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user