feat(electorn): added fill

This commit is contained in:
2025-08-09 20:41:01 -04:00
parent 45de795920
commit 396d594a27
4 changed files with 436 additions and 137 deletions

View File

@@ -33,7 +33,6 @@
<div class="action-buttons">
<button id="sign-bundle-btn">Sign</button>
<button id="validate-bundle-btn">Validate</button>
<button id="fill-bundle-btn">Fill</button>
<button id="clear-bundle-btn">Clear</button>
<button id="save-metadata-btn" class="hidden">Save Changes</button>
</div>
@@ -42,7 +41,8 @@
<div class="tab-nav">
<button class="tab-link active" data-tab="overview-tab">Overview</button>
<button class="tab-link" data-tab="plugins-tab">Plugins</button>
<button class="tab-link" data-tab="validation-tab" class="hidden">Validation</button>
<button class="tab-link" data-tab="fill-tab" id="fill-tab-link">Fill</button>
<button class="tab-link hidden" data-tab="validation-tab">Validation</button>
</div>
<div id="tab-content">
@@ -55,6 +55,35 @@
<div id="validation-tab" class="tab-pane">
<pre id="validation-results"></pre>
</div>
<div id="fill-tab" class="tab-pane">
<div class="fill-header">
<h3>Fill Bundle with Compiled Binaries</h3>
<p>Select the targets you want to build and add to the bundle:</p>
<div class="fill-header-actions">
<button id="load-fillable-targets-btn" class="action-button primary">Load Available Targets</button>
</div>
</div>
<div id="fill-targets-container">
<div id="fill-loading" class="hidden">
<p>Loading available targets...</p>
</div>
<div id="fill-no-targets" class="hidden">
<p>No fillable targets available. The bundle may already be complete.</p>
</div>
<div id="fill-targets-content" class="hidden">
<div id="fill-plugins-tables"></div>
<div class="fill-actions">
<button id="select-all-targets" class="action-button secondary">Select All</button>
<button id="deselect-all-targets" class="action-button secondary">Deselect All</button>
<button id="start-fill-process" class="action-button primary">Start Building</button>
</div>
</div>
</div>
<div id="fill-progress-container" class="hidden">
<h4>Build Progress</h4>
<div id="fill-progress-content"></div>
</div>
</div>
</div>
</div>
@@ -103,7 +132,7 @@
</div>
<!-- Fill Modal -->
<div id="fill-modal" class="modal">
<div id="fill-modal" class="modal-container hidden">
<div class="modal-content">
<span class="close-fill-modal-button">&times;</span>
<h2 id="fill-modal-title">Fill Bundle</h2>

View File

@@ -17,7 +17,7 @@ const createBundleBtn = document.getElementById('create-bundle-btn');
// Bundle action buttons
const signBundleBtn = document.getElementById('sign-bundle-btn');
const validateBundleBtn = document.getElementById('validate-bundle-btn');
const fillBundleBtn = document.getElementById('fill-bundle-btn');
// Fill button removed - Fill tab is now always visible
const clearBundleBtn = document.getElementById('clear-bundle-btn');
const saveMetadataBtn = document.getElementById('save-metadata-btn');
@@ -26,6 +26,19 @@ const saveOptionsModal = document.getElementById('save-options-modal');
const overwriteBundleBtn = document.getElementById('overwrite-bundle-btn');
const saveAsNewBtn = document.getElementById('save-as-new-btn');
// Fill tab elements
const fillTabLink = document.getElementById('fill-tab-link');
const loadFillableTargetsBtn = document.getElementById('load-fillable-targets-btn');
const fillLoading = document.getElementById('fill-loading');
const fillPluginsTables = document.getElementById('fill-plugins-tables');
const fillNoTargets = document.getElementById('fill-no-targets');
const fillTargetsContent = document.getElementById('fill-targets-content');
const selectAllTargetsBtn = document.getElementById('select-all-targets');
const deselectAllTargetsBtn = document.getElementById('deselect-all-targets');
const startFillProcessBtn = document.getElementById('start-fill-process');
const fillProgressContainer = document.getElementById('fill-progress-container');
const fillProgressContent = document.getElementById('fill-progress-content');
// Bundle display
const bundleTitle = document.getElementById('bundle-title');
const manifestDetails = document.getElementById('manifest-details');
@@ -103,71 +116,165 @@ function setupEventListeners() {
saveMetadataBtn.addEventListener('click', showSaveOptionsModal);
overwriteBundleBtn.addEventListener('click', () => handleSaveMetadata(false));
saveAsNewBtn.addEventListener('click', () => handleSaveMetadata(true));
fillBundleBtn.addEventListener('click', async () => {
if (!currentBundlePath) {
showModal('Error', 'No bundle is currently open.');
// Load fillable targets button
loadFillableTargetsBtn.addEventListener('click', async () => {
await loadFillableTargets();
});
// Load fillable targets for the Fill tab
async function loadFillableTargets() {
console.log('loadFillableTargets called, currentBundlePath:', currentBundlePath);
// Check if required DOM elements exist
if (!fillNoTargets || !fillTargetsContent || !fillLoading) {
console.error('Fill tab DOM elements not found');
showModal('Error', 'Fill tab interface not properly initialized.');
return;
}
showSpinner();
if (!currentBundlePath) {
console.log('No bundle path, showing no targets message');
hideAllFillStates();
fillNoTargets.classList.remove('hidden');
return;
}
try {
// Show loading state
hideAllFillStates();
fillLoading.classList.remove('hidden');
loadFillableTargetsBtn.disabled = true;
console.log('Calling get-fillable-targets...');
const result = await ipcRenderer.invoke('get-fillable-targets', currentBundlePath);
hideSpinner();
console.log('get-fillable-targets result:', result);
if (!result.success) {
showModal('Error', `Failed to get fillable targets: ${result.error}`);
console.log('get-fillable-targets failed:', result.error);
hideAllFillStates();
showModal('Error', `Failed to load fillable targets: ${result.error}`);
return;
}
const targets = result.data;
if (Object.keys(targets).length === 0) {
showModal('Info', 'The bundle is already full. No new targets to build.');
return;
console.log('Fillable targets:', targets);
hideAllFillStates();
if (!targets || Object.keys(targets).length === 0) {
console.log('No fillable targets found');
fillNoTargets.classList.remove('hidden');
} else {
console.log('Populating fillable targets table');
fillTargetsContent.classList.remove('hidden');
populateFillTargetsTable(targets);
}
} catch (error) {
console.error('Error in loadFillableTargets:', error);
hideAllFillStates();
showModal('Error', `Error loading fillable targets: ${error.message}`);
} finally {
loadFillableTargetsBtn.disabled = false;
}
}
populateFillTargetsList(targets);
fillModal.style.display = 'block';
});
// Helper function to hide all fill tab states
function hideAllFillStates() {
fillLoading.classList.add('hidden');
fillNoTargets.classList.add('hidden');
fillTargetsContent.classList.add('hidden');
}
closeFillModalButton.addEventListener('click', () => {
fillModal.style.display = 'none';
});
// Old modal code removed - now using tab-based interface
// Create modern table-based interface for fillable targets
function populateFillTargetsTable(plugins) {
fillPluginsTables.innerHTML = '';
function populateFillTargetsList(plugins) {
fillTargetsList.innerHTML = '';
for (const [pluginName, targets] of Object.entries(plugins)) {
if (targets.length > 0) {
const pluginHeader = document.createElement('h4');
pluginHeader.textContent = `Plugin: ${pluginName}`;
fillTargetsList.appendChild(pluginHeader);
// 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>
</th>
<th>Target Platform</th>
<th>Architecture</th>
<th>Type</th>
<th>Compiler</th>
</tr>
`;
table.appendChild(thead);
// Table body
const tbody = document.createElement('tbody');
targets.forEach(target => {
const item = document.createElement('div');
item.className = 'fill-target-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = true;
checkbox.id = `target-${pluginName}-${target.triplet}`;
checkbox.dataset.pluginName = pluginName;
checkbox.dataset.targetTriplet = target.triplet;
checkbox.dataset.targetInfo = JSON.stringify(target);
const label = document.createElement('label');
label.htmlFor = checkbox.id;
label.textContent = `${target.triplet} (${target.type})`;
item.appendChild(checkbox);
item.appendChild(label);
fillTargetsList.appendChild(item);
const row = document.createElement('tr');
row.innerHTML = `
<td>
<input type="checkbox" class="fill-target-checkbox"
data-plugin="${pluginName}"
data-target='${JSON.stringify(target)}'
checked>
</td>
<td><strong>${target.triplet}</strong></td>
<td>${target.arch}</td>
<td><span class="target-type ${target.type}">${target.type}</span></td>
<td>${target.details?.compiler || 'N/A'} ${target.details?.compiler_version || ''}</td>
`;
tbody.appendChild(row);
});
table.appendChild(tbody);
pluginTable.appendChild(table);
fillPluginsTables.appendChild(pluginTable);
}
}
// Reset view
fillModalBody.style.display = 'block';
fillProgressView.style.display = 'none';
}
startFillButton.addEventListener('click', async () => {
// Add event listeners for select all functionality
setupFillTargetEventListeners();
}
// Setup event listeners for Fill tab functionality
function setupFillTargetEventListeners() {
// 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
selectAllTargetsBtn.addEventListener('click', () => {
document.querySelectorAll('.fill-target-checkbox, .plugin-select-all').forEach(cb => cb.checked = true);
});
deselectAllTargetsBtn.addEventListener('click', () => {
document.querySelectorAll('.fill-target-checkbox, .plugin-select-all').forEach(cb => cb.checked = false);
});
// Start fill process button
startFillProcessBtn.addEventListener('click', async () => {
const selectedTargets = {};
const checkboxes = fillTargetsList.querySelectorAll('input[type="checkbox"]:checked');
const checkboxes = document.querySelectorAll('.fill-target-checkbox:checked');
if (checkboxes.length === 0) {
showModal('Info', 'No targets selected to fill.');
@@ -175,88 +282,64 @@ function setupEventListeners() {
}
checkboxes.forEach(cb => {
const pluginName = cb.dataset.pluginName;
const pluginName = cb.dataset.plugin;
const target = JSON.parse(cb.dataset.target);
if (!selectedTargets[pluginName]) {
selectedTargets[pluginName] = [];
}
selectedTargets[pluginName].push(JSON.parse(cb.dataset.targetInfo));
selectedTargets[pluginName].push(target);
});
fillModalBody.style.display = 'none';
fillProgressView.style.display = 'block';
fillModalTitle.textContent = 'Filling Bundle...';
populateFillProgressList(selectedTargets);
// Hide target selection and show progress
fillTargetsContent.classList.add('hidden');
fillProgressContainer.classList.remove('hidden');
populateFillProgress(selectedTargets);
const result = await ipcRenderer.invoke('fill-bundle', {
bundlePath: currentBundlePath,
targetsToBuild: selectedTargets
});
fillModalTitle.textContent = 'Fill Complete';
if (!result.success) {
// A final error message if the whole process fails.
const p = document.createElement('p');
p.style.color = 'var(--error-color)';
p.textContent = `Error: ${result.error}`;
fillProgressList.appendChild(p);
const errorItem = document.createElement('div');
errorItem.className = 'progress-item';
errorItem.innerHTML = `
<span class="progress-status failure">Error</span>
<span>Fill process failed: ${result.error}</span>
`;
fillProgressContent.appendChild(errorItem);
}
});
}
function populateFillProgressList(plugins) {
fillProgressList.innerHTML = '';
for (const [pluginName, targets] of Object.entries(plugins)) {
// Create progress display for fill process
function populateFillProgress(selectedTargets) {
fillProgressContent.innerHTML = '';
for (const [pluginName, targets] of Object.entries(selectedTargets)) {
targets.forEach(target => {
const item = document.createElement('div');
item.className = 'fill-target-item';
item.id = `progress-${pluginName}-${target.triplet}`;
const indicator = document.createElement('div');
indicator.className = 'progress-indicator';
const label = document.createElement('span');
label.textContent = `${pluginName} - ${target.triplet}`;
item.appendChild(indicator);
item.appendChild(label);
fillProgressList.appendChild(item);
const progressItem = document.createElement('div');
progressItem.className = 'progress-item';
progressItem.id = `progress-${pluginName}-${target.triplet}`;
progressItem.innerHTML = `
<span class="progress-status building">Building</span>
<span>${pluginName} - ${target.triplet}</span>
`;
fillProgressContent.appendChild(progressItem);
});
}
}
// Handle progress updates from backend
ipcRenderer.on('fill-bundle-progress', (event, progress) => {
console.log('Progress update:', progress);
if (typeof progress === 'object' && progress.status) {
const { status, plugin, target, message } = progress;
const { status, plugin, target } = progress;
const progressItem = document.getElementById(`progress-${plugin}-${target}`);
if (progressItem) {
const indicator = progressItem.querySelector('.progress-indicator');
indicator.className = 'progress-indicator'; // Reset classes
switch (status) {
case 'building':
indicator.classList.add('spinner-icon');
break;
case 'success':
indicator.classList.add('success-icon');
break;
case 'failure':
indicator.classList.add('failure-icon');
break;
const statusSpan = progressItem.querySelector('.progress-status');
statusSpan.className = `progress-status ${status}`;
statusSpan.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
const label = progressItem.querySelector('span');
if (message) {
label.textContent = `${plugin} - ${target}: ${message}`;
}
}
} else if (typeof progress === 'object' && progress.message) {
// Handle final completion message
if (progress.message.includes('✅')) {
fillModalTitle.textContent = 'Fill Complete!';
}
} else {
// Handle simple string progress messages
const p = document.createElement('p');
p.textContent = progress;
fillProgressList.appendChild(p);
}
});
}
@@ -512,6 +595,13 @@ function displayBundleInfo(report) {
validationTabLink.classList.add('hidden');
}
// Temporarily disabled to fix bundle opening hang
// TODO: Re-enable after debugging fillable targets functionality
// loadFillableTargets().catch(error => {
// console.error('Failed to load fillable targets:', error);
// // Don't block bundle opening if fill targets fail to load
// });
// Reset to overview tab by default
switchTab('overview-tab');
}

View File

@@ -418,6 +418,166 @@ body {
display: none !important;
}
/* Fill Tab Styles */
#fill-tab {
max-height: calc(100vh - 200px);
overflow-y: auto;
padding-right: 8px;
}
.fill-header {
margin-bottom: 20px;
position: sticky;
top: 0;
background-color: var(--main-bg);
z-index: 10;
padding-bottom: 10px;
}
.fill-header h3 {
margin-bottom: 8px;
color: var(--text-color);
}
.fill-header p {
color: var(--text-light);
margin-bottom: 12px;
}
.fill-header-actions {
margin-top: 12px;
}
#fill-targets-container {
margin-bottom: 20px;
}
#fill-plugins-tables {
margin-bottom: 20px;
}
.fill-plugin-table {
margin-bottom: 24px;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.fill-plugin-header {
background-color: var(--bg-color);
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
font-weight: 600;
color: var(--text-color);
}
.fill-targets-table {
width: 100%;
border-collapse: collapse;
}
.fill-targets-table th,
.fill-targets-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.fill-targets-table th {
background-color: var(--sidebar-bg);
font-weight: 600;
color: var(--text-color);
}
.fill-targets-table tr:last-child td {
border-bottom: none;
}
.fill-targets-table tr:hover {
background-color: rgba(52, 152, 219, 0.05);
}
.fill-target-checkbox {
margin-right: 8px;
}
.fill-actions {
display: flex;
gap: 12px;
align-items: center;
padding: 16px 0;
border-top: 1px solid var(--border-color);
margin-top: 20px;
}
.action-button.secondary {
background-color: var(--sidebar-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.action-button.secondary:hover {
background-color: var(--border-color);
}
.action-button.primary {
background-color: var(--primary-color);
color: white;
border: 1px solid var(--primary-color);
}
.action-button.primary:hover {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
}
#fill-progress-container {
margin-top: 20px;
padding: 16px;
background-color: var(--sidebar-bg);
border-radius: 8px;
border: 1px solid var(--border-color);
}
#fill-progress-content {
margin-top: 12px;
}
.progress-item {
padding: 8px 0;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 12px;
}
.progress-item:last-child {
border-bottom: none;
}
.progress-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.progress-status.building {
background-color: #fef3c7;
color: #92400e;
}
.progress-status.success {
background-color: #d1fae5;
color: #065f46;
}
.progress-status.failure {
background-color: #fee2e2;
color: #991b1b;
}
/* Save Options Modal */
.save-options {
display: flex;

View File

@@ -973,6 +973,10 @@ def fill_bundle(bundle_path: Path, targets_to_build: dict, progress_callback: Op
# No fallback to print() - all output goes through callback only
staging_dir = Path(tempfile.mkdtemp(prefix="fourdst_fill_"))
successful_builds = 0
failed_builds = 0
build_details = []
try:
report_progress("Unpacking bundle to temporary directory...")
with zipfile.ZipFile(bundle_path, 'r') as bundle_zip:
@@ -1026,6 +1030,14 @@ def fill_bundle(bundle_path: Path, targets_to_build: dict, progress_callback: Op
}
plugin_info.setdefault('binaries', []).append(new_binary_entry)
successful_builds += 1
build_details.append({
'plugin': plugin_name,
'target': target_triplet,
'status': 'success',
'filename': tagged_filename
})
report_progress({
'status': 'success',
'plugin': plugin_name,
@@ -1034,6 +1046,14 @@ def fill_bundle(bundle_path: Path, targets_to_build: dict, progress_callback: Op
})
except Exception as e:
failed_builds += 1
build_details.append({
'plugin': plugin_name,
'target': target_triplet,
'status': 'failure',
'error': str(e)
})
report_progress({
'status': 'failure',
'plugin': plugin_name,