956 Commits
Author SHA1 Message Date
Andras Bacsai 0bf7719491 feat(cloudflared): add tunnel updates with background container restart
Add an authorized update button with activity logs to pull the latest image and recreate the tunnel container in the background.

Persist new Compose files under /data/coolify/cloudflared and support existing /tmp configurations. Cover updates, missing configuration, and member restrictions with tests.
2026-10-07 14:54:17 +02:00
Andras Bacsai 88ea322e1c fix(cloudflared): allow more time for tunnel startup 2026-10-07 14:32:21 +02:00
Andras Bacsai 81045b2dc2 fix(compose): deploy large Compose files such as Sentry self-hosted
Sentry self-hosted (getsentry/self-hosted 26.8.0) could not be deployed
as a Docker Compose application. Three separate problems blocked it:

- YAML collection aliases: the first fix raised the limit to 256 only in
  Application::oldRawParser(). The safety check added in v4.4.0
  (validateDockerComposeForInjection) and all other Compose parse calls
  still used the Symfony default of 128, so the Sentry file (133 aliases)
  failed with "Maximum number of collection aliases (128) exceeded".
  Add parseDockerComposeYaml() and use it for every Compose parse.
- Argument list too long: the deployment job sent the generated Compose
  file as base64 inside the SSH command. Linux limits one argument to
  128 KiB, and the Sentry file is about 370 KB. execute_remote_command()
  now accepts an 'input' key that streams content over SSH stdin, and
  the job writes the Compose file with it.
- A list-style build arg without a value (build.args: [SENTRY_IMAGE])
  merged into a map environment kept its numeric key, so Docker Compose
  rejected the file with "non-string key in ...environment: 0". It now
  becomes NAME with no value, as in Compose.

Closes #11725
2026-10-07 13:30:42 +02:00
Andras Bacsai 575bfaa085 feat(analytics): add configurable client IP storage modes
Allow full, anonymized, or disabled IP recording per server through the UI
and API, and pass the selected mode to Sentinel. Adapt traffic breakdowns
to the effective IP mode, including views that aggregate multiple servers.
2026-10-07 12:35:22 +02:00
Andras Bacsai 6cc6f1081d fix(deployments): advance the queue on all servers after a cancel
- A queued deployment on a deleted server no longer stops the queue;
  one deployment that fails to start does not stop the others
- Cancelling a deployment starts the queued deployments of the same
  application on all servers, as a finished deployment does
2026-10-05 23:10:52 +02:00
Andras Bacsai 7f12e3b513 fix(databases): keep the server password of existing Redis databases
v4.3.23 started Redis with the stored REDIS_PASSWORD value as it is and
unquoted, and the connection URLs used the same value. 4.4.0 resolved a
shared variable reference for the start command only, so after the first
restart the server password changed and no longer matched the URLs.

- Add legacy_password_quoting to standalone_redis; existing databases
  keep the v4.3.23 server password
- New databases resolve REDIS_PASSWORD (local, shared or remote secret)
  and quote it in the start command
- Connection URLs use the same password as the server
- Hide the flag in the API, as for KeyDB and Dragonfly
2026-10-05 22:49:36 +02:00
Andras Bacsai b08383805c fix(sentinel): prevent concurrent starts and duplicate restarts (#12100) 2026-10-05 21:56:17 +02:00
Andras Bacsai 152e06cf43 fix(databases): let REDIS_PASSWORD override the KeyDB and Dragonfly password
The stored password is the default. Before, a local REDIS_PASSWORD was
rewritten to the stored password, and a shared one replaced the stored
password, so the old password was lost.

- A local, shared, or remote secret REDIS_PASSWORD sets the server
  password; the stored password and the variable are not changed
- Removing the variable, or an empty value, uses the stored password
- Connection URLs show the password in use; for a remote secret they
  show the reference, not the secret
- Databases created before this release keep the v4.3.23 behavior
2026-10-05 21:25:57 +02:00
Andras Bacsai 99d4071f2c fix(service): read compose up and pull input from /dev/null
The service start script goes to the server over stdin. When the
configuration of an existing volume changed, docker compose asked to
recreate the volume and read the next script line as the answer. That
line, for example the proxy network connect, did not run.

- Add < /dev/null to the service compose up and pull commands
- Do not use --yes, because it recreates the volume and deletes data
2026-10-05 21:20:19 +02:00
Andras Bacsai 88efc0716e fix: enforce resource team isolation and preserve ClickHouse data
Scope resource permissions and related records to the owning team across
team switches, cloning, deployments, and resource operations.

Merge ClickHouse volume mounts, preserve anonymous data volumes on stop,
and require adopting current data before starting or restarting.

Fix file storage deletion checks and add regression coverage.
2026-10-05 14:23:04 +02:00
Andras Bacsai 4227ec6ebf fix(storage): require deploy permission for external host writes
Require deploy or root token access when creating managed mounts outside
resource directories or changing their file contents through the API.

Synchronize KeyDB and Dragonfly passwords with local and shared
REDIS_PASSWORD values while preserving secret manager resolution.
2026-10-05 12:03:37 +02:00
Andras Bacsai 554bd354bf fix: support host storage paths and clean up cancelled deployments
Allow validated host file and directory mounts outside resource directories
while protecting external paths from deletion.

Clean up cancelled deployment containers and clear tracked process IDs.
Preserve legacy KeyDB and Dragonfly password parsing, accept server backup
paths without extension restrictions, and allow Compose variable proxy ports.
2026-10-05 11:08:52 +02:00
Andras Bacsai 774e6bf178 feat(account): add self-service deletion and cancellation flow (#12088) 2026-10-05 09:17:10 +02:00
Andras BacsaiandClaude Opus 5.5 80b8b219a2 fix(queues): run service restarts and proxy start/restart on the deployments queue
- On cloud, RestartService and RestartServiceApplication now run on the
  deployments queue instead of high.
- StartProxy and RestartProxyJob, and the remote processes they start,
  now run on the deployments queue.
- Self-hosted still uses the high queue.
- Tests cover the new routing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 00:11:12 +02:00
Andras Bacsai 3575234b38 fix(docker-cleanup): clean after a resource stop only when the disk needs it
A stop or delete no longer queues a full cleanup each time. It skips the
cleanup when one finished on the server in the last hour, and the queued
run only cleans when disk usage is at or above the server cleanup
threshold. The docker_cleanup API flag keeps its default of true.
2026-10-03 21:25:17 +02:00
Andras Bacsai 7956be8833 fix(docker-cleanup): run one bounded cleanup per server on the maintenance queue
Scheduled, manual and stop-triggered cleanups share one per-server lock.
Stop-triggered cleanups wait 60 seconds and are coalesced per server, so
a bulk stop queues one cleanup. Remote commands share a 540 second
deadline below the 600 second job timeout, and application images are
scanned with a fixed number of remote commands.
2026-10-03 21:25:17 +02:00
Andras Bacsai c681dc0d0e fix(queue): coalesce queued container status and helper cleanup jobs per server
Queued GetContainersStatus runs are unique per server until a worker
starts them, and CleanupHelperContainersJob locks per server instead of
class-wide. Add UniqueUntilProcessingJobDecorator for laravel-actions.
2026-10-03 21:25:00 +02:00
Andras Bacsai e144b990c3 feat(horizon): add fixed-size per-queue worker pools on Coolify Cloud
Cloud production now runs one supervisor per queue (deployments, crons,
high, default, maintenance), so a busy queue cannot starve the others.
Add maintenance_queue() for slow Docker cleanups and a shared
horizon.worker_timeout config value. Self-hosted keeps the s6 supervisor.
2026-10-03 21:24:54 +02:00
Andras Bacsai c6b8dfe367 fix(status): store the status of a database or service right after it starts
After a start, the UI showed "exited" until the regular status check ran.
That check runs on the shared high queue, which can be busy (60 s on Cloud).

The start job (database) and the start task (service, on the deployment
queue) now inspect only the started containers and store their status
before they broadcast. While a health check reports "starting", they
inspect again for up to about 15 s.

ContainerStatusAggregator now builds the per-container status and the
Compose aggregation for both writers, so GetContainersStatus and the
targeted check store the same strings.
2026-10-03 21:16:44 +02:00
Andras Bacsai f1f7272494 fix(queue): run service and database start commands on the deployment queue
On cloud, StartService and DeployServiceApplication dispatched their
CoolifyTask on the high queue, so service starts competed with all other
high jobs. remote_process() and CoolifyTask now accept a queue (default
high); start paths pass deployment_queue(). The queued RestartDatabase
action also runs on deployment_queue() instead of the default queue.
2026-10-03 20:50:49 +02:00
Andras BacsaiandClaude Opus 5.5 2e2e43fddc fix(services): finish service cleanup when a volume cannot be removed
- Try to remove every volume and always delete the configuration
  directory (it holds the .env with secrets), then report the volumes that
  failed.
- Delete volume backup schedules also when a service is removed from
  Coolify only.
- The delete API no longer claims the server is unreachable when the
  caller asked to delete from Coolify only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:00:52 +02:00
Andras BacsaiandClaude Opus 5.5 b98e810e3f fix(proxy): keep Compose merge tags and remove the log rotation sidecar
- Parse and dump the proxy Compose file with custom tags, so `!reset` and
  `!override` survive the traffic analytics update and `{}`/`[]` keep
  their type.
- Remove the Traefik log rotation sidecar when the proxy stops or another
  proxy type starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:00:51 +02:00
Andras BacsaiandClaude Opus 5.5 9978929779 fix(databases): keep the server password of existing KeyDB and Dragonfly databases
The start command now quotes the password. Databases created before this
release keep the unquoted v4.3.23 argument when quoting would change the
password the server receives, until their password changes. Remote secret
passwords are always quoted exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:00:41 +02:00
Andras BacsaiandClaude Opus 5.5 9bdbbeef7a fix(secret-manager): use remote secret values exactly and check them before a restart
- Write remote secret values as exact dotenv literals in the service .env
  file and escape "$" where they go directly into a generated compose file
  (environment, start commands, healthchecks).
- Resolve remote secrets before a service or database restart stops the
  running resource, and show the secret manager error instead of a generic
  start failure (new RemoteSecretException).
- Cache fetched key names for the autocomplete and rate limit how often a
  user can contact the secret manager.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:00:41 +02:00
Andras BacsaiandClaude Opus 5.5 1ee34a5ab2 fix(database-import): stop the restore when the import task fails or loses SSH
An import task can now run until the SSH command timeout (capped below the
Horizon worker timeout). When the CoolifyTask fails, times out, or its SSH
command ends with 124/255, Coolify stops the restore in the database
container and keeps the import in progress until the stop has run, so the
database stays blocked. The restore reports its own 124/255 as 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:00:29 +02:00
Andras Bacsai 51e6949c36 fix(sentinel): stop hourly check job pile-up and self-heal Sentinel
- Start Sentinel with --restart unless-stopped so Docker restarts it
  after a crash or reboot.
- Make CheckAndStartSentinelJob unique per server so copies cannot
  collect in a slow queue.
- Read X-Sentinel-Version on push and update an outdated Sentinel
  without SSH; skip the hourly SSH version check for servers that
  report their version.
2026-10-02 09:41:04 +02:00
Andras Bacsai 50be16be8f revert(scheduler): go back to the cron scan and keep the reliability fixes
The missed daily backups on Coolify Cloud came from Redis evicting queue keys
(allkeys-lfu on a full instance), not from the scan of all schedules. The
next_run_at design (452259aac, 5c5740533) did not prevent that, and it added
stored state that must be kept in sync on every frequency, timezone, and server
change. It was not released, so remove it:
- revert next_run_at, its model hooks, the "Next run" UI, and the diagnostics
  changes; restore scheduled_job_states and the queue overlap lock
- a new migration removes the next_run_at columns and creates
  scheduled_job_states again where the removed migration already ran

Keep the changes that fix real problems:
- run the manager every minute from the scheduler with `scheduled:dispatch`,
  so a busy queue cannot delay it past the catch-up window
- load the server chain of scheduled backups with each chunk, which removes
  about 7,000 queries per run on Coolify Cloud
- catch Throwable for each schedule, so one broken schedule does not stop the
  others
- publish database and volume backups again when their queued job was not
  started after 60 minutes (for example, lost in Redis); log lost tasks and
  Docker cleanups as missed. A lost backup is not run again when a newer
  occurrence of the same schedule exists.
2026-10-02 08:05:18 +02:00
Andras Bacsai 658ac6bca5 fix(database): check MongoDB health with ping
The MongoDB health check passed only on the writable primary
(db.hello().isWritablePrimary or isMaster().ismaster). A replica set
that was never initiated, which is common for Prisma setups, and any
non-primary member were reported unhealthy after a restart.

The check now runs db.adminCommand({ ping: 1 }) with mongosh or the
legacy mongo shell. Ping needs no authentication, passes on every
running member, and still fails when mongod is not reachable.
2026-10-01 07:16:27 +02:00
Andras Bacsai 7ef7fee59a fix(proxy): accept Docker Compose merge tags in proxy configurations
Proxy port validation parsed the configuration with plain Symfony YAML,
which rejects Docker Compose tags such as !reset and !override. Proxy
start, restart and save then failed for these valid configurations, and
automatic start after a reboot stopped silently. v4.3.23 did not block
on such configurations.

The parser now reads custom tags and validates the values inside them,
so malformed ports are still rejected. CheckProxy refreshes the status
of a running proxy before it reads the configuration, as in v4.3.23, so
an unreadable configuration no longer hides a running proxy.
2026-10-01 07:16:26 +02:00
Andras Bacsai b13fa4dad0 fix(database): choose the MongoDB health check shell at run time
The health check from #11730 picked mongosh or the legacy mongo shell
from the image name (mongo:4*). Images without mongosh whose name does
not start with mongo:4, such as mongo:3.6 or registry mirrors of
mongo:4.4, ran a missing mongosh (exit 127) and stayed unhealthy.

The check now uses mongosh when the container has it and falls back
to the legacy mongo shell otherwise.

Replace the test that only checked the source text with tests that run
the generated script with fake shells.
2026-09-30 20:48:54 +02:00
Andras Bacsai 0604101781 Merge pull request #11730 from cbratschi/codex/fix-mongodb-healthcheck
fix(database): use MongoDB writable-primary health check
2026-09-30 20:45:48 +02:00
Andras Bacsai 5a2006e99c perf(docker): look up foreign preview owners once per container status check
GetContainersStatus now resolves all unknown preview owners with one query,
like the Sentinel push. The lookup helper also no longer fails on an
application without a uuid.
2026-09-30 19:23:35 +02:00
Andras Bacsai a2cd6351b2 fix(databases): keep SSL certificate files and CA in sync with each engine
- Regenerate SSL Certificates now writes server.crt/server.key for every
  engine except MongoDB, which keeps the combined server.pem. Before, the
  button always wrote server.pem, so Postgres, MySQL, MariaDB, Redis, KeyDB
  and Dragonfly failed to start after a regenerate.
- The certificate renewal job uses the same rule, so MongoDB no longer
  switches to server.crt/server.key on renewal and fails on restart.
- Starting an SSL database writes the CA that signs its certificate to
  /data/coolify/ssl/coolify-ca.crt, so a stale CA file (for example one
  written by another server entry for the same host) no longer causes
  "certificate signature failure".
- Saving a custom CA certificate that does not match the stored CA
  private key is rejected.
- The CA file write commands live in SslHelper::caCertificateFileCommands().

Fixes #10388
Fixes #6771
Fixes #11926
2026-09-30 19:01:11 +02:00
Andras Bacsai 8b007edceb fix(database): skip MySQL system databases when restoring all databases
An all-databases MySQL/MariaDB restore also restored the mysql system
database, so after the next restart the root and app passwords were the
source ones and the MariaDB health check failed. The restore now skips mysql,
sys, performance_schema and information_schema by default. The new option
"Restore users and privileges" (restore_mysql_users in the API) keeps the
old behaviour.
2026-09-30 18:59:49 +02:00
Andras Bacsai 4f8bf46feb fix(auth): remove the unused Fortify profile route that bypassed the SSO email lock
PUT /user/profile-information changed the email with no SSO check and no
verification. The profile page has its own verified email change, so the
Fortify feature, its action and its binding are removed.
2026-09-30 18:10:18 +02:00
Andras Bacsai 41eb524d3e fix(dns): compare IPv6 addresses by value 2026-09-30 18:10:10 +02:00
Andras Bacsai 487e86c493 fix(service): delete a service part even when its container removal fails 2026-09-30 18:10:10 +02:00
Andras Bacsai ad4d1d8e5d fix(database): fail expired queued starts instead of running them late
A queued start or restart whose reservation expired no longer runs. Failed
queued starts are recorded as failed activities and release the reservation.
2026-09-30 18:10:10 +02:00
Andras Bacsai ef90fe06f1 feat(proxy): back up acme.json before changes and allow restoring backups
Coolify copies acme.json to a timestamped backup before every change and
keeps the last 10. The proxy page lists the backups with Restore and Delete
for team admins, and the restore dialog can restart the proxy right away.
2026-09-30 18:10:10 +02:00
Andras Bacsai 9e7cb60b0d feat: add import restore options, OAuth confirmation, and fixes
- database import: add keep_owners (restore PostgreSQL owners and
  privileges) and sqlite_database (pick the target SQLite file) to the
  API, OpenAPI spec, and import form
- oauth: require password confirmation and support team selection on
  OAuth login
- remote process: send team broadcast events to the team of the server
  that ran the process instead of the causer's user id
- servers API: handle the legacy build server setting
- compose: support parent bind paths and volume driver options
- dns: improve record hints and provider management
- notifications: fix throttling and missing backup notifications
- ui: rework the confirmation modal
- add feature and unit tests for the new behavior
2026-09-30 13:35:22 +02:00
Andras BacsaiandClaude Opus 5.5 fd85b740f5 feat(github-runners): add ephemeral self-hosted runners on build servers (#12054)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:13:11 +02:00
Andras Bacsai 2b5bffe37d fix(service): delete resources from Coolify when server is unreachable
DeleteResourceJob no longer restores a service and rethrows when remote
cleanup fails. The resource is always removed from Coolify, and the team
gets a notification that Docker resources may remain on the server. No
notification is sent when the server itself was already deleted.

removeSubresourceContainer now returns false instead of throwing when the
server is not functional. Deleting a service application or database then
shows a warning that the container is removed on the next service start.

StopService handles a missing destination server, environment, project,
or team without throwing. The danger zone warning now shows for any
resource whose server is unreachable, not only services.
2026-09-30 08:02:02 +02:00
Andras Bacsai 2ac5969d90 fix(docker): stop matching containers by numeric resource id labels
Numeric coolify.{type}Id labels can collide with resources on another
Coolify instance managing the same server, so they no longer select or
resolve container owners. Drop the legacyId fallback from container
lookups in DeleteService and ComplexStatusCheck.

Legacy application containers without a UUID label are now resolved by
their compose service name, which starts with the application UUID.
2026-09-29 23:05:34 +02:00
Andras Bacsai a599c9e8b7 fix(server): keep data directory commands working for non-root SSH users
A non-root SSH user cannot open redirects, expand globs, or cd into
/data/coolify (9999:root 0700). Several remote commands relied on that
and failed on non-root servers.

- Write README files with `tee` instead of shell redirects in database
  starts and application deployments
- Clear Postgres init scripts with `find` instead of a glob
- Split the proxy config backfill if/else so each branch gets sudo
- Start the log drain with `docker compose -f` instead of `cd`
- Wrap the preserved-repository compose start in `sh -c` for non-root
- Stage clone archives outside /data/coolify via StagesCloneArchives
- Extend the sudo parser and add non-root command tests
2026-09-29 20:46:54 +02:00
Andras Bacsai 8b8022bac4 fix(proxy): run proxy commands without cd for non-root SSH users
The proxy directory is 9999:root 0700 on the Coolify host, so a
non-root SSH user cannot cd into it, open redirects in it, or expand
globs there (#4255).

- Start/restart proxy with absolute `docker compose -f` and
  `docker stack deploy -c` paths instead of `cd $proxy_path`
- Write the Caddyfile via `tee` instead of a shell redirect
- Check for and prune config backups with `find` instead of globs
- Read the config size with `stat -c %s` instead of `wc -c <`
- Add feature tests for non-root proxy commands and record the lesson
2026-09-29 19:16:50 +02:00
Andras BacsaiandClaude Opus 5.5 cbee84353e refactor(events): broadcast team events to an explicit team
Move the shared team id and team channel logic of 15 broadcast events
into a BroadcastsToTeam trait. Events no longer fall back to the session
team: the session team can differ from the resource team, and queued
jobs have no session.

Fix StopDatabaseProxy, which dispatched DatabaseProxyStopped without a
team, so the event was never broadcast from queued jobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:46:16 +02:00
Andras Bacsai 452259aacc feat(scheduler): dispatch scheduled jobs by their stored next run time
The scheduled job manager checked every enabled schedule each minute and
evaluated its cron expression in PHP. On Coolify Cloud one run took more than
60 seconds, so runs overlapped and minutes were skipped.

Each schedule now stores its next due time:
- next_run_at on scheduled database backups, scheduled tasks, and volume
  backups, and docker_cleanup_next_run_at on server settings (UTC, indexed)
- model hooks calculate it when a schedule is created or its frequency or
  enabled flag changes; a server timezone change or a move to another server
  calculates it again for the affected schedules

The `scheduled:dispatch` command runs the manager every minute from the
scheduler, not from a queue. It selects only due rows and claims each
occurrence with an atomic update of next_run_at, together with its delivery
record, so parallel runs on several nodes never dispatch it twice.
- database and volume backups run once when they are late; tasks and Docker
  cleanups more than 10 minutes late are logged as missed
- a local time that occurs two times when the clocks go back runs once
- the migration continues each schedule after its last recorded occurrence and
  drops scheduled_job_states

Show the next run, in the server timezone, on the database backup, scheduled
task, volume backup, and Docker cleanup pages, or "Invalid frequency" when the
frequency cannot be calculated. scheduled:diagnostics now lists overdue
schedules and open deliveries.

Remove workarounds that are no longer needed: the queue overlap lock and its
stale-lock repair, the catch-up window, and debug-only logs.
2026-09-29 10:07:23 +02:00
Andras Bacsai af983a29d8 refactor(docker): identify containers by resource UUID instead of numeric id
Container ownership labels now use UUIDs: coolify.applicationUuid,
coolify.serviceUuid, coolify.service.subUuid and coolify.databaseUuid.
Numeric ids differ between Coolify instances, so after a server transfer
the target could not find existing containers: redeploys left the old
container running and service statuses were wrong.

Containers are matched by UUID label first, then by the compose project
(Swarm: stack namespace), which is the resource UUID since July 2024, and
last by the older numeric id label. Existing containers keep working
without a redeploy and get the UUID labels on the next deploy or restart.
2026-09-29 09:57:56 +02:00
Andras BacsaiandClaude Opus 5.5 7bd8baa7f0 fix(multi-server): make applications on several servers consistent
- Stop the application on every server, also when one server is not functional.
- Write file mounts on every server of the application, not only on the main server.
- Deploy the exact commit, rollback flag and image tag of the main deployment to the
  additional servers, so a rollback reverts all servers.
- Pull the pushed image on additional servers for inline Dockerfile applications
  instead of building it again on each server.
- Fail multi-server deployments without a Docker image name, also from webhooks and the API.
- Send one deployment notification after all servers finish (parent_deployment_uuid).
- Block persistent volumes on multi-server applications (API, Livewire, SQLite connect)
  and warn in the deployment log for existing applications that have both.
- Remove dead code in push_to_docker_registry().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:38:57 +02:00
Andras BacsaiandClaude Opus 5.5 f540c49f68 fix(security): encrypt stored remote commands and remove them after the final attempt
remote_process() stored the full shell command as plain text in
activity_log and kept it for 60 days. Database starts store the compose file
with the database passwords, imports store S3 keys, and the Cloudflare
tunnel setup stores its token.

- Store the command encrypted. The queue worker decrypts it right before the
  SSH call and still reads plain commands of tasks queued before the upgrade.
- Remove the command when the task is finished: after a successful run, in
  failed() when no attempt is left, and for a stopped import. A failed attempt
  that the queue retries keeps it.
- Do not write a command preview into the error log.
- Migration: remove stored commands from activities that no longer run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:41:56 +02:00