mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-20 02:17:09 +00:00
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.
This commit is contained in:
1 parent
9794ce2686
commit
c3f485e0d6
9 files changed
+165
-2
No files matched your search
@@ -101,6 +101,7 @@ class StackForm extends Component
|
|||||||
$rules = data_get($field, 'rules', 'nullable');
|
$rules = data_get($field, 'rules', 'nullable');
|
||||||
$isPassword = data_get($field, 'isPassword', false);
|
$isPassword = data_get($field, 'isPassword', false);
|
||||||
$customHelper = data_get($field, 'customHelper', false);
|
$customHelper = data_get($field, 'customHelper', false);
|
||||||
|
$sortOrder = data_get($field, 'sortOrder');
|
||||||
$this->fields->put($key, [
|
$this->fields->put($key, [
|
||||||
'serviceName' => $serviceName,
|
'serviceName' => $serviceName,
|
||||||
'key' => $key,
|
'key' => $key,
|
||||||
@@ -109,6 +110,7 @@ class StackForm extends Component
|
|||||||
'isPassword' => $isPassword,
|
'isPassword' => $isPassword,
|
||||||
'rules' => $rules,
|
'rules' => $rules,
|
||||||
'customHelper' => $customHelper,
|
'customHelper' => $customHelper,
|
||||||
|
'sortOrder' => $sortOrder,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->validationAttributes["fields.$key.value"] = $fieldKey;
|
$this->validationAttributes["fields.$key.value"] = $fieldKey;
|
||||||
@@ -116,7 +118,7 @@ class StackForm extends Component
|
|||||||
}
|
}
|
||||||
$this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
|
$this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
|
||||||
return $group->sortBy(function ($field) {
|
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) {
|
})->mapWithKeys(function ($field) {
|
||||||
return [$field['key'] => $field];
|
return [$field['key'] => $field];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1180,6 +1180,27 @@ class Service extends BaseModel
|
|||||||
}
|
}
|
||||||
$fields->put('Openclaw', $data->toArray());
|
$fields->put('Openclaw', $data->toArray());
|
||||||
break;
|
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:
|
default:
|
||||||
$data = collect([]);
|
$data = collect([]);
|
||||||
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
|
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
|
||||||
|
|||||||
@@ -66,7 +66,12 @@
|
|||||||
@foreach ($fields as $serviceName => $field)
|
@foreach ($fields as $serviceName => $field)
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1.5 flex items-center gap-1.5 text-[12px] font-medium">
|
<div class="mb-1.5 flex items-center gap-1.5 text-[12px] font-medium">
|
||||||
<span>{{ data_get($field, 'serviceName') }} · {{ data_get($field, 'name') }}</span>
|
<span>
|
||||||
|
@if (filled(data_get($field, 'serviceName')))
|
||||||
|
{{ data_get($field, 'serviceName') }} ·
|
||||||
|
@endif
|
||||||
|
{{ data_get($field, 'name') }}
|
||||||
|
</span>
|
||||||
@if (data_get($field, 'customHelper'))
|
@if (data_get($field, 'customHelper'))
|
||||||
<x-helper helper="{{ data_get($field, 'customHelper') }}" />
|
<x-helper helper="{{ data_get($field, 'customHelper') }}" />
|
||||||
@else
|
@else
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -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
|
||||||
@@ -2453,6 +2453,25 @@
|
|||||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||||
"port": "80"
|
"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": {
|
"jellyfin": {
|
||||||
"documentation": "https://jellyfin.org?utm_source=coolify.io",
|
"documentation": "https://jellyfin.org?utm_source=coolify.io",
|
||||||
"slogan": "Jellyfin is a media server for hosting and streaming your media collection.",
|
"slogan": "Jellyfin is a media server for hosting and streaming your media collection.",
|
||||||
|
|||||||
@@ -2453,6 +2453,25 @@
|
|||||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||||
"port": "80"
|
"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": {
|
"jellyfin": {
|
||||||
"documentation": "https://jellyfin.org?utm_source=coolify.io",
|
"documentation": "https://jellyfin.org?utm_source=coolify.io",
|
||||||
"slogan": "Jellyfin is a media server for hosting and streaming your media collection.",
|
"slogan": "Jellyfin is a media server for hosting and streaming your media collection.",
|
||||||
|
|||||||
@@ -45,3 +45,30 @@ it('only adds Grafana extra fields for Grafana server images', function (string
|
|||||||
'promtail' => ['grafana/promtail:latest', false],
|
'promtail' => ['grafana/promtail:latest', false],
|
||||||
'tempo' => ['grafana/tempo: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,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('includes a Jean Server one-click service template with all deployment environment variables', function () {
|
||||||
|
$compose = file_get_contents(__DIR__.'/../../templates/compose/jean.yaml');
|
||||||
|
|
||||||
|
expect($compose)
|
||||||
|
->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}');
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user