import { BuildDownloadButton } from "@/components/BuildDownloadButton" import { Button } from "@/components/ui/button" import { TARGETS } from "@/constants/targets" import type { Doc } from "@/convex/_generated/dataModel" import { ArtifactType, getArtifactFilenameBase } from "@/convex/lib/filename" import { computeFlagsFromConfig } from "@/convex/lib/flags" import modulesData from "@/convex/modules.json" import { getImplicitDependencies, humanizeStatus } from "@/lib/utils" import registryData from "@/public/registry.json" import { CheckCircle, Copy, Loader2, Share2, X, XCircle } from "lucide-react" import { useState } from "react" import { toast } from "sonner" import { navigate } from "vike/client/router" interface BuildProgressProps { build: Doc<"builds"> isAdmin?: boolean onRetry?: (buildId: Doc<"builds">["_id"]) => Promise showActions?: boolean } export function BuildProgress({ build, isAdmin = false, onRetry, showActions = true }: BuildProgressProps) { const [shareUrlCopied, setShareUrlCopied] = useState(false) const [bashCopied, setBashCopied] = useState(false) const [showBashModal, setShowBashModal] = useState(false) 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?hash=${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", }) } } const generateBashCommand = (): string => { const flags = computeFlagsFromConfig( build.config, registryData as Record }> ) const target = build.config.target const version = build.config.version const plugins = build.config.pluginsEnabled || [] const commands = [] // Generate directory name matching the download filename format (without .tar.gz extension) const dirName = getArtifactFilenameBase(version, target, build.buildHash, build.githubRunId, "source") // Clone firmware repository into named directory commands.push(`git clone --recursive https://github.com/meshtastic/firmware.git ${dirName}`) commands.push(`cd ${dirName}`) // Checkout the specific version commands.push(`git checkout ${version}`) // Update submodules after checkout commands.push(`git submodule update --init --recursive`) // Install PlatformIO if not already installed commands.push(`pip install platformio`) // Install mesh-plugin-manager commands.push(`pip install mesh-plugin-manager`) // Initialize mpm commands.push(`mpm init`) // Install plugins if any if (plugins.length > 0) { const pluginSlugs = plugins.map(plugin => { // Extract slug from "slug@version" format if present return plugin.includes("@") ? plugin.split("@")[0] : plugin }) commands.push(`mpm install ${pluginSlugs.join(" ")}`) } // Set build flags and build // Always export PLATFORMIO_BUILD_FLAGS (even if empty) so users can see what was used commands.push(`export PLATFORMIO_BUILD_FLAGS="${flags || ""}"`) commands.push(`pio run -e ${target}`) return commands.join("\n") } const handleOpenBashModal = () => { setBashCopied(false) setShowBashModal(true) } const handleCopyBashFromModal = async () => { try { const bashCommand = generateBashCommand() await navigator.clipboard.writeText(bashCommand) setBashCopied(true) toast.success("Bash command copied to clipboard") setTimeout(() => setBashCopied(false), 2000) } catch { toast.error("Failed to copy command", { description: "Please copy the command manually", }) } } const handleRetry = async () => { if (!build?._id || !onRetry) return try { await onRetry(build._id) } catch (error) { toast.error("Failed to retry build", { description: String(error), }) } } // 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 (
{/* Header Section */}

{getStatusIcon()} { e.preventDefault() navigate(`/builds?hash=${build.buildHash}`) }} className="hover:text-cyan-400 transition-colors" > {targetLabel} {status !== "success" && status !== "failure" && ( )}

{build.config.target} v{build.config.version} {build.completedAt ? new Date(build.completedAt).toLocaleString() : new Date(build.updatedAt).toLocaleString()}
{showActions && (
)}
{excludedModules.length > 0 && (
Excluded Modules: {excludedModules.map((module, index) => ( {module.name} {index < excludedModules.length - 1 && ,} ))}
)} {(explicitPlugins.length > 0 || implicitPlugins.length > 0) && (
Plugins: {explicitPlugins.map((plugin, index) => ( {plugin.name} {(index < explicitPlugins.length - 1 || implicitPlugins.length > 0) && ( , )} ))} {implicitPlugins.map((plugin, index) => ( {plugin.name} {index < implicitPlugins.length - 1 && ,} ))}
)} {build && (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} ))}
)}
{/* All Actions */} {showActions && (
{status === "success" && build.buildHash && ( )} {build.buildHash && ( <> )} {isAdmin && build && onRetry && ( )}
)} {/* Status Messages */} {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.

)} {/* Bash Script Modal */} {showBashModal && (
{ setShowBashModal(false) setBashCopied(false) }} >
e.stopPropagation()} >

Build Script

                {generateBashCommand()}
              
)}
) }