0, 'rows' => []]; } $search = trim((string) ($filters['search'] ?? '')); $status = strtolower(trim((string) ($filters['status'] ?? ''))); $triggerType = strtolower(trim((string) ($filters['trigger_type'] ?? ''))); [$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 200, 0); [$order, $dir] = RepoQuery::sanitizeOrder( $filters, ['id', 'started_at', 'finished_at', 'duration_ms', 'status', 'trigger_type'], 'started_at', 'desc' ); $where = ['scheduled_job_runs.job_id = ?']; $params = [(string) $jobId]; RepoQuery::addLikeFilter( $where, $params, ['scheduled_job_runs.run_uuid', 'scheduled_job_runs.error_code', 'scheduled_job_runs.error_message'], $search ); if (in_array($status, ['success', 'failed', 'skipped'], true)) { $where[] = 'scheduled_job_runs.status = ?'; $params[] = $status; } if (in_array($triggerType, ['scheduler', 'manual'], true)) { $where[] = 'scheduled_job_runs.trigger_type = ?'; $params[] = $triggerType; } $whereSql = ' where ' . implode(' and ', $where); $fromSql = ' from scheduled_job_runs left join users on users.id = scheduled_job_runs.actor_user_id '; $total = (int) (DB::selectValue('select count(*)' . $fromSql . $whereSql, ...$params) ?? 0); $rows = DB::select( 'select scheduled_job_runs.id, scheduled_job_runs.run_uuid, scheduled_job_runs.job_id, scheduled_job_runs.job_key, scheduled_job_runs.trigger_type, scheduled_job_runs.status, scheduled_job_runs.actor_user_id, scheduled_job_runs.started_at, scheduled_job_runs.finished_at, scheduled_job_runs.duration_ms, scheduled_job_runs.error_code, scheduled_job_runs.error_message, scheduled_job_runs.result_json, scheduled_job_runs.created_at, users.id, users.uuid, users.display_name, users.email ' . $fromSql . $whereSql . sprintf(' order by scheduled_job_runs.`%s` %s limit ? offset ?', $order, $dir), ...array_merge($params, [(string) $limit, (string) $offset]) ); $normalized = []; if (is_array($rows)) { foreach ($rows as $row) { $item = $this->normalizeRow($row); if ($item !== null) { $normalized[] = $item; } } } return ['total' => $total, 'rows' => $normalized]; } public function purgeOlderThanDays(int $days): int { if ($days <= 0) { return 0; } $cutoff = (new \DateTimeImmutable('now', new \DateTimeZone('UTC'))) ->modify('-' . $days . ' days') ->format('Y-m-d H:i:s'); $deleted = DB::delete('delete from scheduled_job_runs where created_at < ?', $cutoff); return is_int($deleted) ? $deleted : 0; } private function normalizeRow(mixed $row): ?array { if (!is_array($row)) { return null; } $item = $row['scheduled_job_runs'] ?? []; if (!is_array($item) || !isset($item['id'])) { return null; } $user = is_array($row['users'] ?? null) ? $row['users'] : []; $item['actor_user_uuid'] = (string) ($user['uuid'] ?? ''); $item['actor_user_display_name'] = (string) ($user['display_name'] ?? ''); $item['actor_user_email'] = (string) ($user['email'] ?? ''); return $item; } }