import { BuildDownloadButton } from "@/components/BuildDownloadButton" import { Button } from "@/components/ui/button" import { getImplicitDependencies, humanizeStatus } from "@/lib/utils" import { useMutation, useQuery } from "convex/react" import { AlertCircle, ArrowLeft, CheckCircle, Loader2, Share2, XCircle } from "lucide-react" import { useState } from "react" import { toast } from "sonner" import { usePageContext } from "vike-react/usePageContext" import { navigate } from "vike/client/router" import { TARGETS } from "../../../constants/targets" import { api } from "../../../convex/_generated/api" import { ArtifactType } from "../../../convex/builds" import modulesData from "../../../convex/modules.json" import registryData from "../../../public/registry.json" export default function BuildProgress() { const pageContext = usePageContext() const buildHash = pageContext.routeParams?.buildHash as string | undefined const build = useQuery(api.builds.getByHash, buildHash ? { buildHash } : "skip") const isAdmin = useQuery(api.admin.isAdmin) const retryBuild = useMutation(api.admin.retryBuild) const [shareUrlCopied, setShareUrlCopied] = useState(false) if (!buildHash) { return (

Build hash missing.{" "} Start a new build .

) } if (build === undefined) { return (
) } if (!build) { return (
Back to Quick Build

No build found for hash {buildHash}

) } const targetMeta = build.config.target ? TARGETS[build.config.target] : undefined const targetLabel = targetMeta?.name ?? build.config.target const status = build.status || "queued" const getStatusIcon = () => { if (status === "success") { return } if (status === "failure") { return } return } const getStatusColor = () => { if (status === "success") return "text-green-400" if (status === "failure") return "text-red-400" return "text-blue-400" } const githubActionUrl = build.githubRunId && build.githubRunId > 0 ? `https://github.com/MeshEnvy/mesh-forge/actions/runs/${build.githubRunId}` : null const shareUrl = `${window.location.origin}/builds/new/${build.buildHash}` const handleShare = async () => { try { await navigator.clipboard.writeText(shareUrl) setShareUrlCopied(true) toast.success("Share link copied to clipboard") setTimeout(() => setShareUrlCopied(false), 2000) } catch { toast.error("Failed to copy link", { description: "Please copy the URL manually", }) } } // Compute build flags from config (same logic as computeFlagsFromConfig in convex/builds.ts) const computeFlagsFromConfig = (config: typeof build.config): string => { return Object.keys(config.modulesExcluded) .sort() .filter(module => config.modulesExcluded[module]) .map((moduleExcludedName: string) => `-D${moduleExcludedName}=1`) .join(" ") } // Generate GitHub discussion URL with prefilled body const generateDiscussionUrl = (): string => { const flags = computeFlagsFromConfig(build.config) const plugins = build.config.pluginsEnabled?.join(", ") || "(none)" const timestamp = new Date(build.startedAt).toISOString() const githubRunLink = githubActionUrl ? `[View run](${githubActionUrl})` : "(not available)" const buildPageUrl = `${window.location.origin}/builds/${build.buildHash}` // Format plugins as +plugin@version const formattedPlugins = build.config.pluginsEnabled ?.map(plugin => { // Plugin might be "slug@version" or just "slug" return plugin.includes("@") ? `+${plugin}` : `+${plugin}` }) .join(" ") || "" const bracketContent = [ build.config.target, `v${build.config.version}`, ...(formattedPlugins ? [formattedPlugins] : []), ].join(" ") const discussionTitle = `Build ${build.status === "failure" ? "Failed" : "Issue"}: ${targetLabel} [${bracketContent}]` const discussionBody = `## Build ${build.status === "failure" ? "Failed" : "Information"} **Build Hash**: \`${build.buildHash}\` **Target Board**: ${build.config.target} **Firmware Version**: ${build.config.version} **Build Flags**: ${flags || "(none)"} **Plugins**: ${plugins} **Build Timestamp**: ${timestamp} **Build Page**: [View build page](${buildPageUrl}) **GitHub Run**: ${githubRunLink} ## Additional Information (Please add any additional details about the issue here)` const baseUrl = "https://github.com/MeshEnvy/mesh-forge/discussions/new" const params = new URLSearchParams({ category: "q-a", title: discussionTitle, body: discussionBody, }) return `${baseUrl}?${params.toString()}` } const handleReportIssue = () => { window.open(generateDiscussionUrl(), "_blank", "noopener,noreferrer") } const handleRetry = async () => { if (!build?._id) return try { await retryBuild({ buildId: build._id }) toast.success("Build retry initiated", { description: "The build has been queued with the latest YAML.", }) } catch (error) { toast.error("Failed to retry build", { description: String(error), }) } } const getStatusBadge = (status: string) => { const statusConfig = { success: { bg: "bg-green-500/20", text: "text-green-400", label: "Success", }, failure: { bg: "bg-red-500/20", text: "text-red-400", label: "Failed" }, queued: { bg: "bg-yellow-500/20", text: "text-yellow-400", label: "Queued", }, } const config = statusConfig[status as keyof typeof statusConfig] || { bg: "bg-slate-500/20", text: "text-slate-400", label: status, } return {config.label} } const formatDate = (timestamp: number) => { return new Date(timestamp).toLocaleString() } // Get excluded modules const excludedModules = modulesData.modules.filter(module => build.config.modulesExcluded[module.id] === true) // Get explicitly selected plugins from stored config // The stored config only contains explicitly selected plugins (not resolved dependencies) const explicitPluginSlugs = (build.config.pluginsEnabled || []).map(pluginId => { // Extract slug from "slug@version" format if present return pluginId.includes("@") ? pluginId.split("@")[0] : pluginId }) // Get implicit dependencies (dependencies that are not explicitly selected) const implicitDeps = getImplicitDependencies( explicitPluginSlugs, registryData as Record }> ) // Separate explicit and implicit plugins const explicitPlugins: Array<{ id: string name: string description: string version: string }> = [] const implicitPlugins: Array<{ id: string name: string description: string version: string }> = [] // Process explicitly selected plugins ;(build.config.pluginsEnabled || []).forEach(pluginId => { // Extract slug from "slug@version" format if present const slug = pluginId.includes("@") ? pluginId.split("@")[0] : pluginId const pluginData = (registryData as Record)[slug] const pluginInfo = { id: slug, name: pluginData?.name || slug, description: pluginData?.description || "", version: pluginId.includes("@") ? pluginId.split("@")[1] : pluginData?.version || "", } explicitPlugins.push(pluginInfo) }) // Process implicit dependencies (resolved but not in stored config) for (const slug of implicitDeps) { const pluginData = (registryData as Record)[slug] if (pluginData) { implicitPlugins.push({ id: slug, name: pluginData.name || slug, description: pluginData.description || "", version: pluginData.version || "", }) } } return (
{getStatusIcon()}

Target

{targetLabel}

{humanizeStatus(status)} {new Date(build.updatedAt).toLocaleString()} {githubActionUrl && ( <> View run )}
{/* Admin Controls Section */} {isAdmin === true && build && (

Admin Controls

{build.buildHash.substring(0, 8)} {getStatusBadge(build.status)}
{/* Build Configuration Details */}
Target
{build.config.target}
Version
{build.config.version}
{build.completedAt ? "Completed" : "Started"}
{build.completedAt ? formatDate(build.completedAt) : build.startedAt ? formatDate(build.startedAt) : "Unknown"}
{/* Run History Section */} {(build.githubRunId || (build.githubRunIdHistory?.length ?? 0) > 0) && (
Run History {(build.githubRunIdHistory?.length ?? 0) > 0 && ` (${(build.githubRunIdHistory?.length ?? 0) + (build.githubRunId ? 1 : 0)} total)`}
{build.githubRunId && ( {build.githubRunId} )} {build.githubRunIdHistory?.map(id => ( {id} ))}
)}
)} {status !== "success" && status !== "failure" && (

Builds run in GitHub Actions. When the status is success, your firmware artifact will be ready to download.

)} {/* Build Configuration Summary */} {(excludedModules.length > 0 || explicitPlugins.length > 0 || implicitPlugins.length > 0) && (
{/* Excluded Modules */} {excludedModules.length > 0 && (

Excluded Modules

{excludedModules.map(module => (

{module.name}

{module.description}

))}
)} {/* Enabled Plugins */} {explicitPlugins.length > 0 && (

Enabled Plugins

{explicitPlugins.map(plugin => (

{plugin.name}

{plugin.version && v{plugin.version}}
{plugin.description &&

{plugin.description}

}
))}
)} {/* Required Plugins (Implicit Dependencies) */} {implicitPlugins.length > 0 && (

Required Plugins

{implicitPlugins.map(plugin => (

{plugin.name}

{plugin.version && v{plugin.version}}
{plugin.description &&

{plugin.description}

}
))}
)}
)} {status === "success" && build.buildHash && ( )} {build.buildHash && ( )} {status === "failure" && (

Build failed. Please try tweaking your configuration or re-running the build.

)} {status !== "success" && status !== "failure" && (

This build is still running. Leave this tab open or come back later using the URL above.

)}
) }