query( "CREATE TABLE IF NOT EXISTS cron_task_state ( task_id VARCHAR(191) NOT NULL PRIMARY KEY, module VARCHAR(64) NOT NULL, enabled TINYINT(1) NOT NULL DEFAULT 1, schedule_json LONGTEXT NULL, last_run_at DATETIME NULL, next_run_at DATETIME NULL, locked_until DATETIME NULL, lock_owner VARCHAR(191) NULL, current_run_id BIGINT UNSIGNED NULL, last_status VARCHAR(32) NULL, last_error TEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, KEY idx_cron_task_state_next_run (enabled, next_run_at), KEY idx_cron_task_state_lock (locked_until) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); $db->query( "CREATE TABLE IF NOT EXISTS cron_task_runs ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, task_id VARCHAR(191) NOT NULL, module VARCHAR(64) NOT NULL, source VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'running', actor_user_id INT NULL, scheduled_for DATETIME NULL, started_at DATETIME NULL, completed_at DATETIME NULL, duration_ms INT UNSIGNED NULL, summary_json LONGTEXT NULL, error_message TEXT NULL, lock_owner VARCHAR(191) NULL, force_run TINYINT(1) NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, KEY idx_cron_task_runs_task_created (task_id, created_at), KEY idx_cron_task_runs_status_created (status, created_at), KEY idx_cron_task_runs_module_created (module, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); self::ensureColumn('cron_task_runs', 'force_run', 'TINYINT(1) NOT NULL DEFAULT 0 AFTER lock_owner'); $db->query( "CREATE TABLE IF NOT EXISTS cron_worker_state ( worker_id VARCHAR(191) NOT NULL PRIMARY KEY, name VARCHAR(191) NOT NULL, hostname VARCHAR(191) NULL, pid INT UNSIGNED NULL, source VARCHAR(64) NOT NULL DEFAULT 'coolify_worker', status VARCHAR(32) NOT NULL DEFAULT 'starting', release_channel_id BIGINT UNSIGNED NULL, release_target_id BIGINT UNSIGNED NULL, coolify_resource_uuid VARCHAR(128) NULL, coolify_resource_type VARCHAR(32) NULL, commit_sha VARCHAR(64) NULL, poll_seconds INT UNSIGNED NOT NULL DEFAULT 15, last_run_count INT UNSIGNED NOT NULL DEFAULT 0, last_stale_run_count INT UNSIGNED NOT NULL DEFAULT 0, last_error TEXT NULL, started_at DATETIME NULL, last_heartbeat_at DATETIME NULL, last_loop_started_at DATETIME NULL, last_loop_finished_at DATETIME NULL, last_loop_gap_seconds INT UNSIGNED NULL, consecutive_minute_loops INT UNSIGNED NOT NULL DEFAULT 0, stopped_at DATETIME NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, KEY idx_cron_worker_state_heartbeat (last_heartbeat_at), KEY idx_cron_worker_state_status (status), KEY idx_cron_worker_state_release_target (release_target_id), KEY idx_cron_worker_state_coolify_resource (coolify_resource_uuid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); self::ensureColumn('cron_worker_state', 'last_loop_gap_seconds', 'INT UNSIGNED NULL AFTER last_loop_finished_at'); self::ensureColumn('cron_worker_state', 'consecutive_minute_loops', 'INT UNSIGNED NOT NULL DEFAULT 0 AFTER last_loop_gap_seconds'); self::$initialized = true; } private static function ensureColumn(string $table, string $column, string $definition): void { global $db; $table = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $column = preg_replace('/[^a-zA-Z0-9_]/', '', $column); if ($table === '' || $column === '') { return; } $result = $db->query("SHOW COLUMNS FROM `{$table}` LIKE '{$column}'"); if ($result && $result->num_rows > 0) { return; } $db->query("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$definition}"); } }