$this->detailedPayload($this->resolveActiveBuild($request)), ]); } /** * Show the Docker image build history page. */ public function history(Request $request): Response { return Inertia::render('docker/BuildHistory', [ 'builds' => $request->user() ->dockerImageBuilds() ->latest('id') ->limit(25) ->get() ->map(fn (DockerImageBuild $dockerImageBuild): array => $this->summaryPayload($dockerImageBuild)) ->all(), ]); } /** * Return the latest persisted status for a specific build. */ public function show(Request $request, DockerImageBuild $dockerImageBuild): JsonResponse { return response()->json([ 'build' => $this->detailedPayload( $this->ownedBuild($request, $dockerImageBuild)->fresh(), ), ]); } /** * Queue a Docker image build from the submitted Dockerfile contents. */ public function store(DockerImageBuildRequest $request): RedirectResponse { $validated = $request->validated(); $environment = $this->normalizeEnvironment($validated['environment'] ?? []); $dockerImageBuild = DB::transaction(function () use ($request, $validated, $environment): DockerImageBuild { $dockerImageBuild = $request->user()->dockerImageBuilds()->create([ 'image_name' => $validated['image_name'], 'image_tag' => $validated['image_tag'], 'dockerfile_content' => $validated['dockerfile'], 'environment' => $environment, 'status' => DockerImageBuild::STATUS_QUEUED, 'queued_at' => now(), ]); RunDockerImageBuild::dispatch($dockerImageBuild->id)->afterCommit(); return $dockerImageBuild; }); Inertia::flash('toast', [ 'type' => 'success', 'message' => __('Docker build queued. Logs will update automatically while the queue worker runs.'), ]); return to_route('docker-builder.index', [ 'build' => $dockerImageBuild->id, ]); } /** * Resolve the currently selected build for the authenticated user. * * @throws NotFoundHttpException */ protected function resolveActiveBuild(Request $request): ?DockerImageBuild { $buildId = $request->integer('build'); if ($buildId > 0) { return $request->user() ->dockerImageBuilds() ->findOrFail($buildId); } return $request->user() ->dockerImageBuilds() ->latest('id') ->first(); } /** * Ensure the given build belongs to the authenticated user. */ protected function ownedBuild(Request $request, DockerImageBuild $dockerImageBuild): DockerImageBuild { abort_unless( $dockerImageBuild->user_id === $request->user()->getAuthIdentifier(), 404, ); return $dockerImageBuild; } /** * Normalize environment rows before they are saved. * * @param array $environment * @return array */ protected function normalizeEnvironment(array $environment): array { return collect($environment) ->map(fn (array $variable): array => [ 'key' => trim($variable['key'] ?? ''), 'value' => $variable['value'] ?? '', ]) ->filter(fn (array $variable): bool => $variable['key'] !== '' || $variable['value'] !== '') ->values() ->all(); } /** * Transform a build into the detailed payload used by the builder page. */ protected function detailedPayload(?DockerImageBuild $dockerImageBuild): ?array { if ($dockerImageBuild === null) { return null; } return [ 'id' => $dockerImageBuild->id, 'image' => $dockerImageBuild->image(), 'image_name' => $dockerImageBuild->image_name, 'image_tag' => $dockerImageBuild->image_tag, 'dockerfile_content' => $dockerImageBuild->dockerfile_content, 'environment' => $dockerImageBuild->environment ?? [], 'environment_count' => count($dockerImageBuild->environment ?? []), 'status' => $dockerImageBuild->status, 'status_label' => Str::headline($dockerImageBuild->status), 'successful' => $dockerImageBuild->successful, 'exit_code' => $dockerImageBuild->exit_code, 'build_output' => $dockerImageBuild->build_output ?? '', 'queued_at' => $dockerImageBuild->queued_at?->toIso8601String(), 'queued_at_human' => $dockerImageBuild->queued_at?->diffForHumans(), 'started_at' => $dockerImageBuild->started_at?->toIso8601String(), 'started_at_human' => $dockerImageBuild->started_at?->diffForHumans(), 'finished_at' => $dockerImageBuild->finished_at?->toIso8601String(), 'finished_at_human' => $dockerImageBuild->finished_at?->diffForHumans(), 'duration' => $this->durationFor( $dockerImageBuild->started_at, $dockerImageBuild->finished_at, ), ]; } /** * Transform a build into the summary payload used by the history page. */ protected function summaryPayload(DockerImageBuild $dockerImageBuild): array { $output = trim((string) $dockerImageBuild->build_output); $dockerfilePreview = collect(preg_split('/\R/', trim($dockerImageBuild->dockerfile_content)) ?: []) ->take(4) ->implode(PHP_EOL); return [ 'id' => $dockerImageBuild->id, 'image' => $dockerImageBuild->image(), 'status' => $dockerImageBuild->status, 'status_label' => Str::headline($dockerImageBuild->status), 'successful' => $dockerImageBuild->successful, 'exit_code' => $dockerImageBuild->exit_code, 'environment_count' => count($dockerImageBuild->environment ?? []), 'queued_at_human' => $dockerImageBuild->queued_at?->diffForHumans(), 'duration' => $this->durationFor( $dockerImageBuild->started_at, $dockerImageBuild->finished_at, ), 'dockerfile_preview' => $dockerfilePreview !== '' ? $dockerfilePreview : 'Dockerfile was empty.', 'output_preview' => $output !== '' ? Str::limit($output, 220) : null, ]; } /** * Render a short duration string for build timing metadata. */ protected function durationFor(?CarbonInterface $startedAt, ?CarbonInterface $finishedAt): ?string { if ($startedAt === null) { return null; } $totalSeconds = $startedAt->diffInSeconds($finishedAt ?? now()); $hours = intdiv($totalSeconds, 3600); $minutes = intdiv($totalSeconds % 3600, 60); $seconds = $totalSeconds % 60; $parts = collect([ $hours > 0 ? "{$hours}h" : null, $minutes > 0 ? "{$minutes}m" : null, $hours === 0 ? "{$seconds}s" : null, ])->filter()->take(2); return $parts->isNotEmpty() ? $parts->implode(' ') : '0s'; } }