2025-08-10 11:41:47 -04:00
// Fill workflow module for the 4DSTAR Bundle Manager
// Extracted from renderer.js to centralize fill process management
const { ipcRenderer } = require ( 'electron' ) ;
// Import dependencies (these will be injected when integrated)
let stateManager , domManager ;
// Load fillable targets for the Fill tab
async function loadFillableTargets ( ) {
const currentBundlePath = stateManager . getCurrentBundlePath ( ) ;
const elements = domManager . getElements ( ) ;
console . log ( 'loadFillableTargets called, currentBundlePath:' , currentBundlePath ) ;
// Check if required DOM elements exist
if ( ! elements . fillNoTargets || ! elements . fillTargetsContent || ! elements . fillLoading ) {
console . error ( 'Fill tab DOM elements not found' ) ;
domManager . showModal ( 'Error' , 'Fill tab interface not properly initialized.' ) ;
return ;
}
if ( ! currentBundlePath ) {
console . log ( 'No bundle path, showing no targets message' ) ;
hideAllFillStates ( ) ;
elements . fillNoTargets . classList . remove ( 'hidden' ) ;
return ;
}
try {
// Show loading state
hideAllFillStates ( ) ;
elements . fillLoading . classList . remove ( 'hidden' ) ;
elements . loadFillableTargetsBtn . disabled = true ;
console . log ( 'Requesting fillable targets for:' , currentBundlePath ) ;
const result = await ipcRenderer . invoke ( 'get-fillable-targets' , currentBundlePath ) ;
console . log ( 'Fillable targets result:' , result ) ;
if ( result && result . success && result . data ) {
const hasTargets = Object . values ( result . data ) . some ( targets => targets . length > 0 ) ;
if ( hasTargets ) {
console . log ( 'Found fillable targets, populating table' ) ;
hideAllFillStates ( ) ;
elements . fillTargetsContent . classList . remove ( 'hidden' ) ;
populateFillTargetsTable ( result . data ) ;
} else {
console . log ( 'No fillable targets found' ) ;
hideAllFillStates ( ) ;
elements . fillNoTargets . classList . remove ( 'hidden' ) ;
}
} else {
console . error ( 'Failed to get fillable targets:' , result . error ) ;
hideAllFillStates ( ) ;
elements . fillNoTargets . classList . remove ( 'hidden' ) ;
domManager . showModal ( 'Error' , ` Failed to load fillable targets: ${ result . error || 'Unknown error' } ` ) ;
}
} catch ( error ) {
console . error ( 'Exception in loadFillableTargets:' , error ) ;
hideAllFillStates ( ) ;
elements . fillNoTargets . classList . remove ( 'hidden' ) ;
domManager . showModal ( 'Error' , ` Failed to load fillable targets: ${ error . message } ` ) ;
} finally {
elements . loadFillableTargetsBtn . disabled = false ;
}
}
// Helper function to hide all fill tab states
function hideAllFillStates ( ) {
const elements = domManager . getElements ( ) ;
elements . fillLoading . classList . add ( 'hidden' ) ;
elements . fillNoTargets . classList . add ( 'hidden' ) ;
elements . fillTargetsContent . classList . add ( 'hidden' ) ;
elements . fillProgressContainer . classList . add ( 'hidden' ) ;
}
// Create modern table-based interface for fillable targets
function populateFillTargetsTable ( plugins ) {
const elements = domManager . getElements ( ) ;
elements . fillPluginsTables . innerHTML = '' ;
for ( const [ pluginName , targets ] of Object . entries ( plugins ) ) {
if ( targets . length > 0 ) {
// Create plugin table container
const pluginTable = document . createElement ( 'div' ) ;
pluginTable . className = 'fill-plugin-table' ;
// Plugin header
const pluginHeader = document . createElement ( 'div' ) ;
pluginHeader . className = 'fill-plugin-header' ;
pluginHeader . textContent = ` ${ pluginName } ( ${ targets . length } target ${ targets . length > 1 ? 's' : '' } ) ` ;
pluginTable . appendChild ( pluginHeader ) ;
// Create table
const table = document . createElement ( 'table' ) ;
table . className = 'fill-targets-table' ;
// Table header
const thead = document . createElement ( 'thead' ) ;
thead . innerHTML = `
< tr >
< th style = "width: 50px;" >
< input type = "checkbox" class = "plugin-select-all" data - plugin = "${pluginName}" checked >
< / t h >
< th > Target Platform < / t h >
< th > Architecture < / t h >
< th > Type < / t h >
< th > Compiler < / t h >
< / t r >
` ;
table . appendChild ( thead ) ;
// Table body
const tbody = document . createElement ( 'tbody' ) ;
targets . forEach ( target => {
const row = document . createElement ( 'tr' ) ;
row . innerHTML = `
< td >
< input type = "checkbox" class = "fill-target-checkbox"
data - plugin = "${pluginName}"
data - target = '${JSON.stringify(target)}'
checked >
< / t d >
< td > < strong > $ { target . triplet } < / s t r o n g > < / t d >
< td > $ { target . arch } < / t d >
< td > < span class = "target-type ${target.type}" > $ { target . type } < / s p a n > < / t d >
< td > $ { target . type === 'docker' ? 'GCC' : ( target . details ? . compiler || 'N/A' ) } $ { target . details ? . compiler _version || '' } < / t d >
` ;
tbody . appendChild ( row ) ;
} ) ;
table . appendChild ( tbody ) ;
pluginTable . appendChild ( table ) ;
elements . fillPluginsTables . appendChild ( pluginTable ) ;
}
}
// Add event listeners for select all functionality
setupFillTargetEventListeners ( ) ;
}
// Setup event listeners for Fill tab functionality
function setupFillTargetEventListeners ( ) {
const elements = domManager . getElements ( ) ;
// Plugin-level select all checkboxes
document . querySelectorAll ( '.plugin-select-all' ) . forEach ( checkbox => {
checkbox . addEventListener ( 'change' , ( e ) => {
const pluginName = e . target . dataset . plugin ;
const pluginCheckboxes = document . querySelectorAll ( ` .fill-target-checkbox[data-plugin=" ${ pluginName } "] ` ) ;
pluginCheckboxes . forEach ( cb => cb . checked = e . target . checked ) ;
} ) ;
} ) ;
// Global select/deselect all buttons
elements . selectAllTargetsBtn . addEventListener ( 'click' , ( ) => {
document . querySelectorAll ( '.fill-target-checkbox, .plugin-select-all' ) . forEach ( cb => cb . checked = true ) ;
} ) ;
elements . deselectAllTargetsBtn . addEventListener ( 'click' , ( ) => {
document . querySelectorAll ( '.fill-target-checkbox, .plugin-select-all' ) . forEach ( cb => cb . checked = false ) ;
} ) ;
// Start fill process button
elements . startFillProcessBtn . addEventListener ( 'click' , async ( ) => {
const selectedTargetsByPlugin = { } ;
document . querySelectorAll ( '.fill-target-checkbox:checked' ) . forEach ( checkbox => {
try {
const target = JSON . parse ( checkbox . dataset . target ) ;
const pluginName = checkbox . dataset . plugin ;
if ( ! selectedTargetsByPlugin [ pluginName ] ) {
selectedTargetsByPlugin [ pluginName ] = [ ] ;
}
selectedTargetsByPlugin [ pluginName ] . push ( target ) ;
} catch ( error ) {
console . error ( 'Error parsing target data:' , error ) ;
}
} ) ;
if ( Object . keys ( selectedTargetsByPlugin ) . length === 0 ) {
domManager . showModal ( 'No Targets Selected' , 'Please select at least one target to build.' ) ;
return ;
}
2025-08-10 14:06:50 -04:00
// Show signature warning before starting fill process
const currentBundle = stateManager . getCurrentBundle ( ) ;
console . log ( '[FRONTEND] Checking signature for modal:' , currentBundle ) ;
console . log ( '[FRONTEND] Bundle report:' , currentBundle ? . report ) ;
console . log ( '[FRONTEND] Signature info:' , currentBundle ? . report ? . signature ) ;
console . log ( '[FRONTEND] Full signature object:' , JSON . stringify ( currentBundle ? . report ? . signature , null , 2 ) ) ;
// Check multiple possible signature indicators
const signature = currentBundle ? . report ? . signature ;
const isSignedBundle = signature && (
signature . valid === true ||
signature . status === 'VALID' ||
signature . status === 'SIGNED' ||
( signature . fingerprint && signature . fingerprint !== '' ) ||
( signature . publicKeyFingerprint && signature . publicKeyFingerprint !== '' ) ||
signature . verified === true
) ;
console . log ( '[FRONTEND] Signature check result:' , isSignedBundle ) ;
console . log ( '[FRONTEND] Signature properties:' , {
valid : signature ? . valid ,
status : signature ? . status ,
fingerprint : signature ? . fingerprint ,
publicKeyFingerprint : signature ? . publicKeyFingerprint ,
verified : signature ? . verified
} ) ;
if ( currentBundle && currentBundle . report && isSignedBundle ) {
console . log ( '[FRONTEND] Bundle is signed, showing confirmation dialog' ) ;
const confirmed = confirm ( 'Warning: Signature Will Be Invalidated\n\nBuilding new binaries will invalidate the current bundle signature. The bundle will need to be re-signed after the fill process completes.\n\nDo you want to continue?' ) ;
if ( confirmed ) {
console . log ( '[FRONTEND] User confirmed, starting fill process' ) ;
await startFillProcess ( selectedTargetsByPlugin ) ;
} else {
console . log ( '[FRONTEND] User cancelled fill process due to signature warning' ) ;
}
} else {
console . log ( '[FRONTEND] Bundle is not signed or signature is invalid, proceeding without warning' ) ;
await startFillProcess ( selectedTargetsByPlugin ) ;
}
2025-08-10 11:41:47 -04:00
} ) ;
}
// Create progress display for fill process
function populateFillProgress ( selectedTargetsByPlugin ) {
const elements = domManager . getElements ( ) ;
elements . fillProgressContent . innerHTML = '' ;
Object . entries ( selectedTargetsByPlugin ) . forEach ( ( [ pluginName , targets ] ) => {
targets . forEach ( target => {
const progressItem = document . createElement ( 'div' ) ;
progressItem . className = 'fill-progress-item' ;
2025-08-10 14:06:50 -04:00
// Create unique ID using both plugin and target to avoid duplicates
progressItem . id = ` progress- ${ pluginName } - ${ target . triplet } ` ;
2025-08-10 11:41:47 -04:00
progressItem . innerHTML = `
< div class = "progress-target" > $ { pluginName } : $ { target . triplet } < / d i v >
2025-08-10 14:06:50 -04:00
< div class = "progress-status building" >
< span class = "spinner" > < / s p a n >
Building ...
< / d i v >
2025-08-10 11:41:47 -04:00
< div class = "progress-bar" >
< div class = "progress-fill" style = "width: 0%" > < / d i v >
< / d i v >
2025-08-10 14:06:50 -04:00
< div class = "progress-logs" >
< button class = "toggle-logs-btn" > Hide Build Output < / b u t t o n >
< div class = "logs-content" >
< pre class = "terminal-output" > Starting build ... \ n < / p r e >
< / d i v >
< / d i v >
2025-08-10 11:41:47 -04:00
` ;
elements . fillProgressContent . appendChild ( progressItem ) ;
2025-08-10 14:06:50 -04:00
// Add toggle functionality for logs
const toggleBtn = progressItem . querySelector ( '.toggle-logs-btn' ) ;
const logsContent = progressItem . querySelector ( '.logs-content' ) ;
toggleBtn . addEventListener ( 'click' , ( ) => {
const isVisible = ! logsContent . classList . contains ( 'hidden' ) ;
logsContent . classList . toggle ( 'hidden' , isVisible ) ;
toggleBtn . textContent = isVisible ? 'Show Build Output' : 'Hide Build Output' ;
} ) ;
2025-08-10 11:41:47 -04:00
} ) ;
} ) ;
}
// Start the fill process
async function startFillProcess ( selectedTargetsByPlugin ) {
const currentBundlePath = stateManager . getCurrentBundlePath ( ) ;
const elements = domManager . getElements ( ) ;
if ( ! currentBundlePath ) {
domManager . showModal ( 'Error' , 'No bundle is currently open.' ) ;
return ;
}
try {
// Show progress view
hideAllFillStates ( ) ;
elements . fillProgressContainer . classList . remove ( 'hidden' ) ;
populateFillProgress ( selectedTargetsByPlugin ) ;
// Set up progress listener
const progressHandler = ( event , progressData ) => {
updateFillProgress ( progressData ) ;
} ;
ipcRenderer . on ( 'fill-bundle-progress' , progressHandler ) ;
// Start the fill process
const result = await ipcRenderer . invoke ( 'fill-bundle' , {
bundlePath : currentBundlePath ,
targetsToBuild : selectedTargetsByPlugin
} ) ;
// Clean up progress listener
ipcRenderer . removeListener ( 'fill-bundle-progress' , progressHandler ) ;
if ( result . success ) {
domManager . showModal ( 'Fill Complete' , 'Bundle fill process completed successfully. Reloading bundle...' ) ;
// Reload the bundle to show updated information
// Note: This will be handled by the parent module that has access to bundleOperations
if ( window . bundleOperations ) {
await window . bundleOperations . reloadCurrentBundle ( ) ;
}
domManager . hideModal ( ) ;
} else {
domManager . showModal ( 'Fill Error' , ` Fill process failed: ${ result . error } ` ) ;
}
} catch ( error ) {
console . error ( 'Fill process error:' , error ) ;
domManager . showModal ( 'Fill Error' , ` Fill process failed: ${ error . message } ` ) ;
}
}
2025-08-10 14:06:50 -04:00
// Track current building target for stderr routing
let currentBuildingTarget = null ;
2025-08-10 11:41:47 -04:00
// Update progress display during fill process
function updateFillProgress ( progressData ) {
console . log ( 'Fill progress update:' , progressData ) ;
2025-08-10 14:06:50 -04:00
// Handle stderr output - route to current building target
if ( progressData . type === 'stderr' && progressData . stderr ) {
console . log ( ` [FRONTEND] Received stderr: ${ progressData . stderr } ` ) ;
console . log ( ` [FRONTEND] Current building target: ${ currentBuildingTarget } ` ) ;
// Route stderr to the currently building target's terminal
if ( currentBuildingTarget ) {
const targetTerminal = document . querySelector ( ` #progress- ${ currentBuildingTarget } .terminal-output ` ) ;
console . log ( ` [FRONTEND] Found target terminal: ` , targetTerminal ) ;
if ( targetTerminal ) {
const currentOutput = targetTerminal . textContent ;
targetTerminal . textContent = currentOutput + progressData . stderr + '\n' ;
// Auto-scroll to bottom
targetTerminal . scrollTop = targetTerminal . scrollHeight ;
} else {
console . warn ( ` [FRONTEND] Could not find terminal for target: ${ currentBuildingTarget } ` ) ;
}
} else {
console . warn ( ` [FRONTEND] No current building target, adding stderr to all terminals ` ) ;
// Fallback: add to all terminals if no current target
document . querySelectorAll ( '.terminal-output' ) . forEach ( terminalOutput => {
const currentOutput = terminalOutput . textContent ;
terminalOutput . textContent = currentOutput + progressData . stderr + '\n' ;
terminalOutput . scrollTop = terminalOutput . scrollHeight ;
} ) ;
}
return ;
}
if ( progressData . target && progressData . plugin ) {
console . log ( ` [FRONTEND] Processing target update for: ${ progressData . plugin } - ${ progressData . target } , status: ${ progressData . status } ` ) ;
console . log ( ` [FRONTEND] Looking for element with ID: progress- ${ progressData . plugin } - ${ progressData . target } ` ) ;
// Debug: list all existing progress item IDs
const allProgressItems = document . querySelectorAll ( '[id^="progress-"]' ) ;
console . log ( ` [FRONTEND] Existing progress item IDs: ` , Array . from ( allProgressItems ) . map ( item => item . id ) ) ;
const progressItem = document . getElementById ( ` progress- ${ progressData . plugin } - ${ progressData . target } ` ) ;
console . log ( ` [FRONTEND] Found progress item: ` , progressItem ) ;
2025-08-10 11:41:47 -04:00
if ( progressItem ) {
const statusElement = progressItem . querySelector ( '.progress-status' ) ;
const progressBar = progressItem . querySelector ( '.progress-fill' ) ;
2025-08-10 14:06:50 -04:00
const logsContainer = progressItem . querySelector ( '.progress-logs' ) ;
const terminalOutput = progressItem . querySelector ( '.terminal-output' ) ;
2025-08-10 11:41:47 -04:00
if ( progressData . status ) {
2025-08-10 14:06:50 -04:00
console . log ( ` [FRONTEND] Updating status to: ${ progressData . status } ` ) ;
// Update progress bar and status based on status
2025-08-10 11:41:47 -04:00
let percentage = 0 ;
2025-08-10 14:06:50 -04:00
switch ( progressData . status . toLowerCase ( ) ) {
case 'building' :
2025-08-10 11:41:47 -04:00
percentage = 50 ;
2025-08-10 14:06:50 -04:00
statusElement . className = 'progress-status building' ;
statusElement . innerHTML = '<span class="spinner"></span> Building...' ;
2025-08-10 11:41:47 -04:00
progressItem . className = 'fill-progress-item building' ;
2025-08-10 14:06:50 -04:00
// Track this target as currently building for stderr routing
currentBuildingTarget = progressData . target ;
console . log ( ` [FRONTEND] Now building target: ${ currentBuildingTarget } ` ) ;
2025-08-10 11:41:47 -04:00
break ;
2025-08-10 14:06:50 -04:00
case 'success' :
2025-08-10 11:41:47 -04:00
percentage = 100 ;
2025-08-10 14:06:50 -04:00
statusElement . className = 'progress-status success' ;
statusElement . innerHTML = '✓ Built' ;
2025-08-10 11:41:47 -04:00
progressItem . className = 'fill-progress-item success' ;
2025-08-10 14:06:50 -04:00
if ( logsContainer ) logsContainer . style . display = 'block' ;
// Clear current building target when done
if ( currentBuildingTarget === progressData . target ) {
currentBuildingTarget = null ;
console . log ( ` [FRONTEND] Cleared building target after success ` ) ;
}
2025-08-10 11:41:47 -04:00
break ;
2025-08-10 14:06:50 -04:00
case 'failure' :
case 'failed' :
2025-08-10 11:41:47 -04:00
percentage = 100 ;
2025-08-10 14:06:50 -04:00
statusElement . className = 'progress-status failed' ;
statusElement . innerHTML = '✗ Failed' ;
2025-08-10 11:41:47 -04:00
progressItem . className = 'fill-progress-item failed' ;
2025-08-10 14:06:50 -04:00
if ( logsContainer ) logsContainer . style . display = 'block' ;
// Clear current building target when done
if ( currentBuildingTarget === progressData . target ) {
currentBuildingTarget = null ;
console . log ( ` [FRONTEND] Cleared building target after failure ` ) ;
}
2025-08-10 11:41:47 -04:00
break ;
}
if ( progressBar ) {
progressBar . style . width = ` ${ percentage } % ` ;
}
}
2025-08-10 14:06:50 -04:00
// Add stderr output if available (for target-specific messages)
if ( progressData . stderr && terminalOutput ) {
const currentOutput = terminalOutput . textContent ;
terminalOutput . textContent = currentOutput + progressData . stderr + '\n' ;
// Auto-scroll to bottom
terminalOutput . scrollTop = terminalOutput . scrollHeight ;
}
2025-08-10 11:41:47 -04:00
}
}
}
// Initialize dependencies (called when module is loaded)
function initializeDependencies ( deps ) {
stateManager = deps . stateManager ;
domManager = deps . domManager ;
}
module . exports = {
initializeDependencies ,
loadFillableTargets ,
hideAllFillStates ,
populateFillTargetsTable ,
setupFillTargetEventListeners ,
populateFillProgress ,
startFillProcess ,
updateFillProgress
} ;