+
+ Scheduled Job Issues | Coolify
+
+
+
+
+
+
Scheduled Job Issues
+ Refresh
+
+
Shows failed executions, skipped jobs, and scheduler health.
+
+
+ {{-- Tab Buttons --}}
+
+
+ Failures ({{ $executions->count() }})
+
+
+ Scheduler Runs ({{ $managerRuns->count() }})
+
+
+ Skipped Jobs ({{ $skipLogs->count() }})
+
+
+
+ {{-- Executions Tab --}}
+
+ {{-- Filters --}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Type |
+ Resource |
+ Server |
+ Started |
+ Duration |
+ Message |
+
+
+
+ @forelse($executions as $execution)
+
+ |
+ @php
+ $typeLabel = match($execution['type']) {
+ 'backup' => 'Backup',
+ 'task' => 'Task',
+ 'cleanup' => 'Cleanup',
+ default => ucfirst($execution['type']),
+ };
+ $typeBg = match($execution['type']) {
+ 'backup' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
+ 'task' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
+ 'cleanup' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
+ default => 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300',
+ };
+ @endphp
+
+ {{ $typeLabel }}
+
+ |
+
+ {{ $execution['resource_name'] }}
+ @if($execution['resource_type'])
+ ({{ $execution['resource_type'] }})
+ @endif
+ |
+ {{ $execution['server_name'] }} |
+
+ {{ $execution['created_at']->diffForHumans() }}
+ {{ $execution['created_at']->format('M d H:i') }}
+ |
+
+ @if($execution['finished_at'] && $execution['created_at'])
+ {{ \Carbon\Carbon::parse($execution['created_at'])->diffInSeconds(\Carbon\Carbon::parse($execution['finished_at'])) }}s
+ @elseif($execution['status'] === 'running')
+
+ @else
+ -
+ @endif
+ |
+
+ {{ \Illuminate\Support\Str::limit($execution['message'], 80) }}
+ |
+
+ @empty
+
+ |
+ No failures found for the selected filters.
+ |
+
+ @endforelse
+
+
+
+
+
+ {{-- Scheduler Runs Tab --}}
+
+
Shows when the ScheduledJobManager executed. Gaps indicate lock conflicts or missed runs.
+
+
+
+
+ | Time |
+ Event |
+ Duration |
+ Dispatched |
+ Skipped |
+
+
+
+ @forelse($managerRuns as $run)
+
+ | {{ $run['timestamp'] }} |
+ {{ $run['message'] }} |
+
+ @if($run['duration_ms'] !== null)
+ {{ $run['duration_ms'] }}ms
+ @else
+ -
+ @endif
+ |
+ {{ $run['dispatched'] ?? '-' }} |
+
+ @if(($run['skipped'] ?? 0) > 0)
+ {{ $run['skipped'] }}
+ @else
+ {{ $run['skipped'] ?? '-' }}
+ @endif
+ |
+
+ @empty
+
+ |
+ No scheduler run logs found. Logs appear after the ScheduledJobManager runs.
+ |
+
+ @endforelse
+
+
+
+
+
+ {{-- Skipped Jobs Tab --}}
+
+
Jobs that were not dispatched because conditions were not met.
+
+
+
+
+ | Time |
+ Type |
+ Reason |
+ Details |
+
+
+
+ @forelse($skipLogs as $skip)
+
+ | {{ $skip['timestamp'] }} |
+
+ @php
+ $skipTypeBg = match($skip['type']) {
+ 'backup' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
+ 'task' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
+ 'docker_cleanup' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
+ default => 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300',
+ };
+ @endphp
+
+ {{ ucfirst(str_replace('_', ' ', $skip['type'])) }}
+
+ |
+
+ @php
+ $reasonLabel = match($skip['reason']) {
+ 'server_not_functional' => 'Server not functional',
+ 'subscription_unpaid' => 'Subscription unpaid',
+ 'database_deleted' => 'Database deleted',
+ 'server_deleted' => 'Server deleted',
+ 'resource_deleted' => 'Resource deleted',
+ 'application_not_running' => 'Application not running',
+ 'service_not_running' => 'Service not running',
+ default => ucfirst(str_replace('_', ' ', $skip['reason'])),
+ };
+ $reasonBg = match($skip['reason']) {
+ 'server_not_functional', 'database_deleted', 'server_deleted', 'resource_deleted' => 'text-red-600 dark:text-red-400',
+ 'subscription_unpaid' => 'text-warning',
+ 'application_not_running', 'service_not_running' => 'text-orange-600 dark:text-orange-400',
+ default => '',
+ };
+ @endphp
+ {{ $reasonLabel }}
+ |
+
+ @php
+ $details = collect($skip['context'])
+ ->except(['type', 'skip_reason', 'execution_time'])
+ ->map(fn($v, $k) => str_replace('_', ' ', $k) . ': ' . $v)
+ ->implode(', ');
+ @endphp
+ {{ $details }}
+ |
+
+ @empty
+
+ |
+ No skipped jobs found. This means all scheduled jobs passed their conditions.
+ |
+
+ @endforelse
+
+
+
+
+
+
diff --git a/routes/api.php b/routes/api.php
index 8ff1fd1cc..c39f22c02 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -9,6 +9,7 @@ use App\Http\Controllers\Api\HetznerController;
use App\Http\Controllers\Api\OtherController;
use App\Http\Controllers\Api\ProjectController;
use App\Http\Controllers\Api\ResourcesController;
+use App\Http\Controllers\Api\ScheduledTasksController;
use App\Http\Controllers\Api\SecurityController;
use App\Http\Controllers\Api\ServersController;
use App\Http\Controllers\Api\ServicesController;
@@ -106,7 +107,7 @@ Route::group([
/**
* @deprecated Use POST /api/v1/services instead. This endpoint creates a Service, not an Application and is a unstable duplicate of POST /api/v1/services.
- */
+ */
Route::post('/applications/dockercompose', [ApplicationsController::class, 'create_dockercompose_application'])->middleware(['api.ability:write']);
Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid'])->middleware(['api.ability:read']);
@@ -171,6 +172,18 @@ Route::group([
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:write']);
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:write']);
Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware(['api.ability:write']);
+
+ Route::get('/applications/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'scheduled_tasks_by_application_uuid'])->middleware(['api.ability:read']);
+ Route::post('/applications/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'create_scheduled_task_by_application_uuid'])->middleware(['api.ability:write']);
+ Route::patch('/applications/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'update_scheduled_task_by_application_uuid'])->middleware(['api.ability:write']);
+ Route::delete('/applications/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'delete_scheduled_task_by_application_uuid'])->middleware(['api.ability:write']);
+ Route::get('/applications/{uuid}/scheduled-tasks/{task_uuid}/executions', [ScheduledTasksController::class, 'executions_by_application_uuid'])->middleware(['api.ability:read']);
+
+ Route::get('/services/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'scheduled_tasks_by_service_uuid'])->middleware(['api.ability:read']);
+ Route::post('/services/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'create_scheduled_task_by_service_uuid'])->middleware(['api.ability:write']);
+ Route::patch('/services/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'update_scheduled_task_by_service_uuid'])->middleware(['api.ability:write']);
+ Route::delete('/services/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'delete_scheduled_task_by_service_uuid'])->middleware(['api.ability:write']);
+ Route::get('/services/{uuid}/scheduled-tasks/{task_uuid}/executions', [ScheduledTasksController::class, 'executions_by_service_uuid'])->middleware(['api.ability:read']);
});
Route::group([
diff --git a/routes/web.php b/routes/web.php
index e8c738b71..b6c6c95ce 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -62,6 +62,7 @@ use App\Livewire\Server\Show as ServerShow;
use App\Livewire\Server\Swarm as ServerSwarm;
use App\Livewire\Settings\Advanced as SettingsAdvanced;
use App\Livewire\Settings\Index as SettingsIndex;
+use App\Livewire\Settings\ScheduledJobs as SettingsScheduledJobs;
use App\Livewire\Settings\Updates as SettingsUpdates;
use App\Livewire\SettingsBackup;
use App\Livewire\SettingsEmail;
@@ -119,6 +120,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/settings/backup', SettingsBackup::class)->name('settings.backup');
Route::get('/settings/email', SettingsEmail::class)->name('settings.email');
Route::get('/settings/oauth', SettingsOauth::class)->name('settings.oauth');
+ Route::get('/settings/scheduled-jobs', SettingsScheduledJobs::class)->name('settings.scheduled-jobs');
Route::get('/profile', ProfileIndex::class)->name('profile');
diff --git a/templates/compose/glitchtip.yaml b/templates/compose/glitchtip.yaml
index 3e86d813a..5f239daee 100644
--- a/templates/compose/glitchtip.yaml
+++ b/templates/compose/glitchtip.yaml
@@ -1,9 +1,9 @@
# documentation: https://glitchtip.com
-# slogan: GlitchTip is a self-hosted, open-source error tracking system.
+# slogan: GlitchTip is a error tracking system.
# category: monitoring
-# tags: error, tracking, open-source, self-hosted, sentry
+# tags: error, tracking, sentry
# logo: svgs/glitchtip.png
-# port: 8080
+# port: 8000
services:
postgres:
@@ -29,14 +29,14 @@ services:
retries: 10
web:
- image: glitchtip/glitchtip
+ image: glitchtip/glitchtip:6.0
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
- - SERVICE_URL_GLITCHTIP_8080
+ - SERVICE_URL_GLITCHTIP_8000
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgres:5432/${POSTGRESQL_DATABASE:-glitchtip}
- SECRET_KEY=$SERVICE_BASE64_64_ENCRYPTION
- EMAIL_URL=${EMAIL_URL:-consolemail://}
@@ -53,7 +53,7 @@ services:
retries: 10
worker:
- image: glitchtip/glitchtip
+ image: glitchtip/glitchtip:6.0
command: ./bin/run-celery-with-beat.sh
depends_on:
postgres:
@@ -77,7 +77,7 @@ services:
retries: 10
migrate:
- image: glitchtip/glitchtip
+ image: glitchtip/glitchtip:6.0
restart: "no"
depends_on:
postgres:
diff --git a/templates/compose/maybe.yaml b/templates/compose/maybe.yaml
index 27bcbc5a1..1cc738d7e 100644
--- a/templates/compose/maybe.yaml
+++ b/templates/compose/maybe.yaml
@@ -1,3 +1,4 @@
+# ignore: true
# documentation: https://github.com/maybe-finance/maybe
# slogan: Maybe, the OS for your personal finances.
# category: productivity
diff --git a/templates/compose/spacebot.yaml b/templates/compose/spacebot.yaml
new file mode 100644
index 000000000..2ecfdacd6
--- /dev/null
+++ b/templates/compose/spacebot.yaml
@@ -0,0 +1,31 @@
+# documentation: https://docs.spacebot.sh/docker
+# slogan: An agentic AI system with specialized processes for thinking, working, and remembering.
+# category: ai
+# tags: ai, agent, anthropic, openai, discord, slack, llm, agentic
+# logo: svgs/spacebot.png
+# port: 19898
+
+services:
+ spacebot:
+ image: "ghcr.io/spacedriveapp/spacebot:full"
+ environment:
+ - SERVICE_FQDN_SPACEBOT_19898
+ - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
+ - OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
+ - DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
+ - SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN}
+ - SLACK_APP_TOKEN=${SLACK_APP_TOKEN}
+ - BRAVE_SEARCH_API_KEY=${BRAVE_SEARCH_API_KEY}
+ - SPACEBOT_CHANNEL_MODEL=${SPACEBOT_CHANNEL_MODEL}
+ - SPACEBOT_WORKER_MODEL=${SPACEBOT_WORKER_MODEL}
+ volumes:
+ - "spacebot-data:/data"
+ security_opt:
+ - seccomp=unconfined
+ shm_size: 1g
+ healthcheck:
+ test: ["CMD", "curl", "-sf", "http://localhost:19898/api/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
diff --git a/templates/compose/sure.yaml b/templates/compose/sure.yaml
new file mode 100644
index 000000000..7c72156e3
--- /dev/null
+++ b/templates/compose/sure.yaml
@@ -0,0 +1,93 @@
+# documentation: https://github.com/we-promise/sure
+# slogan: An all-in-one personal finance platform.
+# category: finance
+# tags: budgeting,budget,money,expenses,income
+# logo: svgs/sure.png
+# port: 3000
+
+services:
+ web:
+ image: ghcr.io/we-promise/sure:0.6.7
+ environment:
+ - SERVICE_URL_SURE_3000
+ - APP_DOMAIN=$SERVICE_FQDN_SURE
+ - SECRET_KEY_BASE=$SERVICE_BASE64_BASE
+ - POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
+ - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
+ - POSTGRES_DB=${POSTGRESQL_DATABASE:-sure}
+ - 'REDIS_URL=redis://valkey:6379'
+ - DB_HOST=postgresql
+ - DB_PORT=5432
+ - 'SELF_HOSTED=true'
+ - 'RAILS_FORCE_SSL=false'
+ - 'RAILS_ASSUME_SSL=false'
+ - ONBOARDING_STATE=${ONBOARDING_STATE:-open}
+ depends_on:
+ postgresql:
+ condition: service_healthy
+ valkey:
+ condition: service_healthy
+ volumes:
+ - sure-app-storage:/rails/storage
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://127.0.0.1:3000"]
+ interval: 15s
+ timeout: 20s
+ retries: 5
+
+ worker:
+ image: ghcr.io/we-promise/sure:0.6.7
+ command: bundle exec sidekiq
+ environment:
+ - APP_DOMAIN=$SERVICE_FQDN_SURE
+ - SECRET_KEY_BASE=$SERVICE_BASE64_BASE
+ - POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
+ - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
+ - POSTGRES_DB=${POSTGRESQL_DATABASE:-sure}
+ - 'REDIS_URL=redis://valkey:6379'
+ - DB_HOST=postgresql
+ - DB_PORT=5432
+ - 'SELF_HOSTED=true'
+ - 'RAILS_FORCE_SSL=false'
+ - 'RAILS_ASSUME_SSL=false'
+ - ONBOARDING_STATE=${ONBOARDING_STATE:-open}
+ volumes:
+ - sure-app-storage:/rails/storage
+ depends_on:
+ postgresql:
+ condition: service_healthy
+ valkey:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://127.0.0.1:3000"]
+ interval: 15s
+ timeout: 20s
+ retries: 5
+
+ postgresql:
+ image: postgres:16-alpine
+ volumes:
+ - sure-postgresql-data:/var/lib/postgresql/data
+ environment:
+ - POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
+ - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
+ - POSTGRES_DB=${POSTGRESQL_DATABASE:-sure}
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
+ interval: 5s
+ timeout: 20s
+ retries: 10
+
+ valkey:
+ image: valkey/valkey:8-alpine
+ command: valkey-server --appendonly yes
+ volumes:
+ - sure-valkey:/data
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - 'valkey-cli ping | grep PONG'
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ start_period: 3s
diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json
index 3c5773d0e..832899a70 100644
--- a/templates/service-templates-latest.json
+++ b/templates/service-templates-latest.json
@@ -1758,19 +1758,17 @@
},
"glitchtip": {
"documentation": "https://glitchtip.com?utm_source=coolify.io",
- "slogan": "GlitchTip is a self-hosted, open-source error tracking system.",
- "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dsaXRjaHRpcC1wb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiByZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HTElUQ0hUSVBfODA4MAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTEBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9FTkNSWVBUSU9OCiAgICAgIC0gJ0VNQUlMX1VSTD0ke0VNQUlMX1VSTDotY29uc29sZW1haWw6Ly99JwogICAgICAtICdHTElUQ0hUSVBfRE9NQUlOPSR7U0VSVklDRV9VUkxfR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHdvcmtlcjoKICAgIGltYWdlOiBnbGl0Y2h0aXAvZ2xpdGNodGlwCiAgICBjb21tYW5kOiAuL2Jpbi9ydW4tY2VsZXJ5LXdpdGgtYmVhdC5zaAogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTEBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9FTkNSWVBUSU9OCiAgICAgIC0gJ0VNQUlMX1VSTD0ke0VNQUlMX1VSTDotY29uc29sZW1haWw6Ly99JwogICAgICAtICdHTElUQ0hUSVBfRE9NQUlOPSR7U0VSVklDRV9VUkxfR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pZ3JhdGU6CiAgICBpbWFnZTogZ2xpdGNodGlwL2dsaXRjaHRpcAogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=",
+ "slogan": "GlitchTip is a error tracking system.",
+ "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dsaXRjaHRpcC1wb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiByZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6ICdnbGl0Y2h0aXAvZ2xpdGNodGlwOjYuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HTElUQ0hUSVBfODAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTEBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9FTkNSWVBUSU9OCiAgICAgIC0gJ0VNQUlMX1VSTD0ke0VNQUlMX1VSTDotY29uc29sZW1haWw6Ly99JwogICAgICAtICdHTElUQ0hUSVBfRE9NQUlOPSR7U0VSVklDRV9VUkxfR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHdvcmtlcjoKICAgIGltYWdlOiAnZ2xpdGNodGlwL2dsaXRjaHRpcDo2LjAnCiAgICBjb21tYW5kOiAuL2Jpbi9ydW4tY2VsZXJ5LXdpdGgtYmVhdC5zaAogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTEBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9FTkNSWVBUSU9OCiAgICAgIC0gJ0VNQUlMX1VSTD0ke0VNQUlMX1VSTDotY29uc29sZW1haWw6Ly99JwogICAgICAtICdHTElUQ0hUSVBfRE9NQUlOPSR7U0VSVklDRV9VUkxfR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pZ3JhdGU6CiAgICBpbWFnZTogJ2dsaXRjaHRpcC9nbGl0Y2h0aXA6Ni4wJwogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=",
"tags": [
"error",
"tracking",
- "open-source",
- "self-hosted",
"sentry"
],
"category": "monitoring",
"logo": "svgs/glitchtip.png",
"minversion": "0.0.0",
- "port": "8080"
+ "port": "8000"
},
"glpi": {
"documentation": "https://help.glpi-project.org/documentation?utm_source=coolify.io",
@@ -2681,24 +2679,6 @@
"minversion": "0.0.0",
"port": "8065"
},
- "maybe": {
- "documentation": "https://github.com/maybe-finance/maybe?utm_source=coolify.io",
- "slogan": "Maybe, the OS for your personal finances.",
- "compose": "c2VydmljZXM6CiAgbWF5YmU6CiAgICBpbWFnZTogJ2doY3IuaW8vbWF5YmUtZmluYW5jZS9tYXliZTpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdhcHBfc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01BWUJFCiAgICAgIC0gU0VMRl9IT1NURUQ9dHJ1ZQogICAgICAtICdSQUlMU19GT1JDRV9TU0w9JHtSQUlMU19GT1JDRV9TU0w6LWZhbHNlfScKICAgICAgLSAnUkFJTFNfQVNTVU1FX1NTTD0ke1JBSUxTX0FTU1VNRV9TU0w6LWZhbHNlfScKICAgICAgLSAnR09PRF9KT0JfRVhFQ1VUSU9OX01PREU9JHtHT09EX0pPQl9FWEVDVVRJT05fTU9ERTotYXN5bmN9JwogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX0JBU0U2NF82NF9TRUNSRVRLRVlCQVNFfScKICAgICAgLSBEQl9IT1NUPXBvc3RncmVzCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW1heWJlLWRifScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHQ6JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfUByZWRpczo2Mzc5LzEnCiAgICAgIC0gJ09QRU5BSV9BQ0NFU1NfVE9LRU49JHtPUEVOQUlfQUNDRVNTX1RPS0VOfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQogIHdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby9tYXliZS1maW5hbmNlL21heWJlOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICdidW5kbGUgZXhlYyBzaWRla2lxJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1tYXliZS1kYn0nCiAgICAgIC0gJ1NFQ1JFVF9LRVlfQkFTRT0ke1NFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVEtFWUJBU0V9JwogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSAnUkFJTFNfRk9SQ0VfU1NMPSR7UkFJTFNfRk9SQ0VfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ1JBSUxTX0FTU1VNRV9TU0w9JHtSQUlMU19BU1NVTUVfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ0dPT0RfSk9CX0VYRUNVVElPTl9NT0RFPSR7R09PRF9KT0JfRVhFQ1VUSU9OX01PREU6LWFzeW5jfScKICAgICAgLSBEQl9IT1NUPXBvc3RncmVzCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHQ6JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfUByZWRpczo2Mzc5LzEnCiAgICAgIC0gJ09QRU5BSV9BQ0NFU1NfVE9LRU49JHtPUEVOQUlfQUNDRVNTX1RPS0VOfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBleGNsdWRlX2Zyb21faGM6IHRydWUKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnbWF5YmVfcG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW1heWJlLWRifScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6OC1hbHBpbmUnCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tYXBwZW5kb25seSB5ZXMgLS1yZXF1aXJlcGFzcyAke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXNfZGF0YTovZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtIFJFRElTX0RCPTEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctLXBhc3MnCiAgICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAzcwogICAgICByZXRyaWVzOiAzCg==",
- "tags": [
- "finances",
- "wallets",
- "coins",
- "stocks",
- "investments",
- "open",
- "source"
- ],
- "category": "productivity",
- "logo": "svgs/maybe.svg",
- "minversion": "0.0.0",
- "port": "3000"
- },
"mealie": {
"documentation": "https://docs.mealie.io/?utm_source=coolify.io",
"slogan": "A recipe manager and meal planner.",
@@ -4404,6 +4384,25 @@
"minversion": "0.0.0",
"port": "8989"
},
+ "spacebot": {
+ "documentation": "https://docs.spacebot.sh/docker?utm_source=coolify.io",
+ "slogan": "An agentic AI system with specialized processes for thinking, working, and remembering.",
+ "compose": "c2VydmljZXM6CiAgc3BhY2Vib3Q6CiAgICBpbWFnZTogJ2doY3IuaW8vc3BhY2Vkcml2ZWFwcC9zcGFjZWJvdDpmdWxsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NQQUNFQk9UXzE5ODk4CiAgICAgIC0gJ0FOVEhST1BJQ19BUElfS0VZPSR7QU5USFJPUElDX0FQSV9LRVl9JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke09QRU5BSV9BUElfS0VZfScKICAgICAgLSAnT1BFTlJPVVRFUl9BUElfS0VZPSR7T1BFTlJPVVRFUl9BUElfS0VZfScKICAgICAgLSAnRElTQ09SRF9CT1RfVE9LRU49JHtESVNDT1JEX0JPVF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0JPVF9UT0tFTj0ke1NMQUNLX0JPVF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0FQUF9UT0tFTj0ke1NMQUNLX0FQUF9UT0tFTn0nCiAgICAgIC0gJ0JSQVZFX1NFQVJDSF9BUElfS0VZPSR7QlJBVkVfU0VBUkNIX0FQSV9LRVl9JwogICAgICAtICdTUEFDRUJPVF9DSEFOTkVMX01PREVMPSR7U1BBQ0VCT1RfQ0hBTk5FTF9NT0RFTH0nCiAgICAgIC0gJ1NQQUNFQk9UX1dPUktFUl9NT0RFTD0ke1NQQUNFQk9UX1dPUktFUl9NT0RFTH0nCiAgICB2b2x1bWVzOgogICAgICAtICdzcGFjZWJvdC1kYXRhOi9kYXRhJwogICAgc2VjdXJpdHlfb3B0OgogICAgICAtIHNlY2NvbXA9dW5jb25maW5lZAogICAgc2htX3NpemU6IDFnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1zZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjE5ODk4L2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwo=",
+ "tags": [
+ "ai",
+ "agent",
+ "anthropic",
+ "openai",
+ "discord",
+ "slack",
+ "llm",
+ "agentic"
+ ],
+ "category": "ai",
+ "logo": "svgs/spacebot.png",
+ "minversion": "0.0.0",
+ "port": "19898"
+ },
"sparkyfitness": {
"documentation": "https://codewithcj.github.io/SparkyFitness/?utm_source=coolify.io",
"slogan": "SparkyFitness is a comprehensive fitness tracking and management application designed to help users monitor their nutrition, exercise, and body measurements. It provides tools for daily progress tracking, goal setting, and insightful reports to support a healthy lifestyle.",
@@ -4548,6 +4547,22 @@
"minversion": "0.0.0",
"port": "3567"
},
+ "sure": {
+ "documentation": "https://github.com/we-promise/sure?utm_source=coolify.io",
+ "slogan": "An all-in-one personal finance platform.",
+ "compose": "c2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dlLXByb21pc2Uvc3VyZTowLjYuNycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1NVUkVfMzAwMAogICAgICAtIEFQUF9ET01BSU49JFNFUlZJQ0VfRlFETl9TVVJFCiAgICAgIC0gU0VDUkVUX0tFWV9CQVNFPSRTRVJWSUNFX0JBU0U2NF9CQVNFCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1zdXJlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vdmFsa2V5OjYzNzknCiAgICAgIC0gREJfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gU0VMRl9IT1NURUQ9dHJ1ZQogICAgICAtIFJBSUxTX0ZPUkNFX1NTTD1mYWxzZQogICAgICAtIFJBSUxTX0FTU1VNRV9TU0w9ZmFsc2UKICAgICAgLSAnT05CT0FSRElOR19TVEFURT0ke09OQk9BUkRJTkdfU1RBVEU6LW9wZW59JwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICB2YWxrZXk6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtYXBwLXN0b3JhZ2U6L3JhaWxzL3N0b3JhZ2UnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogNQogIHdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby93ZS1wcm9taXNlL3N1cmU6MC42LjcnCiAgICBjb21tYW5kOiAnYnVuZGxlIGV4ZWMgc2lkZWtpcScKICAgIGVudmlyb25tZW50OgogICAgICAtIEFQUF9ET01BSU49JFNFUlZJQ0VfRlFETl9TVVJFCiAgICAgIC0gU0VDUkVUX0tFWV9CQVNFPSRTRVJWSUNFX0JBU0U2NF9CQVNFCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1zdXJlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vdmFsa2V5OjYzNzknCiAgICAgIC0gREJfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gU0VMRl9IT1NURUQ9dHJ1ZQogICAgICAtIFJBSUxTX0ZPUkNFX1NTTD1mYWxzZQogICAgICAtIFJBSUxTX0FTU1VNRV9TU0w9ZmFsc2UKICAgICAgLSAnT05CT0FSRElOR19TVEFURT0ke09OQk9BUkRJTkdfU1RBVEU6LW9wZW59JwogICAgdm9sdW1lczoKICAgICAgLSAnc3VyZS1hcHAtc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgdmFsa2V5OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogNQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotc3VyZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgdmFsa2V5OgogICAgaW1hZ2U6ICd2YWxrZXkvdmFsa2V5OjgtYWxwaW5lJwogICAgY29tbWFuZDogJ3ZhbGtleS1zZXJ2ZXIgLS1hcHBlbmRvbmx5IHllcycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtdmFsa2V5Oi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd2YWxrZXktY2xpIHBpbmcgfCBncmVwIFBPTkcnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogM3MK",
+ "tags": [
+ "budgeting",
+ "budget",
+ "money",
+ "expenses",
+ "income"
+ ],
+ "category": "finance",
+ "logo": "svgs/sure.png",
+ "minversion": "0.0.0",
+ "port": "3000"
+ },
"swetrix": {
"documentation": "https://docs.swetrix.com/selfhosting/how-to?utm_source=coolify.io",
"slogan": "Privacy-friendly and cookieless European web analytics alternative to Google Analytics.",
diff --git a/templates/service-templates.json b/templates/service-templates.json
index 545d9c62e..88eddd10b 100644
--- a/templates/service-templates.json
+++ b/templates/service-templates.json
@@ -1758,19 +1758,17 @@
},
"glitchtip": {
"documentation": "https://glitchtip.com?utm_source=coolify.io",
- "slogan": "GlitchTip is a self-hosted, open-source error tracking system.",
- "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dsaXRjaHRpcC1wb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiByZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR0xJVENIVElQXzgwODAKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWdsaXRjaHRpcH0nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdFTUFJTF9VUkw9JHtFTUFJTF9VUkw6LWNvbnNvbGVtYWlsOi8vfScKICAgICAgLSAnR0xJVENIVElQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HTElUQ0hUSVB9JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdDRUxFUllfV09SS0VSX0FVVE9TQ0FMRT0ke0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFOi0xLDN9JwogICAgICAtICdDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9JHtDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ6LTEwMDAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2NvZGUvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd29ya2VyOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGNvbW1hbmQ6IC4vYmluL3J1bi1jZWxlcnktd2l0aC1iZWF0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0dMSVRDSFRJUF9ET01BSU49JHtTRVJWSUNFX0ZRRE5fR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pZ3JhdGU6CiAgICBpbWFnZTogZ2xpdGNodGlwL2dsaXRjaHRpcAogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=",
+ "slogan": "GlitchTip is a error tracking system.",
+ "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dsaXRjaHRpcC1wb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiByZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6ICdnbGl0Y2h0aXAvZ2xpdGNodGlwOjYuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR0xJVENIVElQXzgwMDAKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWdsaXRjaHRpcH0nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdFTUFJTF9VUkw9JHtFTUFJTF9VUkw6LWNvbnNvbGVtYWlsOi8vfScKICAgICAgLSAnR0xJVENIVElQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HTElUQ0hUSVB9JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdDRUxFUllfV09SS0VSX0FVVE9TQ0FMRT0ke0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFOi0xLDN9JwogICAgICAtICdDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9JHtDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ6LTEwMDAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2NvZGUvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd29ya2VyOgogICAgaW1hZ2U6ICdnbGl0Y2h0aXAvZ2xpdGNodGlwOjYuMCcKICAgIGNvbW1hbmQ6IC4vYmluL3J1bi1jZWxlcnktd2l0aC1iZWF0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0dMSVRDSFRJUF9ET01BSU49JHtTRVJWSUNFX0ZRRE5fR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pZ3JhdGU6CiAgICBpbWFnZTogJ2dsaXRjaHRpcC9nbGl0Y2h0aXA6Ni4wJwogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=",
"tags": [
"error",
"tracking",
- "open-source",
- "self-hosted",
"sentry"
],
"category": "monitoring",
"logo": "svgs/glitchtip.png",
"minversion": "0.0.0",
- "port": "8080"
+ "port": "8000"
},
"glpi": {
"documentation": "https://help.glpi-project.org/documentation?utm_source=coolify.io",
@@ -2681,24 +2679,6 @@
"minversion": "0.0.0",
"port": "8065"
},
- "maybe": {
- "documentation": "https://github.com/maybe-finance/maybe?utm_source=coolify.io",
- "slogan": "Maybe, the OS for your personal finances.",
- "compose": "c2VydmljZXM6CiAgbWF5YmU6CiAgICBpbWFnZTogJ2doY3IuaW8vbWF5YmUtZmluYW5jZS9tYXliZTpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdhcHBfc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NQVlCRQogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSAnUkFJTFNfRk9SQ0VfU1NMPSR7UkFJTFNfRk9SQ0VfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ1JBSUxTX0FTU1VNRV9TU0w9JHtSQUlMU19BU1NVTUVfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ0dPT0RfSk9CX0VYRUNVVElPTl9NT0RFPSR7R09PRF9KT0JfRVhFQ1VUSU9OX01PREU6LWFzeW5jfScKICAgICAgLSAnU0VDUkVUX0tFWV9CQVNFPSR7U0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZQkFTRX0nCiAgICAgIC0gREJfSE9TVD1wb3N0Z3JlcwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1tYXliZS1kYn0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OS8xJwogICAgICAtICdPUEVOQUlfQUNDRVNTX1RPS0VOPSR7T1BFTkFJX0FDQ0VTU19UT0tFTn0nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjMwMDAnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDUKICB3b3JrZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vbWF5YmUtZmluYW5jZS9tYXliZTpsYXRlc3QnCiAgICBjb21tYW5kOiAnYnVuZGxlIGV4ZWMgc2lkZWtpcScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbWF5YmUtZGJ9JwogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX0JBU0U2NF82NF9TRUNSRVRLRVlCQVNFfScKICAgICAgLSBTRUxGX0hPU1RFRD10cnVlCiAgICAgIC0gJ1JBSUxTX0ZPUkNFX1NTTD0ke1JBSUxTX0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdSQUlMU19BU1NVTUVfU1NMPSR7UkFJTFNfQVNTVU1FX1NTTDotZmFsc2V9JwogICAgICAtICdHT09EX0pPQl9FWEVDVVRJT05fTU9ERT0ke0dPT0RfSk9CX0VYRUNVVElPTl9NT0RFOi1hc3luY30nCiAgICAgIC0gREJfSE9TVD1wb3N0Z3JlcwogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OS8xJwogICAgICAtICdPUEVOQUlfQUNDRVNTX1RPS0VOPSR7T1BFTkFJX0FDQ0VTU19UT0tFTn0nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21heWJlX3Bvc3RncmVzX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1tYXliZS1kYn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjgtYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzIC0tcmVxdWlyZXBhc3MgJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzX2RhdGE6L2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSBSRURJU19EQj0xCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLS1wYXNzJwogICAgICAgIC0gJyR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogM3MKICAgICAgcmV0cmllczogMwo=",
- "tags": [
- "finances",
- "wallets",
- "coins",
- "stocks",
- "investments",
- "open",
- "source"
- ],
- "category": "productivity",
- "logo": "svgs/maybe.svg",
- "minversion": "0.0.0",
- "port": "3000"
- },
"mealie": {
"documentation": "https://docs.mealie.io/?utm_source=coolify.io",
"slogan": "A recipe manager and meal planner.",
@@ -4404,6 +4384,25 @@
"minversion": "0.0.0",
"port": "8989"
},
+ "spacebot": {
+ "documentation": "https://docs.spacebot.sh/docker?utm_source=coolify.io",
+ "slogan": "An agentic AI system with specialized processes for thinking, working, and remembering.",
+ "compose": "c2VydmljZXM6CiAgc3BhY2Vib3Q6CiAgICBpbWFnZTogJ2doY3IuaW8vc3BhY2Vkcml2ZWFwcC9zcGFjZWJvdDpmdWxsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NQQUNFQk9UXzE5ODk4CiAgICAgIC0gJ0FOVEhST1BJQ19BUElfS0VZPSR7QU5USFJPUElDX0FQSV9LRVl9JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke09QRU5BSV9BUElfS0VZfScKICAgICAgLSAnT1BFTlJPVVRFUl9BUElfS0VZPSR7T1BFTlJPVVRFUl9BUElfS0VZfScKICAgICAgLSAnRElTQ09SRF9CT1RfVE9LRU49JHtESVNDT1JEX0JPVF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0JPVF9UT0tFTj0ke1NMQUNLX0JPVF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0FQUF9UT0tFTj0ke1NMQUNLX0FQUF9UT0tFTn0nCiAgICAgIC0gJ0JSQVZFX1NFQVJDSF9BUElfS0VZPSR7QlJBVkVfU0VBUkNIX0FQSV9LRVl9JwogICAgICAtICdTUEFDRUJPVF9DSEFOTkVMX01PREVMPSR7U1BBQ0VCT1RfQ0hBTk5FTF9NT0RFTH0nCiAgICAgIC0gJ1NQQUNFQk9UX1dPUktFUl9NT0RFTD0ke1NQQUNFQk9UX1dPUktFUl9NT0RFTH0nCiAgICB2b2x1bWVzOgogICAgICAtICdzcGFjZWJvdC1kYXRhOi9kYXRhJwogICAgc2VjdXJpdHlfb3B0OgogICAgICAtIHNlY2NvbXA9dW5jb25maW5lZAogICAgc2htX3NpemU6IDFnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1zZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjE5ODk4L2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwo=",
+ "tags": [
+ "ai",
+ "agent",
+ "anthropic",
+ "openai",
+ "discord",
+ "slack",
+ "llm",
+ "agentic"
+ ],
+ "category": "ai",
+ "logo": "svgs/spacebot.png",
+ "minversion": "0.0.0",
+ "port": "19898"
+ },
"sparkyfitness": {
"documentation": "https://codewithcj.github.io/SparkyFitness/?utm_source=coolify.io",
"slogan": "SparkyFitness is a comprehensive fitness tracking and management application designed to help users monitor their nutrition, exercise, and body measurements. It provides tools for daily progress tracking, goal setting, and insightful reports to support a healthy lifestyle.",
@@ -4548,6 +4547,22 @@
"minversion": "0.0.0",
"port": "3567"
},
+ "sure": {
+ "documentation": "https://github.com/we-promise/sure?utm_source=coolify.io",
+ "slogan": "An all-in-one personal finance platform.",
+ "compose": "c2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dlLXByb21pc2Uvc3VyZTowLjYuNycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVJFXzMwMDAKICAgICAgLSBBUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fU1VSRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfQkFTRQogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotc3VyZX0nCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3ZhbGtleTo2Mzc5JwogICAgICAtIERCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSBSQUlMU19GT1JDRV9TU0w9ZmFsc2UKICAgICAgLSBSQUlMU19BU1NVTUVfU1NMPWZhbHNlCiAgICAgIC0gJ09OQk9BUkRJTkdfU1RBVEU9JHtPTkJPQVJESU5HX1NUQVRFOi1vcGVufScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgdmFsa2V5OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICdzdXJlLWFwcC1zdG9yYWdlOi9yYWlscy9zdG9yYWdlJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAxNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDUKICB3b3JrZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vd2UtcHJvbWlzZS9zdXJlOjAuNi43JwogICAgY29tbWFuZDogJ2J1bmRsZSBleGVjIHNpZGVraXEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBBUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fU1VSRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfQkFTRQogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotc3VyZX0nCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3ZhbGtleTo2Mzc5JwogICAgICAtIERCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSBSQUlMU19GT1JDRV9TU0w9ZmFsc2UKICAgICAgLSBSQUlMU19BU1NVTUVfU1NMPWZhbHNlCiAgICAgIC0gJ09OQk9BUkRJTkdfU1RBVEU9JHtPTkJPQVJESU5HX1NUQVRFOi1vcGVufScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtYXBwLXN0b3JhZ2U6L3JhaWxzL3N0b3JhZ2UnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHZhbGtleToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAxNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdzdXJlLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LXN1cmV9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHZhbGtleToKICAgIGltYWdlOiAndmFsa2V5L3ZhbGtleTo4LWFscGluZScKICAgIGNvbW1hbmQ6ICd2YWxrZXktc2VydmVyIC0tYXBwZW5kb25seSB5ZXMnCiAgICB2b2x1bWVzOgogICAgICAtICdzdXJlLXZhbGtleTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAndmFsa2V5LWNsaSBwaW5nIHwgZ3JlcCBQT05HJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDNzCg==",
+ "tags": [
+ "budgeting",
+ "budget",
+ "money",
+ "expenses",
+ "income"
+ ],
+ "category": "finance",
+ "logo": "svgs/sure.png",
+ "minversion": "0.0.0",
+ "port": "3000"
+ },
"swetrix": {
"documentation": "https://docs.swetrix.com/selfhosting/how-to?utm_source=coolify.io",
"slogan": "Privacy-friendly and cookieless European web analytics alternative to Google Analytics.",
diff --git a/tests/Browser/screenshots/.gitignore b/tests/Browser/screenshots/.gitignore
deleted file mode 100644
index d6b7ef32c..000000000
--- a/tests/Browser/screenshots/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!.gitignore
diff --git a/tests/Feature/EnvironmentVariableUpdateApiTest.php b/tests/Feature/EnvironmentVariableUpdateApiTest.php
new file mode 100644
index 000000000..9c45dc5ae
--- /dev/null
+++ b/tests/Feature/EnvironmentVariableUpdateApiTest.php
@@ -0,0 +1,194 @@
+team = Team::factory()->create();
+ $this->user = User::factory()->create();
+ $this->team->members()->attach($this->user->id, ['role' => 'owner']);
+
+ session(['currentTeam' => $this->team]);
+
+ $this->token = $this->user->createToken('test-token', ['*']);
+ $this->bearerToken = $this->token->plainTextToken;
+
+ $this->server = Server::factory()->create(['team_id' => $this->team->id]);
+ $this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]);
+ $this->project = Project::factory()->create(['team_id' => $this->team->id]);
+ $this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
+});
+
+describe('PATCH /api/v1/services/{uuid}/envs', function () {
+ test('returns the updated environment variable object', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ EnvironmentVariable::create([
+ 'key' => 'APP_IMAGE_TAG',
+ 'value' => 'old-value',
+ 'resourceable_type' => Service::class,
+ 'resourceable_id' => $service->id,
+ 'is_preview' => false,
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/services/{$service->uuid}/envs", [
+ 'key' => 'APP_IMAGE_TAG',
+ 'value' => 'new-value',
+ ]);
+
+ $response->assertStatus(201);
+ $response->assertJsonStructure([
+ 'uuid',
+ 'key',
+ 'is_literal',
+ 'is_multiline',
+ 'is_shown_once',
+ 'created_at',
+ 'updated_at',
+ ]);
+ $response->assertJsonFragment(['key' => 'APP_IMAGE_TAG']);
+ $response->assertJsonMissing(['message' => 'Environment variable updated.']);
+ });
+
+ test('returns 404 when environment variable does not exist', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/services/{$service->uuid}/envs", [
+ 'key' => 'NONEXISTENT_KEY',
+ 'value' => 'some-value',
+ ]);
+
+ $response->assertStatus(404);
+ $response->assertJson(['message' => 'Environment variable not found.']);
+ });
+
+ test('returns 404 when service does not exist', function () {
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson('/api/v1/services/non-existent-uuid/envs', [
+ 'key' => 'APP_IMAGE_TAG',
+ 'value' => 'some-value',
+ ]);
+
+ $response->assertStatus(404);
+ });
+
+ test('returns 422 when key is missing', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/services/{$service->uuid}/envs", [
+ 'value' => 'some-value',
+ ]);
+
+ $response->assertStatus(422);
+ });
+});
+
+describe('PATCH /api/v1/applications/{uuid}/envs', function () {
+ test('returns the updated environment variable object', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ EnvironmentVariable::create([
+ 'key' => 'APP_IMAGE_TAG',
+ 'value' => 'old-value',
+ 'resourceable_type' => Application::class,
+ 'resourceable_id' => $application->id,
+ 'is_preview' => false,
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/applications/{$application->uuid}/envs", [
+ 'key' => 'APP_IMAGE_TAG',
+ 'value' => 'new-value',
+ ]);
+
+ $response->assertStatus(201);
+ $response->assertJsonStructure([
+ 'uuid',
+ 'key',
+ 'is_literal',
+ 'is_multiline',
+ 'is_shown_once',
+ 'created_at',
+ 'updated_at',
+ ]);
+ $response->assertJsonFragment(['key' => 'APP_IMAGE_TAG']);
+ $response->assertJsonMissing(['message' => 'Environment variable updated.']);
+ });
+
+ test('returns 404 when environment variable does not exist', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/applications/{$application->uuid}/envs", [
+ 'key' => 'NONEXISTENT_KEY',
+ 'value' => 'some-value',
+ ]);
+
+ $response->assertStatus(404);
+ });
+
+ test('returns 422 when key is missing', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/applications/{$application->uuid}/envs", [
+ 'value' => 'some-value',
+ ]);
+
+ $response->assertStatus(422);
+ });
+});
diff --git a/tests/Feature/PushServerUpdateJobTest.php b/tests/Feature/PushServerUpdateJobTest.php
new file mode 100644
index 000000000..d508d58ab
--- /dev/null
+++ b/tests/Feature/PushServerUpdateJobTest.php
@@ -0,0 +1,76 @@
+create();
+ $service = Service::factory()->create([
+ 'server_id' => $server->id,
+ ]);
+ $serviceApp = ServiceApplication::factory()->create([
+ 'service_id' => $service->id,
+ ]);
+
+ $data = [
+ 'containers' => [
+ [
+ 'name' => 'test-container',
+ 'state' => 'running',
+ 'health_status' => 'healthy',
+ 'labels' => [
+ 'coolify.managed' => true,
+ 'coolify.serviceId' => (string) $service->id,
+ 'coolify.service.subType' => 'application',
+ 'coolify.service.subId' => '',
+ ],
+ ],
+ ],
+ ];
+
+ $job = new PushServerUpdateJob($server, $data);
+
+ // Run handle - should not throw a PDOException about empty bigint
+ $job->handle();
+
+ // The empty subId container should have been skipped
+ expect($job->foundServiceApplicationIds)->not->toContain('');
+ expect($job->serviceContainerStatuses)->toBeEmpty();
+});
+
+test('containers with valid service subId are processed', function () {
+ $server = Server::factory()->create();
+ $service = Service::factory()->create([
+ 'server_id' => $server->id,
+ ]);
+ $serviceApp = ServiceApplication::factory()->create([
+ 'service_id' => $service->id,
+ ]);
+
+ $data = [
+ 'containers' => [
+ [
+ 'name' => 'test-container',
+ 'state' => 'running',
+ 'health_status' => 'healthy',
+ 'labels' => [
+ 'coolify.managed' => true,
+ 'coolify.serviceId' => (string) $service->id,
+ 'coolify.service.subType' => 'application',
+ 'coolify.service.subId' => (string) $serviceApp->id,
+ 'com.docker.compose.service' => 'myapp',
+ ],
+ ],
+ ],
+ ];
+
+ $job = new PushServerUpdateJob($server, $data);
+ $job->handle();
+
+ expect($job->foundServiceApplicationIds)->toContain((string) $serviceApp->id);
+});
diff --git a/tests/Feature/ScheduledJobMonitoringTest.php b/tests/Feature/ScheduledJobMonitoringTest.php
new file mode 100644
index 000000000..1348375d4
--- /dev/null
+++ b/tests/Feature/ScheduledJobMonitoringTest.php
@@ -0,0 +1,200 @@
+rootTeam = Team::factory()->create(['id' => 0, 'name' => 'Root Team']);
+ $this->rootUser = User::factory()->create();
+ $this->rootUser->teams()->attach($this->rootTeam, ['role' => 'owner']);
+
+ // Create regular team and user
+ $this->regularTeam = Team::factory()->create();
+ $this->regularUser = User::factory()->create();
+ $this->regularUser->teams()->attach($this->regularTeam, ['role' => 'owner']);
+});
+
+test('scheduled jobs page requires instance admin access', function () {
+ $this->actingAs($this->regularUser);
+ session(['currentTeam' => $this->regularTeam]);
+
+ $response = $this->get(route('settings.scheduled-jobs'));
+ $response->assertRedirect(route('dashboard'));
+});
+
+test('scheduled jobs page is accessible by instance admin', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->assertStatus(200)
+ ->assertSee('Scheduled Job Issues');
+});
+
+test('scheduled jobs page shows failed backup executions', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ $server = Server::factory()->create(['team_id' => $this->rootTeam->id]);
+
+ $backup = ScheduledDatabaseBackup::create([
+ 'team_id' => $this->rootTeam->id,
+ 'frequency' => '0 * * * *',
+ 'database_id' => 1,
+ 'database_type' => 'App\Models\StandalonePostgresql',
+ 'enabled' => true,
+ ]);
+
+ ScheduledDatabaseBackupExecution::create([
+ 'scheduled_database_backup_id' => $backup->id,
+ 'status' => 'failed',
+ 'message' => 'Backup failed: connection timeout',
+ ]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->assertStatus(200)
+ ->assertSee('Backup');
+});
+
+test('scheduled jobs page shows failed cleanup executions', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ $server = Server::factory()->create([
+ 'team_id' => $this->rootTeam->id,
+ ]);
+
+ DockerCleanupExecution::create([
+ 'server_id' => $server->id,
+ 'status' => 'failed',
+ 'message' => 'Cleanup failed: disk full',
+ ]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->assertStatus(200)
+ ->assertSee('Cleanup');
+});
+
+test('filter by type works', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->set('filterType', 'backup')
+ ->assertStatus(200)
+ ->set('filterType', 'cleanup')
+ ->assertStatus(200)
+ ->set('filterType', 'task')
+ ->assertStatus(200);
+});
+
+test('only failed executions are shown', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ $backup = ScheduledDatabaseBackup::create([
+ 'team_id' => $this->rootTeam->id,
+ 'frequency' => '0 * * * *',
+ 'database_id' => 1,
+ 'database_type' => 'App\Models\StandalonePostgresql',
+ 'enabled' => true,
+ ]);
+
+ ScheduledDatabaseBackupExecution::create([
+ 'scheduled_database_backup_id' => $backup->id,
+ 'status' => 'success',
+ 'message' => 'Backup completed successfully',
+ ]);
+
+ ScheduledDatabaseBackupExecution::create([
+ 'scheduled_database_backup_id' => $backup->id,
+ 'status' => 'failed',
+ 'message' => 'Backup failed: connection refused',
+ ]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->assertSee('Backup failed: connection refused')
+ ->assertDontSee('Backup completed successfully');
+});
+
+test('filter by date range works', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->set('filterDate', 'last_7d')
+ ->assertStatus(200)
+ ->set('filterDate', 'last_30d')
+ ->assertStatus(200)
+ ->set('filterDate', 'all')
+ ->assertStatus(200);
+});
+
+test('scheduler log parser returns empty collection when no logs exist', function () {
+ $parser = new SchedulerLogParser;
+
+ $skips = $parser->getRecentSkips();
+ expect($skips)->toBeEmpty();
+
+ $runs = $parser->getRecentRuns();
+ expect($runs)->toBeEmpty();
+})->skip(fn () => file_exists(storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log')), 'Skipped: log file already exists from other tests');
+
+test('scheduler log parser parses skip entries correctly', function () {
+ $logPath = storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log');
+ $logDir = dirname($logPath);
+ if (! is_dir($logDir)) {
+ mkdir($logDir, 0755, true);
+ }
+
+ $logLine = '['.now()->format('Y-m-d H:i:s').'] production.INFO: Backup skipped {"type":"backup","skip_reason":"server_not_functional","execution_time":"'.now()->toIso8601String().'","backup_id":1,"team_id":5}';
+ file_put_contents($logPath, $logLine."\n");
+
+ $parser = new SchedulerLogParser;
+ $skips = $parser->getRecentSkips();
+
+ expect($skips)->toHaveCount(1);
+ expect($skips->first()['type'])->toBe('backup');
+ expect($skips->first()['reason'])->toBe('server_not_functional');
+ expect($skips->first()['team_id'])->toBe(5);
+
+ // Cleanup
+ @unlink($logPath);
+});
+
+test('scheduler log parser filters by team id', function () {
+ $logPath = storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log');
+ $logDir = dirname($logPath);
+ if (! is_dir($logDir)) {
+ mkdir($logDir, 0755, true);
+ }
+
+ $lines = [
+ '['.now()->format('Y-m-d H:i:s').'] production.INFO: Backup skipped {"type":"backup","skip_reason":"server_not_functional","team_id":1}',
+ '['.now()->format('Y-m-d H:i:s').'] production.INFO: Backup skipped {"type":"backup","skip_reason":"subscription_unpaid","team_id":2}',
+ ];
+ file_put_contents($logPath, implode("\n", $lines)."\n");
+
+ $parser = new SchedulerLogParser;
+
+ $allSkips = $parser->getRecentSkips(100);
+ expect($allSkips)->toHaveCount(2);
+
+ $team1Skips = $parser->getRecentSkips(100, 1);
+ expect($team1Skips)->toHaveCount(1);
+ expect($team1Skips->first()['team_id'])->toBe(1);
+
+ // Cleanup
+ @unlink($logPath);
+});
diff --git a/tests/Feature/ScheduledTaskApiTest.php b/tests/Feature/ScheduledTaskApiTest.php
new file mode 100644
index 000000000..fbd6e383e
--- /dev/null
+++ b/tests/Feature/ScheduledTaskApiTest.php
@@ -0,0 +1,365 @@
+team = Team::factory()->create();
+ $this->user = User::factory()->create();
+ $this->team->members()->attach($this->user->id, ['role' => 'owner']);
+
+ session(['currentTeam' => $this->team]);
+
+ $this->token = $this->user->createToken('test-token', ['*']);
+ $this->bearerToken = $this->token->plainTextToken;
+
+ $this->server = Server::factory()->create(['team_id' => $this->team->id]);
+ $this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]);
+ $this->project = Project::factory()->create(['team_id' => $this->team->id]);
+ $this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
+});
+
+function authHeaders($bearerToken): array
+{
+ return [
+ 'Authorization' => 'Bearer '.$bearerToken,
+ 'Content-Type' => 'application/json',
+ ];
+}
+
+describe('GET /api/v1/applications/{uuid}/scheduled-tasks', function () {
+ test('returns empty array when no tasks exist', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks");
+
+ $response->assertStatus(200);
+ $response->assertJson([]);
+ });
+
+ test('returns tasks when they exist', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ ScheduledTask::factory()->create([
+ 'application_id' => $application->id,
+ 'team_id' => $this->team->id,
+ 'name' => 'Test Task',
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks");
+
+ $response->assertStatus(200);
+ $response->assertJsonCount(1);
+ $response->assertJsonFragment(['name' => 'Test Task']);
+ });
+
+ test('returns 404 for unknown application uuid', function () {
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson('/api/v1/applications/nonexistent-uuid/scheduled-tasks');
+
+ $response->assertStatus(404);
+ });
+});
+
+describe('POST /api/v1/applications/{uuid}/scheduled-tasks', function () {
+ test('creates a task with valid data', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
+ 'name' => 'Backup',
+ 'command' => 'php artisan backup',
+ 'frequency' => '0 0 * * *',
+ ]);
+
+ $response->assertStatus(201);
+ $response->assertJsonFragment(['name' => 'Backup']);
+
+ $this->assertDatabaseHas('scheduled_tasks', [
+ 'name' => 'Backup',
+ 'command' => 'php artisan backup',
+ 'frequency' => '0 0 * * *',
+ 'application_id' => $application->id,
+ 'team_id' => $this->team->id,
+ ]);
+ });
+
+ test('returns 422 when name is missing', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
+ 'command' => 'echo test',
+ 'frequency' => '* * * * *',
+ ]);
+
+ $response->assertStatus(422);
+ });
+
+ test('returns 422 for invalid cron expression', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
+ 'name' => 'Test',
+ 'command' => 'echo test',
+ 'frequency' => 'not-a-cron',
+ ]);
+
+ $response->assertStatus(422);
+ $response->assertJsonPath('errors.frequency.0', 'Invalid cron expression or frequency format.');
+ });
+
+ test('returns 422 when extra fields are present', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
+ 'name' => 'Test',
+ 'command' => 'echo test',
+ 'frequency' => '* * * * *',
+ 'unknown_field' => 'value',
+ ]);
+
+ $response->assertStatus(422);
+ });
+
+ test('defaults timeout and enabled when not provided', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
+ 'name' => 'Test',
+ 'command' => 'echo test',
+ 'frequency' => '* * * * *',
+ ]);
+
+ $response->assertStatus(201);
+
+ $this->assertDatabaseHas('scheduled_tasks', [
+ 'name' => 'Test',
+ 'timeout' => 300,
+ 'enabled' => true,
+ ]);
+ });
+});
+
+describe('PATCH /api/v1/applications/{uuid}/scheduled-tasks/{task_uuid}', function () {
+ test('updates task with partial data', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $task = ScheduledTask::factory()->create([
+ 'application_id' => $application->id,
+ 'team_id' => $this->team->id,
+ 'name' => 'Old Name',
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->patchJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/{$task->uuid}", [
+ 'name' => 'New Name',
+ ]);
+
+ $response->assertStatus(200);
+ $response->assertJsonFragment(['name' => 'New Name']);
+ });
+
+ test('returns 404 when task not found', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->patchJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/nonexistent", [
+ 'name' => 'Test',
+ ]);
+
+ $response->assertStatus(404);
+ });
+});
+
+describe('DELETE /api/v1/applications/{uuid}/scheduled-tasks/{task_uuid}', function () {
+ test('deletes task successfully', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $task = ScheduledTask::factory()->create([
+ 'application_id' => $application->id,
+ 'team_id' => $this->team->id,
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->deleteJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/{$task->uuid}");
+
+ $response->assertStatus(200);
+ $response->assertJson(['message' => 'Scheduled task deleted.']);
+
+ $this->assertDatabaseMissing('scheduled_tasks', ['uuid' => $task->uuid]);
+ });
+
+ test('returns 404 when task not found', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->deleteJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/nonexistent");
+
+ $response->assertStatus(404);
+ });
+});
+
+describe('GET /api/v1/applications/{uuid}/scheduled-tasks/{task_uuid}/executions', function () {
+ test('returns executions for a task', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $task = ScheduledTask::factory()->create([
+ 'application_id' => $application->id,
+ 'team_id' => $this->team->id,
+ ]);
+
+ ScheduledTaskExecution::create([
+ 'scheduled_task_id' => $task->id,
+ 'status' => 'success',
+ 'message' => 'OK',
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/{$task->uuid}/executions");
+
+ $response->assertStatus(200);
+ $response->assertJsonCount(1);
+ $response->assertJsonFragment(['status' => 'success']);
+ });
+
+ test('returns 404 when task not found', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/nonexistent/executions");
+
+ $response->assertStatus(404);
+ });
+});
+
+describe('Service scheduled tasks API', function () {
+ test('can list tasks for a service', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ ScheduledTask::factory()->create([
+ 'service_id' => $service->id,
+ 'team_id' => $this->team->id,
+ 'name' => 'Service Task',
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson("/api/v1/services/{$service->uuid}/scheduled-tasks");
+
+ $response->assertStatus(200);
+ $response->assertJsonCount(1);
+ $response->assertJsonFragment(['name' => 'Service Task']);
+ });
+
+ test('can create a task for a service', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/services/{$service->uuid}/scheduled-tasks", [
+ 'name' => 'Service Backup',
+ 'command' => 'pg_dump',
+ 'frequency' => '0 2 * * *',
+ ]);
+
+ $response->assertStatus(201);
+ $response->assertJsonFragment(['name' => 'Service Backup']);
+ });
+
+ test('can delete a task for a service', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ $task = ScheduledTask::factory()->create([
+ 'service_id' => $service->id,
+ 'team_id' => $this->team->id,
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->deleteJson("/api/v1/services/{$service->uuid}/scheduled-tasks/{$task->uuid}");
+
+ $response->assertStatus(200);
+ $response->assertJson(['message' => 'Scheduled task deleted.']);
+ });
+});
diff --git a/tests/Feature/ServerIpUniquenessTest.php b/tests/Feature/ServerIpUniquenessTest.php
new file mode 100644
index 000000000..705b1eddc
--- /dev/null
+++ b/tests/Feature/ServerIpUniquenessTest.php
@@ -0,0 +1,110 @@
+user = User::factory()->create();
+ $this->team = Team::factory()->create();
+ $this->user->teams()->attach($this->team, ['role' => 'owner']);
+ $this->actingAs($this->user);
+ session(['currentTeam' => $this->team]);
+
+ $this->privateKey = PrivateKey::create([
+ 'name' => 'Test Key',
+ 'private_key' => 'test-key-content',
+ 'team_id' => $this->team->id,
+ ]);
+});
+
+it('detects duplicate ip within the same team', function () {
+ Server::factory()->create([
+ 'ip' => '1.2.3.4',
+ 'team_id' => $this->team->id,
+ 'private_key_id' => $this->privateKey->id,
+ ]);
+
+ $foundServer = Server::whereIp('1.2.3.4')->first();
+
+ expect($foundServer)->not->toBeNull();
+ expect($foundServer->team_id)->toBe($this->team->id);
+});
+
+it('detects duplicate ip from another team', function () {
+ $otherTeam = Team::factory()->create();
+
+ Server::factory()->create([
+ 'ip' => '5.6.7.8',
+ 'team_id' => $otherTeam->id,
+ ]);
+
+ $foundServer = Server::whereIp('5.6.7.8')->first();
+
+ expect($foundServer)->not->toBeNull();
+ expect($foundServer->team_id)->not->toBe($this->team->id);
+});
+
+it('shows correct error message for same team duplicate in boarding', function () {
+ Server::factory()->create([
+ 'ip' => '1.2.3.4',
+ 'team_id' => $this->team->id,
+ 'private_key_id' => $this->privateKey->id,
+ ]);
+
+ $foundServer = Server::whereIp('1.2.3.4')->first();
+ if ($foundServer->team_id === currentTeam()->id) {
+ $message = 'A server with this IP/Domain already exists in your team.';
+ } else {
+ $message = 'A server with this IP/Domain is already in use by another team.';
+ }
+
+ expect($message)->toBe('A server with this IP/Domain already exists in your team.');
+});
+
+it('shows correct error message for other team duplicate in boarding', function () {
+ $otherTeam = Team::factory()->create();
+
+ Server::factory()->create([
+ 'ip' => '5.6.7.8',
+ 'team_id' => $otherTeam->id,
+ ]);
+
+ $foundServer = Server::whereIp('5.6.7.8')->first();
+ if ($foundServer->team_id === currentTeam()->id) {
+ $message = 'A server with this IP/Domain already exists in your team.';
+ } else {
+ $message = 'A server with this IP/Domain is already in use by another team.';
+ }
+
+ expect($message)->toBe('A server with this IP/Domain is already in use by another team.');
+});
+
+it('allows adding ip that does not exist globally', function () {
+ $foundServer = Server::whereIp('10.20.30.40')->first();
+
+ expect($foundServer)->toBeNull();
+});
+
+it('enforces global uniqueness not just team-scoped', function () {
+ $otherTeam = Team::factory()->create();
+
+ Server::factory()->create([
+ 'ip' => '9.8.7.6',
+ 'team_id' => $otherTeam->id,
+ ]);
+
+ // Global check finds the server even though it belongs to another team
+ $foundServer = Server::whereIp('9.8.7.6')->first();
+ expect($foundServer)->not->toBeNull();
+
+ // Team-scoped check would miss it - this is why global check is needed
+ $teamScopedServer = Server::where('team_id', $this->team->id)
+ ->where('ip', '9.8.7.6')
+ ->first();
+ expect($teamScopedServer)->toBeNull();
+});
diff --git a/tests/Feature/StartDatabaseProxyTest.php b/tests/Feature/StartDatabaseProxyTest.php
new file mode 100644
index 000000000..c62569866
--- /dev/null
+++ b/tests/Feature/StartDatabaseProxyTest.php
@@ -0,0 +1,45 @@
+create();
+
+ $database = StandalonePostgresql::factory()->create([
+ 'team_id' => $team->id,
+ 'is_public' => true,
+ 'public_port' => 5432,
+ ]);
+
+ expect($database->is_public)->toBeTrue();
+
+ $action = new StartDatabaseProxy;
+
+ // Use reflection to test the private method directly
+ $method = new ReflectionMethod($action, 'isNonTransientError');
+
+ expect($method->invoke($action, 'Bind for 0.0.0.0:5432 failed: port is already allocated'))->toBeTrue();
+ expect($method->invoke($action, 'address already in use'))->toBeTrue();
+ expect($method->invoke($action, 'some other error'))->toBeFalse();
+});
+
+test('isNonTransientError detects port conflict patterns', function () {
+ $action = new StartDatabaseProxy;
+ $method = new ReflectionMethod($action, 'isNonTransientError');
+
+ expect($method->invoke($action, 'Bind for 0.0.0.0:5432 failed: port is already allocated'))->toBeTrue()
+ ->and($method->invoke($action, 'address already in use'))->toBeTrue()
+ ->and($method->invoke($action, 'Bind for 0.0.0.0:3306 failed: port is already allocated'))->toBeTrue()
+ ->and($method->invoke($action, 'network timeout'))->toBeFalse()
+ ->and($method->invoke($action, 'connection refused'))->toBeFalse();
+});
diff --git a/tests/Pest.php b/tests/Pest.php
index 619dea153..cec77b86f 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -13,7 +13,7 @@ use Illuminate\Support\Once;
| need to change it using the "uses()" function to bind a different classes or traits.
|
*/
-uses(Tests\TestCase::class)->in('Feature');
+uses(Tests\TestCase::class)->in('Feature', 'v4/Feature', 'v4/Browser');
/*
|--------------------------------------------------------------------------
diff --git a/tests/v4/Browser/DashboardTest.php b/tests/v4/Browser/DashboardTest.php
new file mode 100644
index 000000000..b4a97f268
--- /dev/null
+++ b/tests/v4/Browser/DashboardTest.php
@@ -0,0 +1,162 @@
+ 0]);
+
+ $this->user = User::factory()->create([
+ 'id' => 0,
+ 'name' => 'Root User',
+ 'email' => 'test@example.com',
+ 'password' => Hash::make('password'),
+ ]);
+
+ PrivateKey::create([
+ 'id' => 1,
+ 'uuid' => 'ssh-test',
+ 'team_id' => 0,
+ 'name' => 'Test Key',
+ 'description' => 'Test SSH key',
+ 'private_key' => '-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
+hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
+AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
+uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
+-----END OPENSSH PRIVATE KEY-----',
+ ]);
+
+ Server::create([
+ 'id' => 0,
+ 'uuid' => 'localhost',
+ 'name' => 'localhost',
+ 'description' => 'This is a test docker container in development mode',
+ 'ip' => 'coolify-testing-host',
+ 'team_id' => 0,
+ 'private_key_id' => 1,
+ 'proxy' => [
+ 'type' => ProxyTypes::TRAEFIK->value,
+ 'status' => ProxyStatus::EXITED->value,
+ ],
+ ]);
+
+ Server::create([
+ 'uuid' => 'production-1',
+ 'name' => 'production-web',
+ 'description' => 'Production web server cluster',
+ 'ip' => '10.0.0.1',
+ 'team_id' => 0,
+ 'private_key_id' => 1,
+ 'proxy' => [
+ 'type' => ProxyTypes::TRAEFIK->value,
+ 'status' => ProxyStatus::EXITED->value,
+ ],
+ ]);
+
+ Server::create([
+ 'uuid' => 'staging-1',
+ 'name' => 'staging-server',
+ 'description' => 'Staging environment server',
+ 'ip' => '10.0.0.2',
+ 'team_id' => 0,
+ 'private_key_id' => 1,
+ 'proxy' => [
+ 'type' => ProxyTypes::TRAEFIK->value,
+ 'status' => ProxyStatus::EXITED->value,
+ ],
+ ]);
+
+ Project::create([
+ 'uuid' => 'project-1',
+ 'name' => 'My first project',
+ 'description' => 'This is a test project in development',
+ 'team_id' => 0,
+ ]);
+
+ Project::create([
+ 'uuid' => 'project-2',
+ 'name' => 'Production API',
+ 'description' => 'Backend services for production',
+ 'team_id' => 0,
+ ]);
+
+ Project::create([
+ 'uuid' => 'project-3',
+ 'name' => 'Staging Environment',
+ 'description' => 'Staging and QA testing',
+ 'team_id' => 0,
+ ]);
+});
+
+function loginAndSkipOnboarding(): mixed
+{
+ return visit('/login')
+ ->fill('email', 'test@example.com')
+ ->fill('password', 'password')
+ ->click('Login')
+ ->click('Skip Setup');
+}
+
+it('redirects to login when not authenticated', function () {
+ $page = visit('/');
+
+ $page->assertPathIs('/login')
+ ->screenshot();
+});
+
+it('shows onboarding after first login', function () {
+ $page = visit('/login');
+
+ $page->fill('email', 'test@example.com')
+ ->fill('password', 'password')
+ ->click('Login')
+ ->assertSee('Welcome to Coolify')
+ ->assertSee("Let's go!")
+ ->assertSee('Skip Setup')
+ ->screenshot();
+});
+
+it('shows dashboard after skipping onboarding', function () {
+ $page = loginAndSkipOnboarding();
+
+ $page->assertSee('Dashboard')
+ ->assertSee('Your self-hosted infrastructure.')
+ ->screenshot();
+});
+
+it('shows all projects on dashboard', function () {
+ $page = loginAndSkipOnboarding();
+
+ $page->assertSee('Projects')
+ ->assertSee('My first project')
+ ->assertSee('This is a test project in development')
+ ->assertSee('Production API')
+ ->assertSee('Backend services for production')
+ ->assertSee('Staging Environment')
+ ->assertSee('Staging and QA testing')
+ ->screenshot();
+});
+
+it('shows servers on dashboard', function () {
+ $page = loginAndSkipOnboarding();
+
+ $page->assertSee('Servers')
+ ->assertSee('localhost')
+ ->assertSee('This is a test docker container in development mode')
+ ->assertSee('production-web')
+ ->assertSee('Production web server cluster')
+ ->assertSee('staging-server')
+ ->assertSee('Staging environment server')
+ ->screenshot();
+});
diff --git a/tests/v4/Browser/LoginTest.php b/tests/v4/Browser/LoginTest.php
new file mode 100644
index 000000000..7666e07e2
--- /dev/null
+++ b/tests/v4/Browser/LoginTest.php
@@ -0,0 +1,52 @@
+ 0]);
+});
+
+it('shows registration page when no users exist', function () {
+ $page = visit('/login');
+
+ $page->assertSee('Root User Setup')
+ ->assertSee('Create Account')
+ ->screenshot();
+});
+
+it('can login with valid credentials', function () {
+ User::factory()->create([
+ 'id' => 0,
+ 'email' => 'test@example.com',
+ 'password' => Hash::make('password'),
+ ]);
+
+ $page = visit('/login');
+
+ $page->fill('email', 'test@example.com')
+ ->fill('password', 'password')
+ ->click('Login')
+ ->assertSee('Welcome to Coolify')
+ ->screenshot();
+});
+
+it('fails login with invalid credentials', function () {
+ User::factory()->create([
+ 'id' => 0,
+ 'email' => 'test@example.com',
+ 'password' => Hash::make('password'),
+ ]);
+
+ $page = visit('/login');
+
+ $page->fill('email', 'random@email.com')
+ ->fill('password', 'wrongpassword123')
+ ->click('Login')
+ ->assertSee('These credentials do not match our records')
+ ->screenshot();
+});
diff --git a/tests/v4/Browser/RegistrationTest.php b/tests/v4/Browser/RegistrationTest.php
new file mode 100644
index 000000000..e2a232357
--- /dev/null
+++ b/tests/v4/Browser/RegistrationTest.php
@@ -0,0 +1,67 @@
+ 0]);
+});
+
+it('shows registration page when no users exist', function () {
+ $page = visit('/register');
+
+ $page->assertSee('Root User Setup')
+ ->assertSee('Create Account')
+ ->screenshot();
+});
+
+it('can register a new root user', function () {
+ $page = visit('/register');
+
+ $page->fill('name', 'Test User')
+ ->fill('email', 'root@example.com')
+ ->fill('password', 'Password1!@')
+ ->fill('password_confirmation', 'Password1!@')
+ ->click('Create Account')
+ ->assertPathIs('/onboarding')
+ ->screenshot();
+
+ expect(User::where('email', 'root@example.com')->exists())->toBeTrue();
+});
+
+it('fails registration with mismatched passwords', function () {
+ $page = visit('/register');
+
+ $page->fill('name', 'Test User')
+ ->fill('email', 'root@example.com')
+ ->fill('password', 'Password1!@')
+ ->fill('password_confirmation', 'DifferentPass1!@')
+ ->click('Create Account')
+ ->assertSee('password')
+ ->screenshot();
+});
+
+it('fails registration with weak password', function () {
+ $page = visit('/register');
+
+ $page->fill('name', 'Test User')
+ ->fill('email', 'root@example.com')
+ ->fill('password', 'short')
+ ->fill('password_confirmation', 'short')
+ ->click('Create Account')
+ ->assertSee('password')
+ ->screenshot();
+});
+
+it('shows login link when a user already exists', function () {
+ User::factory()->create(['id' => 0]);
+
+ $page = visit('/register');
+
+ $page->assertSee('Already registered?')
+ ->assertDontSee('Root User Setup')
+ ->screenshot();
+});
diff --git a/tests/v4/Feature/SqliteDatabaseTest.php b/tests/v4/Feature/SqliteDatabaseTest.php
new file mode 100644
index 000000000..d2df5c0a6
--- /dev/null
+++ b/tests/v4/Feature/SqliteDatabaseTest.php
@@ -0,0 +1,18 @@
+getDriverName())->toBe('sqlite');
+});
+
+it('runs migrations successfully', function () {
+ expect(Schema::hasTable('users'))->toBeTrue();
+ expect(Schema::hasTable('teams'))->toBeTrue();
+ expect(Schema::hasTable('servers'))->toBeTrue();
+ expect(Schema::hasTable('applications'))->toBeTrue();
+});