×
Fill Bundle
diff --git a/electron/renderer.js b/electron/renderer.js
index 63d943a..5144f6f 100644
--- a/electron/renderer.js
+++ b/electron/renderer.js
@@ -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,160 +116,230 @@ function setupEventListeners() {
saveMetadataBtn.addEventListener('click', showSaveOptionsModal);
overwriteBundleBtn.addEventListener('click', () => handleSaveMetadata(false));
saveAsNewBtn.addEventListener('click', () => handleSaveMetadata(true));
- fillBundleBtn.addEventListener('click', async () => {
+
+ // 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;
+ }
+
if (!currentBundlePath) {
- showModal('Error', 'No bundle is currently open.');
+ console.log('No bundle path, showing no targets message');
+ hideAllFillStates();
+ fillNoTargets.classList.remove('hidden');
return;
}
- showSpinner();
- const result = await ipcRenderer.invoke('get-fillable-targets', currentBundlePath);
- hideSpinner();
+
+ 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);
+ console.log('get-fillable-targets result:', result);
- if (!result.success) {
- showModal('Error', `Failed to get 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;
- }
-
- populateFillTargetsList(targets);
- fillModal.style.display = 'block';
- });
-
- closeFillModalButton.addEventListener('click', () => {
- fillModal.style.display = 'none';
- });
-
- 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);
-
- 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);
- });
+ if (!result.success) {
+ console.log('get-fillable-targets failed:', result.error);
+ hideAllFillStates();
+ showModal('Error', `Failed to load fillable targets: ${result.error}`);
+ return;
}
+
+ const targets = result.data;
+ 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;
}
- // Reset view
- fillModalBody.style.display = 'block';
- fillProgressView.style.display = 'none';
+ }
+
+ // Helper function to hide all fill tab states
+ function hideAllFillStates() {
+ fillLoading.classList.add('hidden');
+ fillNoTargets.classList.add('hidden');
+ fillTargetsContent.classList.add('hidden');
}
- startFillButton.addEventListener('click', async () => {
- const selectedTargets = {};
- const checkboxes = fillTargetsList.querySelectorAll('input[type="checkbox"]:checked');
+ // Old modal code removed - now using tab-based interface
- if (checkboxes.length === 0) {
- showModal('Info', 'No targets selected to fill.');
- return;
- }
-
- checkboxes.forEach(cb => {
- const pluginName = cb.dataset.pluginName;
- if (!selectedTargets[pluginName]) {
- selectedTargets[pluginName] = [];
- }
- selectedTargets[pluginName].push(JSON.parse(cb.dataset.targetInfo));
- });
-
- fillModalBody.style.display = 'none';
- fillProgressView.style.display = 'block';
- fillModalTitle.textContent = 'Filling Bundle...';
- populateFillProgressList(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);
- }
- });
-
- function populateFillProgressList(plugins) {
- fillProgressList.innerHTML = '';
+ // Create modern table-based interface for fillable targets
+ function populateFillTargetsTable(plugins) {
+ fillPluginsTables.innerHTML = '';
+
for (const [pluginName, targets] of Object.entries(plugins)) {
- 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';
+ if (targets.length > 0) {
+ // Create plugin table container
+ const pluginTable = document.createElement('div');
+ pluginTable.className = 'fill-plugin-table';
- const label = document.createElement('span');
- label.textContent = `${pluginName} - ${target.triplet}`;
+ // 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 = `
+
+ |
+
+ |
+ Target Platform |
+ Architecture |
+ Type |
+ Compiler |
+
+ `;
+ table.appendChild(thead);
+
+ // Table body
+ const tbody = document.createElement('tbody');
+ targets.forEach(target => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+
+
+ |
+ ${target.triplet} |
+ ${target.arch} |
+ ${target.type} |
+ ${target.details?.compiler || 'N/A'} ${target.details?.compiler_version || ''} |
+ `;
+ tbody.appendChild(row);
+ });
+ table.appendChild(tbody);
+
+ pluginTable.appendChild(table);
+ fillPluginsTables.appendChild(pluginTable);
+ }
+ }
+
+ // Add event listeners for select all functionality
+ setupFillTargetEventListeners();
+ }
- item.appendChild(indicator);
- item.appendChild(label);
- fillProgressList.appendChild(item);
+ // 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 = document.querySelectorAll('.fill-target-checkbox:checked');
+
+ if (checkboxes.length === 0) {
+ showModal('Info', 'No targets selected to fill.');
+ return;
+ }
+
+ checkboxes.forEach(cb => {
+ const pluginName = cb.dataset.plugin;
+ const target = JSON.parse(cb.dataset.target);
+ if (!selectedTargets[pluginName]) {
+ selectedTargets[pluginName] = [];
+ }
+ selectedTargets[pluginName].push(target);
+ });
+
+ // 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
+ });
+
+ if (!result.success) {
+ const errorItem = document.createElement('div');
+ errorItem.className = 'progress-item';
+ errorItem.innerHTML = `
+ Error
+ Fill process failed: ${result.error}
+ `;
+ fillProgressContent.appendChild(errorItem);
+ }
+ });
+ }
+
+ // Create progress display for fill process
+ function populateFillProgress(selectedTargets) {
+ fillProgressContent.innerHTML = '';
+
+ for (const [pluginName, targets] of Object.entries(selectedTargets)) {
+ targets.forEach(target => {
+ const progressItem = document.createElement('div');
+ progressItem.className = 'progress-item';
+ progressItem.id = `progress-${pluginName}-${target.triplet}`;
+ progressItem.innerHTML = `
+ Building
+ ${pluginName} - ${target.triplet}
+ `;
+ 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 label = progressItem.querySelector('span');
- if (message) {
- label.textContent = `${plugin} - ${target}: ${message}`;
- }
+ const statusSpan = progressItem.querySelector('.progress-status');
+ statusSpan.className = `progress-status ${status}`;
+ statusSpan.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
- } 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');
}
diff --git a/electron/styles.css b/electron/styles.css
index 00148a9..22fd5a1 100644
--- a/electron/styles.css
+++ b/electron/styles.css
@@ -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;
diff --git a/fourdst/core/bundle.py b/fourdst/core/bundle.py
index cefefcb..156d1a1 100644
--- a/fourdst/core/bundle.py
+++ b/fourdst/core/bundle.py
@@ -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,