LP
libplugin Bundle Manager
-
Create, manage, and analyze stellar evolution bundles
+
Create, manage, and edit plugin bundles build with libplugin.
@@ -172,22 +172,22 @@
Access fundamental physical constants used in stellar evolution calculations.
-
SL
SERiF Libraries
-
Stellar Evolution Rapid interpolation Framework
+
Stellar Evolution and Rotation in 4D
@@ -279,23 +271,15 @@
diff --git a/electron/main/app-lifecycle.js b/electron/main/app-lifecycle.js
index 827a6b8..6098e08 100644
--- a/electron/main/app-lifecycle.js
+++ b/electron/main/app-lifecycle.js
@@ -7,6 +7,7 @@ if (require('electron-squirrel-startup')) {
}
let mainWindow;
+let themeUpdateListener;
const createWindow = () => {
// Create the browser window.
@@ -29,10 +30,34 @@ const createWindow = () => {
// Open the DevTools for debugging
// mainWindow.webContents.openDevTools();
- nativeTheme.on('updated', () => {
- if (mainWindow) {
- mainWindow.webContents.send('theme-updated', { shouldUseDarkColors: nativeTheme.shouldUseDarkColors });
+ // Clean up any existing theme listener
+ if (themeUpdateListener) {
+ nativeTheme.removeListener('updated', themeUpdateListener);
+ }
+
+ // Create new theme listener with proper safety checks
+ themeUpdateListener = () => {
+ if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents) {
+ try {
+ mainWindow.webContents.send('theme-updated', { shouldUseDarkColors: nativeTheme.shouldUseDarkColors });
+ } catch (error) {
+ console.warn('Failed to send theme update:', error.message);
+ // Remove the listener if sending fails
+ nativeTheme.removeListener('updated', themeUpdateListener);
+ themeUpdateListener = null;
+ }
}
+ };
+
+ nativeTheme.on('updated', themeUpdateListener);
+
+ // Clean up when window is closed
+ mainWindow.on('closed', () => {
+ if (themeUpdateListener) {
+ nativeTheme.removeListener('updated', themeUpdateListener);
+ themeUpdateListener = null;
+ }
+ mainWindow = null;
});
};
diff --git a/electron/main/backend-bridge.js b/electron/main/backend-bridge.js
index d2eda39..4954cf0 100644
--- a/electron/main/backend-bridge.js
+++ b/electron/main/backend-bridge.js
@@ -22,8 +22,52 @@ function runPythonCommand(command, kwargs, event) {
let errorOutput = '';
process.stderr.on('data', (data) => {
- errorOutput += data.toString();
- console.error('Backend STDERR:', data.toString().trim());
+ const stderrChunk = data.toString();
+ errorOutput += stderrChunk;
+ console.error('Backend STDERR:', stderrChunk.trim());
+
+ // For fill_bundle, forward stderr to frontend for terminal display
+ if (isStreaming && event && command === 'fill_bundle') {
+ // Parse stderr lines and send them as progress updates
+ const lines = stderrChunk.split('\n').filter(line => line.trim());
+ lines.forEach(line => {
+ const trimmedLine = line.trim();
+
+ // Check if this is a structured progress message
+ if (trimmedLine.startsWith('[PROGRESS] {')) {
+ try {
+ // Extract JSON from [PROGRESS] prefix
+ const jsonStr = trimmedLine.substring('[PROGRESS] '.length);
+ const progressData = JSON.parse(jsonStr);
+ console.log(`[MAIN_PROCESS] Parsed progress data:`, progressData);
+
+ // Send as proper progress update
+ event.sender.send('fill-bundle-progress', progressData);
+ } catch (e) {
+ console.error(`[MAIN_PROCESS] Failed to parse progress JSON: ${trimmedLine}`, e);
+ // Fallback to stderr if JSON parsing fails
+ event.sender.send('fill-bundle-progress', {
+ type: 'stderr',
+ stderr: trimmedLine
+ });
+ }
+ } else {
+ // Only skip very specific system messages, include everything else as stderr
+ const shouldSkip = trimmedLine.includes('[BRIDGE_INFO]') ||
+ trimmedLine.includes('--- Python backend bridge') ||
+ trimmedLine.startsWith('[PROGRESS]') || // Skip non-JSON progress messages
+ trimmedLine === '';
+
+ if (!shouldSkip) {
+ console.log(`[MAIN_PROCESS] Forwarding stderr to frontend: ${trimmedLine}`);
+ event.sender.send('fill-bundle-progress', {
+ type: 'stderr',
+ stderr: trimmedLine
+ });
+ }
+ }
+ });
+ }
});
const isStreaming = command === 'fill_bundle';
diff --git a/electron/main/ipc-handlers.js b/electron/main/ipc-handlers.js
index ea9e1d2..6be87a7 100644
--- a/electron/main/ipc-handlers.js
+++ b/electron/main/ipc-handlers.js
@@ -1,6 +1,7 @@
-const { ipcMain, dialog } = require('electron');
+const { ipcMain, dialog, shell } = require('electron');
const { runPythonCommand } = require('./backend-bridge');
const fs = require('fs-extra');
+const path = require('path');
const setupBundleIPCHandlers = () => {
// Create bundle handler
@@ -121,6 +122,52 @@ const setupBundleIPCHandlers = () => {
return { success: false, error: error.message };
}
});
+
+ // Read license file handler
+ ipcMain.handle('read-license', async () => {
+ try {
+ const licensePath = path.join(__dirname, '..', 'LICENSE.txt');
+ console.log(`[IPC_HANDLER] Reading license from: ${licensePath}`);
+
+ // Check if file exists
+ const exists = await fs.pathExists(licensePath);
+ console.log(`[IPC_HANDLER] License file exists: ${exists}`);
+
+ if (!exists) {
+ return {
+ success: false,
+ error: 'License file not found',
+ content: `License file not found at: ${licensePath}`
+ };
+ }
+
+ const licenseContent = await fs.readFile(licensePath, 'utf8');
+ console.log(`[IPC_HANDLER] License content length: ${licenseContent.length} characters`);
+ console.log(`[IPC_HANDLER] License starts with: "${licenseContent.substring(0, 100)}..."`);
+ console.log(`[IPC_HANDLER] License ends with: "...${licenseContent.substring(licenseContent.length - 100)}"`);
+
+ return { success: true, content: licenseContent };
+ } catch (error) {
+ console.error('Failed to read LICENSE.txt:', error);
+ return {
+ success: false,
+ error: 'Could not load license file',
+ content: 'GPL v3 license text could not be loaded. Please check that LICENSE.txt exists in the application directory.'
+ };
+ }
+ });
+
+ // Open external URL handler
+ ipcMain.handle('open-external-url', async (event, url) => {
+ try {
+ console.log(`[IPC_HANDLER] Opening external URL: ${url}`);
+ await shell.openExternal(url);
+ return { success: true };
+ } catch (error) {
+ console.error('Failed to open external URL:', error);
+ return { success: false, error: error.message };
+ }
+ });
};
module.exports = {
diff --git a/electron/renderer/event-handlers.js b/electron/renderer/event-handlers.js
index a2712d7..ff315ca 100644
--- a/electron/renderer/event-handlers.js
+++ b/electron/renderer/event-handlers.js
@@ -207,7 +207,7 @@ function setupInfoModal() {
// Info tab navigation
infoTabLinks.forEach(link => {
- link.addEventListener('click', (e) => {
+ link.addEventListener('click', async (e) => {
e.preventDefault();
const targetTab = link.dataset.tab;
@@ -218,6 +218,14 @@ function setupInfoModal() {
link.classList.add('active');
const targetPane = document.getElementById(targetTab);
if (targetPane) targetPane.classList.add('active');
+
+ // Load license content when license tab is clicked
+ if (targetTab === 'license-info-tab') {
+ console.log('[FRONTEND] License tab clicked, loading content...');
+ await loadLicenseContent();
+ } else {
+ console.log(`[FRONTEND] Tab clicked: ${targetTab}`);
+ }
});
});
@@ -226,7 +234,9 @@ function setupInfoModal() {
if (githubLink) {
githubLink.addEventListener('click', (e) => {
e.preventDefault();
- ipcRenderer.invoke('open-external-url', 'https://github.com/tboudreaux/4DSTAR');
+ // Get the URL from the href attribute instead of hardcoding
+ const url = githubLink.getAttribute('href');
+ ipcRenderer.invoke('open-external-url', url);
});
}
}
@@ -237,6 +247,47 @@ function hideInfoModal() {
if (infoModal) infoModal.classList.add('hidden');
}
+// Load license content from LICENSE.txt file
+async function loadLicenseContent() {
+ console.log('[FRONTEND] loadLicenseContent() called');
+ const licenseTextarea = document.querySelector('.license-text');
+ console.log('[FRONTEND] License textarea found:', licenseTextarea);
+ if (!licenseTextarea) {
+ console.error('[FRONTEND] License textarea not found!');
+ return;
+ }
+
+ // Don't reload if already loaded (not placeholder)
+ if (licenseTextarea.value && !licenseTextarea.value.includes('GPL v3 license text will be pasted here')) {
+ return;
+ }
+
+ try {
+ console.log('[FRONTEND] Requesting license content...');
+ const result = await ipcRenderer.invoke('read-license');
+ console.log('[FRONTEND] License result:', result);
+
+ if (result.success) {
+ console.log(`[FRONTEND] License content length: ${result.content.length} characters`);
+ console.log(`[FRONTEND] License starts with: "${result.content.substring(0, 100)}..."`);
+ console.log(`[FRONTEND] License ends with: "...${result.content.substring(result.content.length - 100)}"`);
+
+ licenseTextarea.value = result.content;
+ licenseTextarea.placeholder = '';
+ // Scroll to the top to show the beginning of the license
+ licenseTextarea.scrollTop = 0;
+ } else {
+ licenseTextarea.value = result.content; // Fallback error message
+ licenseTextarea.placeholder = '';
+ console.error('Failed to load license:', result.error);
+ }
+ } catch (error) {
+ console.error('Error loading license content:', error);
+ licenseTextarea.value = 'Error loading license content. Please check that LICENSE.txt exists in the application directory.';
+ licenseTextarea.placeholder = '';
+ }
+}
+
// Handle save metadata with option for save as new
async function handleSaveMetadata(saveAsNew = false) {
const currentBundlePath = stateManager.getCurrentBundlePath();
diff --git a/electron/renderer/fill-workflow.js b/electron/renderer/fill-workflow.js
index e9d8649..0073ce9 100644
--- a/electron/renderer/fill-workflow.js
+++ b/electron/renderer/fill-workflow.js
@@ -185,7 +185,46 @@ function setupFillTargetEventListeners() {
return;
}
- await startFillProcess(selectedTargetsByPlugin);
+ // 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);
+ }
});
}
@@ -198,15 +237,35 @@ function populateFillProgress(selectedTargetsByPlugin) {
targets.forEach(target => {
const progressItem = document.createElement('div');
progressItem.className = 'fill-progress-item';
- progressItem.id = `progress-${target.triplet}`;
+ // Create unique ID using both plugin and target to avoid duplicates
+ progressItem.id = `progress-${pluginName}-${target.triplet}`;
progressItem.innerHTML = `
${pluginName}: ${target.triplet}
-
Waiting...
+
+
+ Building...
+
+
`;
elements.fillProgressContent.appendChild(progressItem);
+
+ // 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';
+ });
});
});
}
@@ -261,33 +320,98 @@ async function startFillProcess(selectedTargetsByPlugin) {
}
}
+// Track current building target for stderr routing
+let currentBuildingTarget = null;
+
// Update progress display during fill process
function updateFillProgress(progressData) {
console.log('Fill progress update:', progressData);
- if (progressData.target) {
- const progressItem = document.getElementById(`progress-${progressData.target}`);
+ // 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);
+
if (progressItem) {
const statusElement = progressItem.querySelector('.progress-status');
const progressBar = progressItem.querySelector('.progress-fill');
+ const logsContainer = progressItem.querySelector('.progress-logs');
+ const terminalOutput = progressItem.querySelector('.terminal-output');
if (progressData.status) {
- statusElement.textContent = progressData.status;
-
- // Update progress bar based on status
+ console.log(`[FRONTEND] Updating status to: ${progressData.status}`);
+ // Update progress bar and status based on status
let percentage = 0;
- switch (progressData.status) {
- case 'Building':
+ switch (progressData.status.toLowerCase()) {
+ case 'building':
percentage = 50;
+ statusElement.className = 'progress-status building';
+ statusElement.innerHTML = '
Building...';
progressItem.className = 'fill-progress-item building';
+ // Track this target as currently building for stderr routing
+ currentBuildingTarget = progressData.target;
+ console.log(`[FRONTEND] Now building target: ${currentBuildingTarget}`);
break;
- case 'Success':
+ case 'success':
percentage = 100;
+ statusElement.className = 'progress-status success';
+ statusElement.innerHTML = '✓ Built';
progressItem.className = 'fill-progress-item success';
+ 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`);
+ }
break;
- case 'Failed':
+ case 'failure':
+ case 'failed':
percentage = 100;
+ statusElement.className = 'progress-status failed';
+ statusElement.innerHTML = '✗ Failed';
progressItem.className = 'fill-progress-item failed';
+ 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`);
+ }
break;
}
@@ -295,6 +419,15 @@ function updateFillProgress(progressData) {
progressBar.style.width = `${percentage}%`;
}
}
+
+ // 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;
+ }
}
}
}
diff --git a/electron/styles.css b/electron/styles.css
index 2a55dfc..eec52ba 100644
--- a/electron/styles.css
+++ b/electron/styles.css
@@ -1762,7 +1762,8 @@ body.dark-mode .save-option-btn small {
background-color: var(--primary-color-dark);
}
-.spinner {
+/* Old spinner - now unused, keeping for compatibility */
+.spinner-old {
position: absolute;
top: 50%;
left: 50%;
@@ -1949,6 +1950,145 @@ body.dark-mode .info-tab-pane a {
#opat-tab-content .tab-pane {
padding: 0;
}
+
+/* Fill Progress Enhancements */
+.progress-status {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-weight: 500;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 13px;
+}
+
+.progress-status.building {
+ color: #f59e0b;
+ background: rgba(245, 158, 11, 0.1);
+ border: 1px solid rgba(245, 158, 11, 0.3);
+}
+
+.progress-status.success {
+ color: #10b981;
+ background: rgba(16, 185, 129, 0.1);
+ border: 1px solid rgba(16, 185, 129, 0.3);
+}
+
+.progress-status.failed {
+ color: #ef4444;
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+}
+
+/* Spinner Animation */
+.spinner {
+ width: 12px;
+ height: 12px;
+ border: 1.5px solid rgba(245, 158, 11, 0.2);
+ border-top: 1.5px solid #f59e0b;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ flex-shrink: 0;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Progress Logs */
+.progress-logs {
+ margin-top: 8px;
+}
+
+.toggle-logs-btn {
+ background: transparent;
+ border: none;
+ padding: 4px 8px;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 11px;
+ color: var(--text-light);
+ transition: all 0.2s ease;
+ text-decoration: underline;
+}
+
+.toggle-logs-btn:hover {
+ color: var(--text-color);
+ background: var(--hover-color);
+}
+
+.logs-content {
+ margin-top: 6px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background: #1a1a1a;
+ max-height: 150px;
+ overflow: hidden;
+}
+
+.logs-content.hidden {
+ display: none;
+}
+
+.terminal-output {
+ background: #1a1a1a;
+ color: #e5e7eb;
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
+ font-size: 10px;
+ line-height: 1.3;
+ padding: 8px 10px;
+ margin: 0;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ max-height: 150px;
+ overflow-y: auto;
+ border-radius: 4px;
+}
+
+/* Fill progress item layout improvements */
+.fill-progress-item {
+ background: var(--background-color);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 12px;
+ margin-bottom: 12px;
+}
+
+.progress-target {
+ font-weight: 600;
+ margin-bottom: 6px;
+ color: var(--text-color);
+}
+
+.progress-bar {
+ background: var(--hover-color);
+ border-radius: 4px;
+ height: 6px;
+ margin: 8px 0;
+ overflow: hidden;
+}
+
+.progress-fill {
+ background: var(--primary-color);
+ height: 100%;
+ transition: width 0.3s ease;
+}
+
+/* Dark mode adjustments */
+body.dark-mode .toggle-logs-btn {
+ background: #374151;
+ border-color: #4b5563;
+ color: #e5e7eb;
+}
+
+body.dark-mode .toggle-logs-btn:hover {
+ background: #4b5563;
+}
+
+body.dark-mode .logs-content {
+ border-color: #4b5563;
+}
.opat-section {
margin-bottom: 20px;
padding: 16px;
@@ -2298,7 +2438,7 @@ body.dark-mode .opat-table-tag-highlight {
.license-text {
width: 100%;
- height: 200px;
+ height: 400px;
margin-top: 12px;
padding: 12px;
border: 1px solid var(--border-color);
@@ -2308,6 +2448,7 @@ body.dark-mode .opat-table-tag-highlight {
line-height: 1.4;
resize: vertical;
background-color: #f8f9fa;
+ overflow-y: auto;
}
.funding-content,