Login
4 branches 0 tags
Ben (T14/NixOS) Improved flake 41eb128 11 days ago 252 Commits
rubhub / frontend / ci.ts
interface CiStepResultResponse {
	name: string;
	status: string;
	exit_code: number | null;
}

interface CiJobResultResponse {
	name: string;
	status: string;
	exit_code: number | null;
	steps: CiStepResultResponse[];
	log: string;
}

interface CiRunStatusResponse {
	id: string;
	status: string;
	jobs: CiJobResultResponse[];
}

export function initCiLogPolling() {
	const container = document.getElementById("ci-jobs-container");
	if (!container) return;

	const statusUrl = container.dataset.statusUrl;
	const initialStatus = container.dataset.initialStatus;
	if (!statusUrl) return;

	// Don't poll if already finished - but still do one fetch to load logs
	const isFinished = initialStatus === "success" || initialStatus === "failed";

	async function fetchAndUpdate(): Promise<boolean> {
		try {
			const res = await fetch(statusUrl);
			if (!res.ok) return true; // Stop polling on error

			const data: CiRunStatusResponse = await res.json();

			// Update overall status badge
			const statusBadge = document.getElementById("run-status");
			if (statusBadge) {
				statusBadge.textContent = data.status;
				statusBadge.className = `issue-status status-${data.status}`;
			}

			// Update each job's logs and status
			for (const job of data.jobs) {
				const jobEl = container?.querySelector(`[data-job-name="${job.name}"]`);
				if (!jobEl) continue;

				// Update job status
				const statusEl = jobEl.querySelector(".ci-job-status");
				if (statusEl) {
					statusEl.textContent = job.status;
					statusEl.className = `ci-job-status status-${job.status}`;
				}

				// Update step statuses
				for (const step of job.steps) {
					const stepEl = jobEl.querySelector(`[data-step-name="${step.name}"]`);
					if (!stepEl) continue;

					const stepStatusEl = stepEl.querySelector(".ci-step-status");
					if (stepStatusEl) {
						stepStatusEl.className = `ci-step-status status-${step.status}`;
					}

					// Update or add exit code
					let exitEl = stepEl.querySelector(".ci-step-exit");
					if (step.exit_code !== null) {
						if (!exitEl) {
							exitEl = document.createElement("span");
							exitEl.className = "ci-step-exit";
							stepEl.appendChild(exitEl);
						}
						exitEl.textContent = `(${step.exit_code})`;
						exitEl.className = `ci-step-exit ${step.exit_code === 0 ? "ci-exit-success" : "ci-exit-failure"}`;
					}
				}

				// Update log with auto-scroll
				const logEl = jobEl.querySelector<HTMLElement>(".ci-log");
				if (logEl) {
					// Check if user is scrolled to bottom before updating
					const isAtBottom =
						logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 5;

					logEl.textContent = job.log || "(no output)";

					// If they were at bottom, keep them there
					if (isAtBottom) {
						logEl.scrollTop = logEl.scrollHeight;
					}
				}
			}

			// Return true if we should stop polling (finished)
			return data.status === "success" || data.status === "failed";
		} catch {
			// Stop polling on exception - user can refresh if needed
			return true;
		}
	}

	async function poll() {
		const shouldStop = await fetchAndUpdate();
		if (!shouldStop) {
			// Schedule next poll after 1 second
			// Using setTimeout instead of setInterval ensures we don't pile up
			// requests on slow connections
			setTimeout(poll, 1000);
		}
	}

	if (isFinished) {
		// Just load logs once, don't poll
		fetchAndUpdate();
	} else {
		// Start polling
		poll();
	}
}