false, 'error' => 'id required'], 400); } $build = BuildRepository::getById($id); if (!$build) { json_out(['ok' => false, 'error' => 'not found'], 404); } $logPath = $build['log_path'] ?? null; if ($download) { if (!$logPath || !is_file($logPath)) { json_out(['ok' => false, 'error' => 'log not found'], 404); } $code = preg_replace('/[^a-zA-Z0-9._-]+/', '_', (string) ($build['build_code'] ?? 'build')); header('Content-Type: text/plain; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $code . '.log"'); readfile($logPath); exit; } $tail = $full ? BuildRunner::readLogFull($logPath) : BuildRunner::readLogTail($logPath, $offset); if ($build['status'] === 'running' && $tail['eof'] && is_file(BuildRunner::artifactDir($id) . '/BUILD_INFO.json')) { BuildRunner::finalizeFromArtifacts($id); $build = BuildRepository::getById($id); } json_out([ 'ok' => true, 'build' => $build, 'log' => $tail, ]);