From c3f485e0d6196d46f4a6c7f424ceb3e2e07a403f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:00:04 +0200 Subject: [PATCH] feat(services): add Jean Server one-click template and extra fields Register the jean-server compose template, logo, and catalog entries, and expose token and allowed-origins settings on the service form. Sort extra fields by sortOrder and omit empty service-name prefixes. --- app/Livewire/Project/Service/StackForm.php | 4 +- app/Models/Service.php | 21 ++++++++++ .../project/service/stack-form.blade.php | 7 +++- svgs/jean.png | Bin 0 -> 10770 bytes templates/compose/jean.yaml | 31 ++++++++++++++ templates/service-templates-latest.json | 19 +++++++++ templates/service-templates.json | 19 +++++++++ tests/Feature/ServiceExtraFieldsTest.php | 27 ++++++++++++ tests/Unit/JeanServiceTemplateTest.php | 39 ++++++++++++++++++ 9 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 svgs/jean.png create mode 100644 templates/compose/jean.yaml create mode 100644 tests/Unit/JeanServiceTemplateTest.php diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 92829edd6..de79fcd24 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -101,6 +101,7 @@ class StackForm extends Component $rules = data_get($field, 'rules', 'nullable'); $isPassword = data_get($field, 'isPassword', false); $customHelper = data_get($field, 'customHelper', false); + $sortOrder = data_get($field, 'sortOrder'); $this->fields->put($key, [ 'serviceName' => $serviceName, 'key' => $key, @@ -109,6 +110,7 @@ class StackForm extends Component 'isPassword' => $isPassword, 'rules' => $rules, 'customHelper' => $customHelper, + 'sortOrder' => $sortOrder, ]); $this->validationAttributes["fields.$key.value"] = $fieldKey; @@ -116,7 +118,7 @@ class StackForm extends Component } $this->fields = $this->fields->groupBy('serviceName')->map(function ($group) { return $group->sortBy(function ($field) { - return data_get($field, 'isPassword') ? 1 : 0; + return data_get($field, 'sortOrder') ?? (data_get($field, 'isPassword') ? 1 : 0); })->mapWithKeys(function ($field) { return [$field['key'] => $field]; }); diff --git a/app/Models/Service.php b/app/Models/Service.php index 224507c4e..0da97b301 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -1180,6 +1180,27 @@ class Service extends BaseModel } $fields->put('Openclaw', $data->toArray()); break; + case $image->contains('coollabsio/jean-server'): + $data = collect([]); + $settings = [ + 'Token' => ['key' => 'SERVICE_PASSWORD_64_JEAN', 'rules' => 'required', 'isPassword' => true, 'sortOrder' => 1, 'customHelper' => 'Token required to access Jean Server. Variable name: SERVICE_PASSWORD_64_JEAN'], + 'Allowed Origins' => ['key' => 'JEAN_ALLOWED_ORIGINS', 'rules' => 'nullable|string', 'sortOrder' => 2, 'customHelper' => 'Comma-separated additional browser origins. Same-origin access is always allowed. Variable name: JEAN_ALLOWED_ORIGINS'], + ]; + + foreach ($settings as $label => $setting) { + $variable = $this->environment_variables()->where('key', $setting['key'])->first(); + if (! $variable) { + continue; + } + + $data->put($label, [ + ...$setting, + 'value' => data_get($variable, 'value'), + ]); + } + + $fields->put('', $data->toArray()); + break; default: $data = collect([]); $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php index 89dfa0e5c..d6c55ecfe 100644 --- a/resources/views/livewire/project/service/stack-form.blade.php +++ b/resources/views/livewire/project/service/stack-form.blade.php @@ -66,7 +66,12 @@ @foreach ($fields as $serviceName => $field)
- {{ data_get($field, 'serviceName') }} · {{ data_get($field, 'name') }} + + @if (filled(data_get($field, 'serviceName'))) + {{ data_get($field, 'serviceName') }} · + @endif + {{ data_get($field, 'name') }} + @if (data_get($field, 'customHelper')) @else diff --git a/svgs/jean.png b/svgs/jean.png new file mode 100644 index 0000000000000000000000000000000000000000..b5f15fdc28e7f16cf80cf0004d795480340f8986 GIT binary patch literal 10770 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 Zwfyh!ktWp-B0Qly-TbB$xGFbN{|6s)>6`!n literal 0 HcmV?d00001 diff --git a/templates/compose/jean.yaml b/templates/compose/jean.yaml new file mode 100644 index 000000000..4b24ee02d --- /dev/null +++ b/templates/compose/jean.yaml @@ -0,0 +1,31 @@ +# documentation: https://github.com/coollabsio/jean/blob/main/docs/headless-server.md +# slogan: Open-source desktop and server client for orchestrating AI coding agents. +# category: development +# tags: jean,ai,coding,agents,development,git,worktrees +# logo: svgs/jean.png +# port: 3456 + +services: + jean: + image: 'ghcr.io/coollabsio/jean-server:${JEAN_VERSION:-latest}' + environment: + - SERVICE_URL_JEAN_3456 + - JEAN_HEADLESS=${JEAN_HEADLESS:-1} + - JEAN_HOST=${JEAN_HOST:-0.0.0.0} + - JEAN_PORT=${JEAN_PORT:-3456} + - JEAN_TOKEN=${SERVICE_PASSWORD_64_JEAN} + - JEAN_NO_TOKEN=${JEAN_NO_TOKEN:-0} + - JEAN_ALLOW_UNSAFE_NO_TOKEN=${JEAN_ALLOW_UNSAFE_NO_TOKEN:-0} + - JEAN_ALLOW_NATIVE_OPEN=${JEAN_ALLOW_NATIVE_OPEN:-0} + - JEAN_ALLOWED_ORIGINS=${JEAN_ALLOWED_ORIGINS:-} + - JEAN_DATA_DIR=/home/jean/.local/share/com.jean.desktop + volumes: + - jean-data:/home/jean/.local/share/com.jean.desktop + healthcheck: + test: + - CMD-SHELL + - 'curl -fsS http://127.0.0.1:3456/readyz >/dev/null || exit 1' + interval: 10s + timeout: 5s + retries: 12 + start_period: 15s diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index cda94584b..f90fa3790 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -2453,6 +2453,25 @@ "template_last_updated_at": "2025-08-17T18:23:57+02:00", "port": "80" }, + "jean": { + "documentation": "https://github.com/coollabsio/jean/blob/main/docs/headless-server.md?utm_source=coolify.io", + "slogan": "Open-source desktop and server client for orchestrating AI coding agents.", + "compose": "c2VydmljZXM6CiAgamVhbjoKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL2plYW4tc2VydmVyOiR7SkVBTl9WRVJTSU9OOi1sYXRlc3R9JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfSkVBTl8zNDU2CiAgICAgIC0gJ0pFQU5fSEVBRExFU1M9JHtKRUFOX0hFQURMRVNTOi0xfScKICAgICAgLSAnSkVBTl9IT1NUPSR7SkVBTl9IT1NUOi0wLjAuMC4wfScKICAgICAgLSAnSkVBTl9QT1JUPSR7SkVBTl9QT1JUOi0zNDU2fScKICAgICAgLSAnSkVBTl9UT0tFTj0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfSkVBTn0nCiAgICAgIC0gJ0pFQU5fTk9fVE9LRU49JHtKRUFOX05PX1RPS0VOOi0wfScKICAgICAgLSAnSkVBTl9BTExPV19VTlNBRkVfTk9fVE9LRU49JHtKRUFOX0FMTE9XX1VOU0FGRV9OT19UT0tFTjotMH0nCiAgICAgIC0gJ0pFQU5fQUxMT1dfTkFUSVZFX09QRU49JHtKRUFOX0FMTE9XX05BVElWRV9PUEVOOi0wfScKICAgICAgLSAnSkVBTl9BTExPV0VEX09SSUdJTlM9JHtKRUFOX0FMTE9XRURfT1JJR0lOUzotfScKICAgICAgLSBKRUFOX0RBVEFfRElSPS9ob21lL2plYW4vLmxvY2FsL3NoYXJlL2NvbS5qZWFuLmRlc2t0b3AKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2plYW4tZGF0YTovaG9tZS9qZWFuLy5sb2NhbC9zaGFyZS9jb20uamVhbi5kZXNrdG9wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdjdXJsIC1mc1MgaHR0cDovLzEyNy4wLjAuMTozNDU2L3JlYWR5eiA+L2Rldi9udWxsIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMgogICAgICBzdGFydF9wZXJpb2Q6IDE1cwo=", + "tags": [ + "jean", + "ai", + "coding", + "agents", + "development", + "git", + "worktrees" + ], + "category": "development", + "logo": "svgs/jean.png", + "minversion": "0.0.0", + "template_last_updated_at": null, + "port": "3456" + }, "jellyfin": { "documentation": "https://jellyfin.org?utm_source=coolify.io", "slogan": "Jellyfin is a media server for hosting and streaming your media collection.", diff --git a/templates/service-templates.json b/templates/service-templates.json index 50f647315..5a4ad35be 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -2453,6 +2453,25 @@ "template_last_updated_at": "2025-08-17T18:23:57+02:00", "port": "80" }, + "jean": { + "documentation": "https://github.com/coollabsio/jean/blob/main/docs/headless-server.md?utm_source=coolify.io", + "slogan": "Open-source desktop and server client for orchestrating AI coding agents.", + "compose": "c2VydmljZXM6CiAgamVhbjoKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL2plYW4tc2VydmVyOiR7SkVBTl9WRVJTSU9OOi1sYXRlc3R9JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0pFQU5fMzQ1NgogICAgICAtICdKRUFOX0hFQURMRVNTPSR7SkVBTl9IRUFETEVTUzotMX0nCiAgICAgIC0gJ0pFQU5fSE9TVD0ke0pFQU5fSE9TVDotMC4wLjAuMH0nCiAgICAgIC0gJ0pFQU5fUE9SVD0ke0pFQU5fUE9SVDotMzQ1Nn0nCiAgICAgIC0gJ0pFQU5fVE9LRU49JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0pFQU59JwogICAgICAtICdKRUFOX05PX1RPS0VOPSR7SkVBTl9OT19UT0tFTjotMH0nCiAgICAgIC0gJ0pFQU5fQUxMT1dfVU5TQUZFX05PX1RPS0VOPSR7SkVBTl9BTExPV19VTlNBRkVfTk9fVE9LRU46LTB9JwogICAgICAtICdKRUFOX0FMTE9XX05BVElWRV9PUEVOPSR7SkVBTl9BTExPV19OQVRJVkVfT1BFTjotMH0nCiAgICAgIC0gJ0pFQU5fQUxMT1dFRF9PUklHSU5TPSR7SkVBTl9BTExPV0VEX09SSUdJTlM6LX0nCiAgICAgIC0gSkVBTl9EQVRBX0RJUj0vaG9tZS9qZWFuLy5sb2NhbC9zaGFyZS9jb20uamVhbi5kZXNrdG9wCiAgICB2b2x1bWVzOgogICAgICAtICdqZWFuLWRhdGE6L2hvbWUvamVhbi8ubG9jYWwvc2hhcmUvY29tLmplYW4uZGVza3RvcCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtZnNTIGh0dHA6Ly8xMjcuMC4wLjE6MzQ1Ni9yZWFkeXogPi9kZXYvbnVsbCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTIKICAgICAgc3RhcnRfcGVyaW9kOiAxNXMK", + "tags": [ + "jean", + "ai", + "coding", + "agents", + "development", + "git", + "worktrees" + ], + "category": "development", + "logo": "svgs/jean.png", + "minversion": "0.0.0", + "template_last_updated_at": null, + "port": "3456" + }, "jellyfin": { "documentation": "https://jellyfin.org?utm_source=coolify.io", "slogan": "Jellyfin is a media server for hosting and streaming your media collection.", diff --git a/tests/Feature/ServiceExtraFieldsTest.php b/tests/Feature/ServiceExtraFieldsTest.php index 2b6ed0c0b..2a521f97e 100644 --- a/tests/Feature/ServiceExtraFieldsTest.php +++ b/tests/Feature/ServiceExtraFieldsTest.php @@ -45,3 +45,30 @@ it('only adds Grafana extra fields for Grafana server images', function (string 'promtail' => ['grafana/promtail:latest', false], 'tempo' => ['grafana/tempo:latest', false], ]); + +it('exposes Jean Server authentication and access settings', function () { + $service = serviceExtraFieldsTestServiceWithApplicationImage('ghcr.io/coollabsio/jean-server:latest'); + + $service->environment_variables()->createMany([ + ['key' => 'SERVICE_PASSWORD_64_JEAN', 'value' => 'secret-token', 'is_preview' => false], + ['key' => 'JEAN_ALLOWED_ORIGINS', 'value' => 'https://jean.example.com', 'is_preview' => false], + ]); + + $fields = $service->extraFields(); + + expect($fields)->toHaveKey('') + ->and($fields[''])->toMatchArray([ + 'Token' => [ + 'key' => 'SERVICE_PASSWORD_64_JEAN', + 'value' => 'secret-token', + 'rules' => 'required', + 'isPassword' => true, + 'sortOrder' => 1, + ], + 'Allowed Origins' => [ + 'key' => 'JEAN_ALLOWED_ORIGINS', + 'value' => 'https://jean.example.com', + 'sortOrder' => 2, + ], + ]); +}); diff --git a/tests/Unit/JeanServiceTemplateTest.php b/tests/Unit/JeanServiceTemplateTest.php new file mode 100644 index 000000000..32de62ed3 --- /dev/null +++ b/tests/Unit/JeanServiceTemplateTest.php @@ -0,0 +1,39 @@ +toContain('ghcr.io/coollabsio/jean-server:${JEAN_VERSION:-latest}') + ->toContain('SERVICE_URL_JEAN_3456') + ->toContain('JEAN_HEADLESS=${JEAN_HEADLESS:-1}') + ->toContain('JEAN_HOST=${JEAN_HOST:-0.0.0.0}') + ->toContain('JEAN_PORT=${JEAN_PORT:-3456}') + ->toContain('JEAN_TOKEN=${SERVICE_PASSWORD_64_JEAN}') + ->toContain('JEAN_NO_TOKEN=${JEAN_NO_TOKEN:-0}') + ->toContain('JEAN_ALLOW_UNSAFE_NO_TOKEN=${JEAN_ALLOW_UNSAFE_NO_TOKEN:-0}') + ->toContain('JEAN_ALLOW_NATIVE_OPEN=${JEAN_ALLOW_NATIVE_OPEN:-0}') + ->toContain('JEAN_ALLOWED_ORIGINS=${JEAN_ALLOWED_ORIGINS:-}') + ->toContain('JEAN_DATA_DIR=/home/jean/.local/share/com.jean.desktop') + ->toContain('jean-data:/home/jean/.local/share/com.jean.desktop') + ->toContain('http://127.0.0.1:3456/readyz'); + + foreach (['service-templates.json', 'service-templates-latest.json'] as $templateFile) { + $templates = json_decode( + file_get_contents(__DIR__."/../../templates/{$templateFile}"), + associative: true, + flags: JSON_THROW_ON_ERROR, + ); + + expect($templates)->toHaveKey('jean'); + expect($templates['jean']['port'] ?? null)->toBe('3456'); + expect($templates['jean']['logo'] ?? null)->toBe('svgs/jean.png'); + expect($templates['jean']['category'] ?? null)->toBe('development'); + + $generatedCompose = base64_decode($templates['jean']['compose'], strict: true); + + expect($generatedCompose) + ->toContain('ghcr.io/coollabsio/jean-server:${JEAN_VERSION:-latest}') + ->toContain('JEAN_TOKEN=${SERVICE_PASSWORD_64_JEAN}'); + } +});