import type { CiArtifactRow, CiStepRow, RepositoryRow, } from "../../db/index.ts"; import { formatDateTime } from "../../lib/formatDate.ts"; import type { SessionUser } from "../../middleware/session.ts"; import { Layout } from "../layout.tsx"; import { RepoHeader } from "../repos/RepoHeader.tsx"; import { RepoNav } from "../repos/RepoNav.tsx"; import { CiStatusPill } from "./CiStatusPill.tsx"; interface RunDetail { id: number; repo_run_id: number | null; status: string; trigger_source: string; commit_sha: string | null; commit_branch: string | null; commit_tag: string | null; variable_overrides: string | null; started_at: string | null; finished_at: string | null; created_at: string; triggered_by_username: string | null; } interface CiRunDetailProps { user: SessionUser | null; repo: RepositoryRow; run: RunDetail; steps: CiStepRow[]; artifacts: CiArtifactRow[]; autoRefresh: boolean; queuePosition: number | null; } function queueTitle(position: number | null): string { if (position === null) return "Waiting in the build queue"; if (position === 1) return "Waiting in the build queue — next up"; return `Waiting in the build queue — ${position - 1} run${position - 1 === 1 ? "" : "s"} ahead`; } function duration(start: string | null, end: string | null): string { if (!start || !end) return ""; const ms = new Date(end).getTime() - new Date(start).getTime(); if (ms < 0) return ""; const s = Math.floor(ms / 1000); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); const rem = s % 60; return rem > 0 ? `${m}m ${rem}s` : `${m}m`; } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } export function CiRunDetail({ user, repo, run, steps, artifacts, autoRefresh, queuePosition, }: CiRunDetailProps) { const isActive = run.status === "pending" || run.status === "running" || run.status === "queued"; const isQueued = run.status === "queued"; const queueText = queueTitle(queuePosition); const displayId = run.repo_run_id ?? run.id; let variableOverrides: Record = {}; if (run.variable_overrides) { try { variableOverrides = JSON.parse(run.variable_overrides); } catch { // corrupted DB value — treat as empty } } const hasOverrides = Object.keys(variableOverrides).length > 0; return ( {isActive && autoRefresh ? ( ) : null}

{" "} Pipeline #{displayId}

{!!run.commit_sha && ( {run.commit_sha.slice(0, 8)} )} {!!run.commit_branch && ( {run.commit_branch} )} {!!run.commit_tag && ( {run.commit_tag} )} triggered by {run.trigger_source} {!!run.triggered_by_username && ` (${run.triggered_by_username})`} {!!run.started_at && !!run.finished_at && ( {duration(run.started_at, run.finished_at)} )}
{isActive && ( {autoRefresh ? "Pause refresh" : "Resume refresh"} )} {user?.isAdmin && (isActive ? (
) : (
))}
{hasOverrides && (

Variable overrides

{Object.entries(variableOverrides).map(([k, v]) => ( <>
{k}
{v}
))}
)}

Steps

{steps.length === 0 ? (
{isQueued ? queueText : "Waiting to start…"}
) : ( steps.map((step) => (
{step.name} {!!step.started_at && !!step.finished_at && ( {duration( step.started_at, step.finished_at, )} )} {step.log ? (
                                        {step.log}
                                    
) : step.status === "running" ? (
Running…
) : null}
)) )}
{artifacts.length > 0 && (

Artifacts

)}
); }