From 12b14665b5a395edb5d79bdc518ab9d59ae2d23f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:31:57 +0200 Subject: [PATCH] fix(ui): use 100dvh body height and coollabs auth glow Replace min-h-screen with min-height: 100dvh on body for correct mobile viewport height, and point the auth shell radial glow at --color-coollabs instead of --color-accent. Add branding CSS tests. --- openapi.json | 577 ++++++++++++++++++++++++ openapi.yaml | 380 ++++++++++++++++ resources/css/app.css | 5 +- templates/service-templates-latest.json | 2 +- templates/service-templates.json | 2 +- tests/Feature/LoginPageBrandingTest.php | 16 + 6 files changed, 978 insertions(+), 4 deletions(-) diff --git a/openapi.json b/openapi.json index 372e0000c..bab99cb18 100644 --- a/openapi.json +++ b/openapi.json @@ -4132,6 +4132,73 @@ ] } }, + "\/applications\/{uuid}\/migrate": { + "post": { + "tags": [ + "Applications" + ], + "summary": "Migrate to Server", + "description": "Migrate an application to another destination\/server owned by the authenticated team. Stops the application, optionally transfers persistent volume data when both servers are managed by Coolify, and updates database records. Redeploy after migration completes.", + "operationId": "migrate-application-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "destination_uuid" + ], + "properties": { + "destination_uuid": { + "description": "UUID of the target destination.", + "type": "string" + }, + "migrate_volumes": { + "description": "Whether to transfer persistent volume data when migrating across servers.", + "type": "boolean", + "default": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Application migration started or completed." + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "404": { + "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/applications\/{uuid}\/storages": { "get": { "tags": [ @@ -7940,6 +8007,73 @@ ] } }, + "\/databases\/{uuid}\/migrate": { + "post": { + "tags": [ + "Databases" + ], + "summary": "Migrate to Server", + "description": "Migrate a database to another destination\/server owned by the authenticated team. Stops the database, optionally transfers persistent volume data when both servers are managed by Coolify, and updates database records. Redeploy after migration completes.", + "operationId": "migrate-database-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "destination_uuid" + ], + "properties": { + "destination_uuid": { + "description": "UUID of the target destination.", + "type": "string" + }, + "migrate_volumes": { + "description": "Whether to transfer persistent volume data when migrating across servers.", + "type": "boolean", + "default": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database migration started or completed." + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "404": { + "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/databases\/{uuid}\/start": { "post": { "tags": [ @@ -15494,6 +15628,382 @@ ] } }, + "\/servers\/{uuid}\/migrate": { + "post": { + "tags": [ + "Servers" + ], + "summary": "Migrate server to another Coolify instance", + "description": "One-shot handoff: export this server, import+claim on the target instance (using the provided token), then disable automations here. Requires read:sensitive and write.", + "operationId": "migrate-server-between-instances", + "parameters": [ + { + "name": "uuid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "target_url", + "target_token" + ], + "properties": { + "target_url": { + "type": "string", + "example": "https:\/\/coolify-b.example.com" + }, + "target_token": { + "description": "API token on the target instance (root or write)", + "type": "string" + }, + "write_remote": { + "type": "boolean", + "default": false + }, + "rebind_sentinel": { + "type": "boolean", + "default": true + }, + "preserve_uuids": { + "type": "boolean", + "default": true + }, + "adopt_mode": { + "type": "boolean", + "default": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Migrated" + }, + "403": { + "description": "Missing sensitive permission" + }, + "404": { + "$ref": "#\/components\/responses\/404" + }, + "422": { + "description": "Validation or remote import failed" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/servers\/{uuid}\/export": { + "get": { + "tags": [ + "Servers" + ], + "summary": "Export server transfer bundle", + "description": "Export a server and all resources hosted on it as a versioned transfer bundle for moving between Coolify instances. Requires read:sensitive.", + "operationId": "export-server-transfer-bundle", + "parameters": [ + { + "name": "uuid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "encrypt", + "in": "query", + "description": "If true and passphrase is provided, return an encrypted envelope.", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "passphrase", + "in": "query", + "description": "Passphrase used when encrypt=true.", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Transfer bundle" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "403": { + "description": "Missing sensitive permission" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/servers\/import": { + "post": { + "tags": [ + "Servers" + ], + "summary": "Import server transfer bundle", + "description": "Import a server transfer bundle into this Coolify instance (adopt mode by default).", + "operationId": "import-server-transfer-bundle", + "requestBody": { + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "bundle": { + "description": "Plain or encrypted transfer bundle", + "type": "object" + }, + "passphrase": { + "type": [ + "string", + "null" + ] + }, + "dry_run": { + "type": "boolean", + "default": false + }, + "preserve_uuids": { + "type": "boolean", + "default": true + }, + "adopt_mode": { + "description": "Import without forcing redeploy; keep statuses for adoption", + "type": "boolean", + "default": true + }, + "claim": { + "description": "Automatically claim the host for this instance after import", + "type": "boolean", + "default": true + }, + "write_remote": { + "description": "When claiming, write ownership file on the host via SSH", + "type": "boolean", + "default": false + }, + "rebind_sentinel": { + "description": "When claiming, rebind Sentinel to this instance", + "type": "boolean", + "default": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Dry-run result" + }, + "201": { + "description": "Imported" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "422": { + "description": "Validation failed" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/servers\/{uuid}\/claim": { + "post": { + "tags": [ + "Servers" + ], + "summary": "Claim imported server", + "description": "Claim a managed host for this instance: write ownership file and rebind Sentinel.", + "operationId": "claim-server", + "parameters": [ + { + "name": "uuid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "properties": { + "write_remote": { + "type": "boolean", + "default": true + }, + "rebind_sentinel": { + "type": "boolean", + "default": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Claim result" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/servers\/{uuid}\/transfer\/complete": { + "post": { + "tags": [ + "Servers" + ], + "summary": "Mark server transferred", + "description": "Source-instance step: disable automations after a successful export\/import handoff.", + "operationId": "complete-server-transfer", + "parameters": [ + { + "name": "uuid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "properties": { + "export_id": { + "type": [ + "string", + "null" + ] + }, + "target_instance_url": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Marked transferred" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/servers\/{uuid}\/export\/mailbox": { + "post": { + "tags": [ + "Servers" + ], + "summary": "Write transfer bundle to server mailbox", + "description": "Write an export bundle to \/data\/coolify\/exports on the managed host for air-gapped import.", + "operationId": "export-server-transfer-mailbox", + "parameters": [ + { + "name": "uuid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "properties": { + "passphrase": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Mailbox write result" + }, + "403": { + "description": "Missing sensitive permission" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/servers": { "get": { "tags": [ @@ -18233,6 +18743,73 @@ ] } }, + "\/services\/{uuid}\/migrate": { + "post": { + "tags": [ + "Services" + ], + "summary": "Migrate to Server", + "description": "Migrate a service to another destination\/server owned by the authenticated team. Stops the service, optionally transfers persistent volume data when both servers are managed by Coolify, and updates database records. Redeploy after migration completes.", + "operationId": "migrate-service-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the service.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "destination_uuid" + ], + "properties": { + "destination_uuid": { + "description": "UUID of the target destination.", + "type": "string" + }, + "migrate_volumes": { + "description": "Whether to transfer persistent volume data when migrating across servers.", + "type": "boolean", + "default": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Service migration started or completed." + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "404": { + "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/services\/{uuid}\/start": { "post": { "tags": [ diff --git a/openapi.yaml b/openapi.yaml index 4ff4b12a8..ce0a2c3c6 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2692,6 +2692,51 @@ paths: security: - bearerAuth: [] + '/applications/{uuid}/migrate': + post: + tags: + - Applications + summary: 'Migrate to Server' + description: 'Migrate an application to another destination/server owned by the authenticated team. Stops the application, optionally transfers persistent volume data when both servers are managed by Coolify, and updates database records. Redeploy after migration completes.' + operationId: migrate-application-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + required: + - destination_uuid + properties: + destination_uuid: + description: 'UUID of the target destination.' + type: string + migrate_volumes: + description: 'Whether to transfer persistent volume data when migrating across servers.' + type: boolean + default: true + type: object + responses: + '200': + description: 'Application migration started or completed.' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '404': + $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' + security: + - + bearerAuth: [] '/applications/{uuid}/storages': get: tags: @@ -5222,6 +5267,51 @@ paths: security: - bearerAuth: [] + '/databases/{uuid}/migrate': + post: + tags: + - Databases + summary: 'Migrate to Server' + description: 'Migrate a database to another destination/server owned by the authenticated team. Stops the database, optionally transfers persistent volume data when both servers are managed by Coolify, and updates database records. Redeploy after migration completes.' + operationId: migrate-database-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the database.' + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + required: + - destination_uuid + properties: + destination_uuid: + description: 'UUID of the target destination.' + type: string + migrate_volumes: + description: 'Whether to transfer persistent volume data when migrating across servers.' + type: boolean + default: true + type: object + responses: + '200': + description: 'Database migration started or completed.' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '404': + $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' + security: + - + bearerAuth: [] '/databases/{uuid}/start': post: tags: @@ -9822,6 +9912,251 @@ paths: security: - bearerAuth: [] + '/servers/{uuid}/migrate': + post: + tags: + - Servers + summary: 'Migrate server to another Coolify instance' + description: 'One-shot handoff: export this server, import+claim on the target instance (using the provided token), then disable automations here. Requires read:sensitive and write.' + operationId: migrate-server-between-instances + parameters: + - + name: uuid + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + required: + - target_url + - target_token + properties: + target_url: + type: string + example: 'https://coolify-b.example.com' + target_token: + description: 'API token on the target instance (root or write)' + type: string + write_remote: + type: boolean + default: false + rebind_sentinel: + type: boolean + default: true + preserve_uuids: + type: boolean + default: true + adopt_mode: + type: boolean + default: true + type: object + responses: + '200': + description: Migrated + '403': + description: 'Missing sensitive permission' + '404': + $ref: '#/components/responses/404' + '422': + description: 'Validation or remote import failed' + security: + - + bearerAuth: [] + '/servers/{uuid}/export': + get: + tags: + - Servers + summary: 'Export server transfer bundle' + description: 'Export a server and all resources hosted on it as a versioned transfer bundle for moving between Coolify instances. Requires read:sensitive.' + operationId: export-server-transfer-bundle + parameters: + - + name: uuid + in: path + required: true + schema: + type: string + - + name: encrypt + in: query + description: 'If true and passphrase is provided, return an encrypted envelope.' + required: false + schema: + type: boolean + - + name: passphrase + in: query + description: 'Passphrase used when encrypt=true.' + required: false + schema: + type: string + responses: + '200': + description: 'Transfer bundle' + '401': + $ref: '#/components/responses/401' + '403': + description: 'Missing sensitive permission' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + /servers/import: + post: + tags: + - Servers + summary: 'Import server transfer bundle' + description: 'Import a server transfer bundle into this Coolify instance (adopt mode by default).' + operationId: import-server-transfer-bundle + requestBody: + required: true + content: + application/json: + schema: + properties: + bundle: + description: 'Plain or encrypted transfer bundle' + type: object + passphrase: + type: [string, 'null'] + dry_run: + type: boolean + default: false + preserve_uuids: + type: boolean + default: true + adopt_mode: + description: 'Import without forcing redeploy; keep statuses for adoption' + type: boolean + default: true + claim: + description: 'Automatically claim the host for this instance after import' + type: boolean + default: true + write_remote: + description: 'When claiming, write ownership file on the host via SSH' + type: boolean + default: false + rebind_sentinel: + description: 'When claiming, rebind Sentinel to this instance' + type: boolean + default: true + type: object + responses: + '200': + description: 'Dry-run result' + '201': + description: Imported + '401': + $ref: '#/components/responses/401' + '422': + description: 'Validation failed' + security: + - + bearerAuth: [] + '/servers/{uuid}/claim': + post: + tags: + - Servers + summary: 'Claim imported server' + description: 'Claim a managed host for this instance: write ownership file and rebind Sentinel.' + operationId: claim-server + parameters: + - + name: uuid + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + write_remote: + type: boolean + default: true + rebind_sentinel: + type: boolean + default: true + type: object + responses: + '200': + description: 'Claim result' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/servers/{uuid}/transfer/complete': + post: + tags: + - Servers + summary: 'Mark server transferred' + description: 'Source-instance step: disable automations after a successful export/import handoff.' + operationId: complete-server-transfer + parameters: + - + name: uuid + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + export_id: + type: [string, 'null'] + target_instance_url: + type: [string, 'null'] + type: object + responses: + '200': + description: 'Marked transferred' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/servers/{uuid}/export/mailbox': + post: + tags: + - Servers + summary: 'Write transfer bundle to server mailbox' + description: 'Write an export bundle to /data/coolify/exports on the managed host for air-gapped import.' + operationId: export-server-transfer-mailbox + parameters: + - + name: uuid + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + passphrase: + type: [string, 'null'] + type: object + responses: + '200': + description: 'Mailbox write result' + '403': + description: 'Missing sensitive permission' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] /servers: get: tags: @@ -11507,6 +11842,51 @@ paths: security: - bearerAuth: [] + '/services/{uuid}/migrate': + post: + tags: + - Services + summary: 'Migrate to Server' + description: 'Migrate a service to another destination/server owned by the authenticated team. Stops the service, optionally transfers persistent volume data when both servers are managed by Coolify, and updates database records. Redeploy after migration completes.' + operationId: migrate-service-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + required: + - destination_uuid + properties: + destination_uuid: + description: 'UUID of the target destination.' + type: string + migrate_volumes: + description: 'Whether to transfer persistent volume data when migrating across servers.' + type: boolean + default: true + type: object + responses: + '200': + description: 'Service migration started or completed.' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '404': + $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' + security: + - + bearerAuth: [] '/services/{uuid}/start': post: tags: diff --git a/resources/css/app.css b/resources/css/app.css index 6fb1b35cb..ccdde81b5 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -257,7 +257,8 @@ body { body { /* overflow-x: clip (not hidden) — hidden creates a scroll containment that breaks position:sticky on settings sidebars and other in-page pins. */ - @apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-clip; + min-height: 100dvh; + @apply text-sm font-sans antialiased scrollbar overflow-x-clip; } .coolify-monaco-editor { @@ -981,7 +982,7 @@ html[data-theme="custom"] textarea:disabled { min-height: 100dvh; display: flex; background: - radial-gradient(circle at 50% 0%, color-mix(in oklab, var(--color-accent) 9%, transparent), transparent 34rem), + radial-gradient(circle at 50% 0%, color-mix(in oklab, var(--color-coollabs) 9%, transparent), transparent 34rem), var(--coollabs-canvas); color: #171717; } diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 3ed26d4af..ca3012c64 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -552,7 +552,7 @@ "category": "backend", "logo": "svgs/denoKV.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-08-07T14:41:11+02:00", + "template_last_updated_at": "2026-08-07T18:17:33+02:00", "port": "8080" }, "changedetection": { diff --git a/templates/service-templates.json b/templates/service-templates.json index c1944c165..a59d62e47 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -552,7 +552,7 @@ "category": "backend", "logo": "svgs/denoKV.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-08-07T14:41:11+02:00", + "template_last_updated_at": "2026-08-07T18:17:33+02:00", "port": "8080" }, "changedetection": { diff --git a/tests/Feature/LoginPageBrandingTest.php b/tests/Feature/LoginPageBrandingTest.php index 88a951953..328d91199 100644 --- a/tests/Feature/LoginPageBrandingTest.php +++ b/tests/Feature/LoginPageBrandingTest.php @@ -21,6 +21,22 @@ test('login register and forgot password headers are Coolify', function () { ->not->toContain("title=\"{{ __('auth.forgot_password_heading') }}\""); }); +test('page body uses the dynamic viewport height on mobile', function () { + $styles = file_get_contents(resource_path('css/app.css')); + + expect($styles) + ->toMatch('/body\s*\{[^}]*min-height:\s*100dvh;/s') + ->not->toMatch('/body\s*\{[^}]*@apply[^;]*min-h-screen/s'); +}); + +test('auth pages use the Coollabs purple background glow', function () { + $styles = file_get_contents(resource_path('css/app.css')); + + expect($styles) + ->toMatch('/\.auth-shell\s*\{[^}]*color-mix\(in oklab, var\(--color-coollabs\) 9%, transparent\)/s') + ->not->toMatch('/\.auth-shell\s*\{[^}]*color-mix\(in oklab, var\(--color-accent\) 9%, transparent\)/s'); +}); + test('confirm password page uses the shared auth shell', function () { $confirm = file_get_contents(resource_path('views/auth/confirm-password.blade.php'));