diff --git a/build_manager/manager.py b/build_manager/manager.py index b3fad5c..315025f 100644 --- a/build_manager/manager.py +++ b/build_manager/manager.py @@ -45,6 +45,7 @@ def to_dict(self) -> dict: class BuildInfo: def __init__(self, vehicle_id: str, + version_id: str, remote_info: RemoteInfo, git_hash: str, board: str, @@ -56,6 +57,7 @@ def __init__(self, Parameters: vehicle_id (str): The vehicle ID associated with the build. + version_id (str): The version ID associated with the build. remote_info (RemoteInfo): The remote repository containing the source commit to build on. git_hash (str): The git commit hash to build on. @@ -63,6 +65,7 @@ def __init__(self, selected_features (set): Set of features selected for the build. """ self.vehicle_id = vehicle_id + self.version_id = version_id self.remote_info = remote_info self.git_hash = git_hash self.board = board @@ -77,6 +80,7 @@ def __init__(self, def to_dict(self) -> dict: return { 'vehicle_id': self.vehicle_id, + 'version_id': self.version_id, 'remote_info': self.remote_info.to_dict(), 'git_hash': self.git_hash, 'board': self.board, diff --git a/web/schemas/__init__.py b/web/schemas/__init__.py index 3f9340a..9202cac 100644 --- a/web/schemas/__init__.py +++ b/web/schemas/__init__.py @@ -12,6 +12,7 @@ # Build schemas from .builds import ( + BuildVersionInfo, RemoteInfo, BuildProgress, BuildRequest, @@ -36,6 +37,7 @@ # Admin "RefreshRemotesResponse", # Builds + "BuildVersionInfo", "RemoteInfo", "BuildProgress", "BuildRequest", diff --git a/web/schemas/builds.py b/web/schemas/builds.py index dd5223b..861d92c 100644 --- a/web/schemas/builds.py +++ b/web/schemas/builds.py @@ -43,15 +43,24 @@ class BuildSubmitResponse(BaseModel): ) +# --- Build Version Info --- +class BuildVersionInfo(BaseModel): + """Version information for a build.""" + id: str = Field(..., description="Version ID used for this build") + remote_info: RemoteInfo = Field( + ..., description="Source repository information" + ) + git_hash: str = Field(..., description="Git commit hash used for build") + + # --- Build Output --- class BuildOut(BaseModel): """Complete build information output schema.""" build_id: str = Field(..., description="Unique build identifier") vehicle: VehicleBase = Field(..., description="Target vehicle information") board: BoardBase = Field(..., description="Target board information") - git_hash: str = Field(..., description="Git commit hash used for build") - remote_info: RemoteInfo = Field( - ..., description="Source repository information" + version: BuildVersionInfo = Field( + ..., description="Version information for this build" ) selected_features: List[str] = Field( default_factory=list, diff --git a/web/services/builds.py b/web/services/builds.py index 691577b..ede6d91 100644 --- a/web/services/builds.py +++ b/web/services/builds.py @@ -12,6 +12,7 @@ BuildOut, BuildProgress, RemoteInfo, + BuildVersionInfo, ) from schemas.vehicles import VehicleBase, BoardBase @@ -139,6 +140,7 @@ def create_build( # Create build info build_info = build_manager.BuildInfo( vehicle_id=vehicle_id, + version_id=build_request.version_id, remote_info=remote_info, git_hash=git_hash, board=board_name, @@ -370,8 +372,11 @@ def _build_info_to_output( id=build_info.board, name=build_info.board # Board name is same as board ID for now ), - git_hash=build_info.git_hash, - remote_info=remote_info, + version=BuildVersionInfo( + id=build_info.version_id, + remote_info=remote_info, + git_hash=build_info.git_hash + ), selected_features=selected_feature_labels, progress=progress, time_created=build_info.time_created, diff --git a/web/static/js/add_build.js b/web/static/js/add_build.js index 4b298d8..b65241d 100644 --- a/web/static/js/add_build.js +++ b/web/static/js/add_build.js @@ -87,7 +87,7 @@ const Features = (() => { }); } - function handleOptionStateChange(feature_id, triggered_by_ui) { + function handleOptionStateChange(feature_id, triggered_by_ui, updateDependencies = true) { // feature_id is the feature ID from the API let element = document.getElementById(feature_id); if (!element) return; @@ -97,13 +97,17 @@ const Features = (() => { if (element.checked) { selected_options += 1; - enableDependenciesForFeature(feature.id); + if (updateDependencies) { + enableDependenciesForFeature(feature.id); + } } else { selected_options -= 1; - if (triggered_by_ui) { - askToDisableDependentsForFeature(feature.id); - } else { - disabledDependentsForFeature(feature.id); + if (updateDependencies) { + if (triggered_by_ui) { + askToDisableDependentsForFeature(feature.id); + } else { + disabledDependentsForFeature(feature.id); + } } } @@ -262,7 +266,7 @@ const Features = (() => { }); } - function checkUncheckOptionById(id, check) { + function checkUncheckOptionById(id, check, updateDependencies = true) { let feature = getOptionById(id); if (!feature) return; @@ -273,7 +277,7 @@ const Features = (() => { } element.checked = check; const triggered_by_ui = false; - handleOptionStateChange(feature.id, triggered_by_ui); + handleOptionStateChange(feature.id, triggered_by_ui, updateDependencies); } function checkUncheckAll(check) { @@ -288,15 +292,77 @@ const Features = (() => { }); } - return {reset, handleOptionStateChange, getCategoryIdByName, applyDefaults, checkUncheckAll, checkUncheckCategory, getOptionById}; + return {reset, handleOptionStateChange, getCategoryIdByName, applyDefaults, checkUncheckAll, checkUncheckCategory, getOptionById, checkUncheckOptionById}; })(); var init_categories_expanded = false; -function init() { +var rebuildConfig = { + vehicleId: null, + versionId: null, + boardId: null, + selectedFeatures: [], + isRebuildMode: false +}; + +async function init() { + if (typeof rebuildFromBuildId !== 'undefined') { + await initRebuild(rebuildFromBuildId); + } + fetchVehicles(); } +async function initRebuild(buildId) { + try { + const buildResponse = await fetch(`/api/v1/builds/${buildId}`); + if (!buildResponse.ok) { + throw new Error('Failed to fetch build details'); + } + const buildData = await buildResponse.json(); + + if (!buildData.vehicle || !buildData.vehicle.id) { + throw new Error('Vehicle information is missing from the build'); + } + if (!buildData.version || !buildData.version.id) { + throw new Error('Version information is missing from the build'); + } + if (!buildData.board || !buildData.board.id) { + throw new Error('Board information is missing from the build'); + } + + rebuildConfig.vehicleId = buildData.vehicle.id; + rebuildConfig.versionId = buildData.version.id; + rebuildConfig.boardId = buildData.board.id; + rebuildConfig.selectedFeatures = buildData.selected_features || []; + rebuildConfig.isRebuildMode = true; + + } catch (error) { + console.error('Error loading rebuild configuration:', error); + alert('Failed to load build configuration: ' + error.message + '\n\nRedirecting to new build page...'); + window.location.href = '/add_build'; + throw error; + } +} + +function applyRebuildFeatures(featuresList) { + Features.checkUncheckAll(false); + + if (featuresList && featuresList.length > 0) { + featuresList.forEach(featureId => { + Features.checkUncheckOptionById(featureId, true, false); + }); + } +} + +function clearRebuildConfig() { + rebuildConfig.vehicleId = null; + rebuildConfig.versionId = null; + rebuildConfig.boardId = null; + rebuildConfig.selectedFeatures = []; + rebuildConfig.isRebuildMode = false; +} + // enables or disables the elements with ids passed as an array // if enable is true, the elements are enabled and vice-versa function enableDisableElementsById(ids, enable) { @@ -330,7 +396,19 @@ function fetchVehicles() { sendAjaxRequestForJsonResponse(request_url) .then((json_response) => { let all_vehicles = json_response; - let new_vehicle = all_vehicles.find(vehicle => vehicle.name === "Copter") ? "copter": all_vehicles[0].id; + + if (rebuildConfig.vehicleId) { + const vehicleExists = all_vehicles.some(v => v.id === rebuildConfig.vehicleId); + if (!vehicleExists) { + console.warn(`Rebuild vehicle '${rebuildConfig.vehicleId}' not found in available vehicles`); + alert(`Warning: The vehicle from the original build is no longer available.\n\nRedirecting to new build page...`); + window.location.href = '/add_build'; + return; + } + } + + let new_vehicle = rebuildConfig.vehicleId || + (all_vehicles.find(vehicle => vehicle.name === "Copter") ? "copter" : all_vehicles[0].id); updateVehicles(all_vehicles, new_vehicle); }) .catch((message) => { @@ -360,7 +438,18 @@ function onVehicleChange(new_vehicle_id) { .then((json_response) => { let all_versions = json_response; all_versions = sortVersions(all_versions); - const new_version = all_versions[0].id; + + if (rebuildConfig.versionId) { + const versionExists = all_versions.some(v => v.id === rebuildConfig.versionId); + if (!versionExists) { + console.warn(`Rebuild version '${rebuildConfig.versionId}' not found for vehicle '${new_vehicle_id}'`); + alert(`Warning: The version from the original build is no longer available.\n\nRedirecting to new build page...`); + window.location.href = '/add_build'; + return; + } + } + + const new_version = rebuildConfig.versionId || all_versions[0].id; updateVersions(all_versions, new_version); }) .catch((message) => { @@ -405,7 +494,18 @@ function onVersionChange(new_version) { .then((boards_response) => { // Keep full board objects with id and name let boards = boards_response; - let new_board = boards.length > 0 ? boards[0].id : null; + + if (rebuildConfig.boardId) { + const boardExists = boards.some(b => b.id === rebuildConfig.boardId); + if (!boardExists) { + console.warn(`Rebuild board '${rebuildConfig.boardId}' not found for version '${version_id}'`); + alert(`Warning: The board from the original build is no longer available.\n\nRedirecting to new build page...`); + window.location.href = '/add_build'; + return; + } + } + + let new_board = rebuildConfig.boardId || (boards.length > 0 ? boards[0].id : null); updateBoards(boards, new_board); }) .catch((message) => { @@ -443,7 +543,14 @@ function onBoardChange(new_board) { .then((features_response) => { Features.reset(features_response); fillBuildOptions(features_response); - Features.applyDefaults(); + + // TODO: Refactor to use a single method to apply both rebuild and default features + if (rebuildConfig.isRebuildMode) { + applyRebuildFeatures(rebuildConfig.selectedFeatures); + clearRebuildConfig(); + } else { + Features.applyDefaults(); + } }) .catch((message) => { console.log("Features update failed. "+message); diff --git a/web/static/js/index.js b/web/static/js/index.js index d13cd85..a831f02 100644 --- a/web/static/js/index.js +++ b/web/static/js/index.js @@ -81,7 +81,7 @@ function updateBuildsTable(builds) { table_body_html += ` ${build_info['progress']['state']} ${build_age} - ${build_info['git_hash'].substring(0,8)} + ${build_info['version']['git_hash'].substring(0,8)} ${build_info['board']['name']} ${build_info['vehicle']['name']} @@ -101,6 +101,9 @@ function updateBuildsTable(builds) { + `; row_num += 1; @@ -115,7 +118,7 @@ function updateBuildsTable(builds) { Vehicle Features Progress - Actions + Actions ${table_body_html} `; diff --git a/web/templates/add_build.html b/web/templates/add_build.html index 6c97488..ba792b0 100644 --- a/web/templates/add_build.html +++ b/web/templates/add_build.html @@ -123,6 +123,12 @@ + {% if rebuild_from != None %} + + {% endif %} + diff --git a/web/ui/router.py b/web/ui/router.py index fe00662..5689236 100644 --- a/web/ui/router.py +++ b/web/ui/router.py @@ -33,17 +33,18 @@ async def index(request: Request, build_id: str = None): @router.get("/add_build", response_class=HTMLResponse) -async def add_build(request: Request): +async def add_build(request: Request, rebuild_from: str = None): """ Render the add build page for creating new firmware builds. Args: request: FastAPI Request object + rebuild_from: Optional build ID to copy configuration from Returns: Rendered HTML template """ return templates.TemplateResponse( "add_build.html", - {"request": request} + {"request": request, "rebuild_from": rebuild_from} )