Merge branch 'next' into fix/service-update-required-params
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,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -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([
|
||||
|
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 |
@@ -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');
|
||||
@@ -81,39 +82,7 @@
|
||||
<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
|
||||
|
@@ -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>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</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
|
||||
|
@@ -9,7 +9,7 @@ services:
|
||||
volumes:
|
||||
- classicpress-files:/var/www/html
|
||||
environment:
|
||||
- SERVICE_FQDN_CLASSICPRESS
|
||||
- SERVICE_URL_CLASSICPRESS
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1"]
|
||||
interval: 2s
|
||||
|
@@ -10,7 +10,7 @@ services:
|
||||
volumes:
|
||||
- cloudbeaver-data:/opt/cloudbeaver/workspace
|
||||
environment:
|
||||
- SERVICE_FQDN_CLOUDBEAVER_8978
|
||||
- SERVICE_URL_CLOUDBEAVER_8978
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8978/"]
|
||||
interval: 5s
|
||||
|
@@ -8,7 +8,7 @@ services:
|
||||
code-server:
|
||||
image: lscr.io/linuxserver/code-server:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_CODESERVER_8443
|
||||
- SERVICE_URL_CODESERVER_8443
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Madrid
|
||||
|
@@ -9,10 +9,10 @@ services:
|
||||
coder:
|
||||
image: ghcr.io/coder/coder:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_CODER_7080
|
||||
- SERVICE_URL_CODER_7080
|
||||
- CODER_PG_CONNECTION_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@coder-database/${POSTGRES_DB:-coder-db}?sslmode=disable
|
||||
- CODER_HTTP_ADDRESS=0.0.0.0:7080
|
||||
- CODER_ACCESS_URL=${SERVICE_FQDN_CODER}
|
||||
- CODER_ACCESS_URL=${SERVICE_URL_CODER}
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
depends_on:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user