Add complete manufacturing build pipeline with enhanced features

Build Pipeline:
- Generate schematic PDF with all sheets
- Generate 11 individual board layer PDFs (copper, silkscreen, soldermask, paste, fabrication, outline)
- Add "Layer: <name>" text overlay to each board layer PDF
- Merge all board PDFs into single document
- Generate Gerbers with all layers
- Generate drill files in Excellon format
- Generate ODB++ package (optional)
- Export STEP 3D model
- Generate BOMs for active variant
- Package all outputs into timestamped ZIP file
- Real-time progress bar (0-100%) with status updates
- Detailed build log with timestamps
- Auto-download ZIP on completion

Symbol & Footprint Library Integration:
- Browse KiCad symbol libraries from UM_KICAD environment variable
- Live SVG preview of selected symbols with pins, graphics, and labels
- Browse KiCad footprint libraries (.pretty directories)
- Live SVG preview of selected footprints with pads and silkscreen
- Associate symbols and footprints with parts in database
- Store as LibraryName:ComponentName format

WebSocket Connection Improvements:
- Increase ping timeout to 120 seconds (from 60s default)
- Add 25-second ping interval to keep connections alive
- Wait 10 seconds for reconnection before shutdown (handles page refresh)
- Cancel shutdown timer when client reconnects
- Use hidden link download to preserve WebSocket connection (not window.location)

PDF Text Overlay:
- Add reportlab and PyPDF2 imports for PDF manipulation
- Add add_text_overlay_to_pdf() helper function
- Overlay layer names in upper left corner of board PDFs
- Use Helvetica-Bold 14pt font at position (50, 750)

Bug Fixes:
- Fix BOM generator argument order (schematic, project, variant, dnp_uuids, pcb_file)
- Pass empty JSON array '[]' for dnp_uuids instead of output directory
- Move generated BOM files from project dir to output dir for packaging
- Fix datetime import (was missing)
- Use app_args instead of config for getting schematic/board file paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
brentperteet
2026-02-23 13:01:35 -06:00
parent bcb2c70e93
commit 0de59dabfc
2 changed files with 1190 additions and 19 deletions

View File

@@ -356,16 +356,34 @@
<p>Generate all manufacturing outputs in a single operation with progress feedback.</p>
<div class="actions">
<h3>Coming Soon</h3>
<p>This tab will allow you to:</p>
<ul style="margin-left: 20px; line-height: 1.8;">
<li>Generate PDFs (Schematic + Board)</li>
<li>Export Gerbers & Drill Files</li>
<li>Export STEP model</li>
<li>Generate BOM</li>
<li>Run design checks</li>
</ul>
<p style="margin-top: 20px;">All with a single button press and real-time progress tracking.</p>
<button id="buildAllBtn" class="btn" style="font-size: 16px; padding: 12px 24px; margin: 20px 0;">
🚀 Build All Manufacturing Outputs
</button>
<div id="buildProgress" style="display: none; margin-top: 30px;">
<h3>Build Progress</h3>
<!-- Progress Bar -->
<div style="background: #e0e0e0; border-radius: 10px; height: 30px; margin: 20px 0; position: relative; overflow: hidden;">
<div id="buildProgressBar" style="background: linear-gradient(90deg, #007bff, #0056b3); height: 100%; width: 0%; transition: width 0.3s ease; display: flex; align-items: center; justify-content: center;">
<span id="buildProgressPercent" style="color: white; font-weight: bold; font-size: 14px; position: absolute;"></span>
</div>
</div>
<!-- Current Status -->
<div id="buildStatus" style="padding: 15px; background: #f8f9fa; border-left: 4px solid #007bff; border-radius: 5px; margin-bottom: 20px;">
<strong>Status:</strong> <span id="buildStatusText">Initializing...</span>
</div>
<!-- Build Steps Log -->
<div style="background: #f8f9fa; border-radius: 8px; padding: 15px; max-height: 300px; overflow-y: auto;">
<h4 style="margin-top: 0;">Build Log:</h4>
<div id="buildLog" style="font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.6;">
</div>
</div>
</div>
<div id="buildMessage" class="message" style="margin-top: 20px;"></div>
</div>
</div>
</div><!-- End publishTab -->
@@ -427,6 +445,43 @@
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Datasheet URL (optional):</label>
<input type="text" id="datasheetInput" placeholder="https://..." style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
</div>
<!-- Symbol Selection -->
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Symbol (optional):</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div>
<select id="symbolLibrarySelect" style="width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
<option value="">-- Select Library --</option>
</select>
<select id="symbolSelect" style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;" disabled>
<option value="">-- Select Symbol --</option>
</select>
</div>
<div id="symbolPreview" style="border: 1px solid #dee2e6; border-radius: 5px; padding: 10px; background: white; min-height: 150px; display: flex; align-items: center; justify-content: center; overflow: auto;">
<span style="color: #999;">No symbol selected</span>
</div>
</div>
</div>
<!-- Footprint Selection -->
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Footprint (optional):</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div>
<select id="footprintLibrarySelect" style="width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
<option value="">-- Select Library --</option>
</select>
<select id="footprintSelect" style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;" disabled>
<option value="">-- Select Footprint --</option>
</select>
</div>
<div id="footprintPreview" style="border: 1px solid #dee2e6; border-radius: 5px; padding: 10px; background: #2C2C2C; min-height: 150px; display: flex; align-items: center; justify-content: center; overflow: auto;">
<span style="color: #999;">No footprint selected</span>
</div>
</div>
</div>
<button id="createPartBtn" class="btn" disabled>Create Part</button>
<button id="cancelCreatePartBtn" class="btn secondary" style="margin-left: 10px;">Cancel</button>
</div>
@@ -990,7 +1045,21 @@
const datasheetInput = document.getElementById('datasheetInput');
const createPartBtn = document.getElementById('createPartBtn');
// Symbol and Footprint selection elements
const symbolLibrarySelect = document.getElementById('symbolLibrarySelect');
const symbolSelect = document.getElementById('symbolSelect');
const symbolPreview = document.getElementById('symbolPreview');
const footprintLibrarySelect = document.getElementById('footprintLibrarySelect');
const footprintSelect = document.getElementById('footprintSelect');
const footprintPreview = document.getElementById('footprintPreview');
let missingPartsData = [];
let symbolLibraries = [];
let footprintLibraries = [];
let selectedSymbolLibraryPath = '';
let selectedFootprintLibraryPath = '';
let selectedSymbol = '';
let selectedFootprint = '';
function showCreatePartMessage(text, type) {
createPartMessage.textContent = text;
@@ -1006,6 +1075,9 @@
createPartForm.style.display = 'none';
createPartMessage.style.display = 'none';
socket.emit('get_missing_ipns');
// Load symbol and footprint libraries
socket.emit('get_symbol_libraries');
socket.emit('get_footprint_libraries');
});
// Cancel and return to browser
@@ -1020,10 +1092,142 @@
manufacturerInput.value = '';
mpnInput.value = '';
datasheetInput.value = '';
symbolLibrarySelect.value = '';
symbolSelect.value = '';
symbolSelect.disabled = true;
symbolPreview.innerHTML = '<span style="color: #999;">No symbol selected</span>';
footprintLibrarySelect.value = '';
footprintSelect.value = '';
footprintSelect.disabled = true;
footprintPreview.innerHTML = '<span style="color: #999;">No footprint selected</span>';
selectedSymbol = '';
selectedFootprint = '';
createPartBtn.disabled = true;
missingPartsData = [];
});
// Symbol library handlers
socket.on('symbol_libraries_result', (data) => {
symbolLibraries = data.libraries;
symbolLibrarySelect.innerHTML = '<option value="">-- Select Library --</option>';
for (const lib of data.libraries) {
const option = document.createElement('option');
option.value = lib.path;
option.textContent = lib.name;
symbolLibrarySelect.appendChild(option);
}
});
symbolLibrarySelect.addEventListener('change', () => {
const libraryPath = symbolLibrarySelect.value;
if (!libraryPath) {
symbolSelect.innerHTML = '<option value="">-- Select Symbol --</option>';
symbolSelect.disabled = true;
symbolPreview.innerHTML = '<span style="color: #999;">No symbol selected</span>';
selectedSymbol = '';
return;
}
selectedSymbolLibraryPath = libraryPath;
symbolSelect.innerHTML = '<option value="">Loading...</option>';
symbolSelect.disabled = true;
socket.emit('get_symbols_in_library', { library_path: libraryPath });
});
socket.on('symbols_in_library_result', (data) => {
symbolSelect.innerHTML = '<option value="">-- Select Symbol --</option>';
for (const symbol of data.symbols) {
const option = document.createElement('option');
option.value = symbol;
option.textContent = symbol;
symbolSelect.appendChild(option);
}
symbolSelect.disabled = false;
});
symbolSelect.addEventListener('change', () => {
const symbolName = symbolSelect.value;
if (!symbolName) {
symbolPreview.innerHTML = '<span style="color: #999;">No symbol selected</span>';
selectedSymbol = '';
return;
}
symbolPreview.innerHTML = '<span style="color: #999;">Loading...</span>';
socket.emit('render_symbol', {
library_path: selectedSymbolLibraryPath,
symbol_name: symbolName
});
});
socket.on('symbol_render_result', (data) => {
symbolPreview.innerHTML = data.svg;
// Store as LibraryName:SymbolName format
const libName = symbolLibrarySelect.options[symbolLibrarySelect.selectedIndex].text;
selectedSymbol = `${libName}:${data.symbol_name}`;
});
// Footprint library handlers
socket.on('footprint_libraries_result', (data) => {
footprintLibraries = data.libraries;
footprintLibrarySelect.innerHTML = '<option value="">-- Select Library --</option>';
for (const lib of data.libraries) {
const option = document.createElement('option');
option.value = lib.path;
option.textContent = lib.name;
footprintLibrarySelect.appendChild(option);
}
});
footprintLibrarySelect.addEventListener('change', () => {
const libraryPath = footprintLibrarySelect.value;
if (!libraryPath) {
footprintSelect.innerHTML = '<option value="">-- Select Footprint --</option>';
footprintSelect.disabled = true;
footprintPreview.innerHTML = '<span style="color: #999;">No footprint selected</span>';
selectedFootprint = '';
return;
}
selectedFootprintLibraryPath = libraryPath;
footprintSelect.innerHTML = '<option value="">Loading...</option>';
footprintSelect.disabled = true;
socket.emit('get_footprints_in_library', { library_path: libraryPath });
});
socket.on('footprints_in_library_result', (data) => {
footprintSelect.innerHTML = '<option value="">-- Select Footprint --</option>';
for (const footprint of data.footprints) {
const option = document.createElement('option');
option.value = footprint;
option.textContent = footprint;
footprintSelect.appendChild(option);
}
footprintSelect.disabled = false;
});
footprintSelect.addEventListener('change', () => {
const footprintName = footprintSelect.value;
if (!footprintName) {
footprintPreview.innerHTML = '<span style="color: #999;">No footprint selected</span>';
selectedFootprint = '';
return;
}
footprintPreview.innerHTML = '<span style="color: #999;">Loading...</span>';
socket.emit('render_footprint', {
library_path: selectedFootprintLibraryPath,
footprint_name: footprintName
});
});
socket.on('footprint_render_result', (data) => {
footprintPreview.innerHTML = data.svg;
// Store as LibraryName:FootprintName format
const libName = footprintLibrarySelect.options[footprintLibrarySelect.selectedIndex].text;
selectedFootprint = `${libName}:${data.footprint_name}`;
});
socket.on('missing_ipns_result', (data) => {
createPartFormLoading.style.display = 'none';
@@ -1085,7 +1289,9 @@
class: part.class,
manufacturer: part.manufacturer,
mpn: part.mpn,
datasheet: datasheetInput.value.trim()
datasheet: datasheetInput.value.trim(),
symbol: selectedSymbol,
footprint: selectedFootprint
});
});
@@ -1110,6 +1316,16 @@
manufacturerInput.value = '';
mpnInput.value = '';
datasheetInput.value = '';
symbolLibrarySelect.value = '';
symbolSelect.value = '';
symbolSelect.disabled = true;
symbolPreview.innerHTML = '<span style="color: #999;">No symbol selected</span>';
footprintLibrarySelect.value = '';
footprintSelect.value = '';
footprintSelect.disabled = true;
footprintPreview.innerHTML = '<span style="color: #999;">No footprint selected</span>';
selectedSymbol = '';
selectedFootprint = '';
createPartBtn.disabled = true;
if (missingPartsData.length === 0) {
@@ -1118,6 +1334,77 @@
}
});
// ========================================
// Build Pipeline
// ========================================
const buildAllBtn = document.getElementById('buildAllBtn');
const buildProgress = document.getElementById('buildProgress');
const buildProgressBar = document.getElementById('buildProgressBar');
const buildProgressPercent = document.getElementById('buildProgressPercent');
const buildStatusText = document.getElementById('buildStatusText');
const buildLog = document.getElementById('buildLog');
const buildMessage = document.getElementById('buildMessage');
function showBuildMessage(text, type) {
buildMessage.textContent = text;
buildMessage.className = 'message ' + type;
buildMessage.style.display = 'block';
}
function addBuildLogEntry(text, isError = false) {
const timestamp = new Date().toLocaleTimeString();
const color = isError ? '#dc3545' : '#333';
buildLog.innerHTML += `<div style="color: ${color};">[${timestamp}] ${text}</div>`;
buildLog.parentElement.scrollTop = buildLog.parentElement.scrollHeight;
}
function updateBuildProgress(percent, status) {
buildProgressBar.style.width = percent + '%';
buildProgressPercent.textContent = percent + '%';
buildStatusText.textContent = status;
}
buildAllBtn.addEventListener('click', () => {
buildAllBtn.disabled = true;
buildProgress.style.display = 'block';
buildMessage.style.display = 'none';
buildLog.innerHTML = '';
updateBuildProgress(0, 'Starting build...');
addBuildLogEntry('Build pipeline started');
socket.emit('build_all');
});
socket.on('build_progress', (data) => {
updateBuildProgress(data.percent, data.status);
addBuildLogEntry(data.message);
});
socket.on('build_complete', (data) => {
updateBuildProgress(100, 'Build complete!');
addBuildLogEntry('✓ Build completed successfully');
buildAllBtn.disabled = false;
// Auto-download the ZIP file using hidden link (preserves WebSocket connection)
if (data.download_url) {
addBuildLogEntry('Downloading build package...');
const link = document.createElement('a');
link.href = data.download_url;
link.download = data.filename || 'manufacturing.zip';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showBuildMessage('Build complete! Download started automatically.', 'success');
}
});
socket.on('build_error', (data) => {
addBuildLogEntry('✗ Error: ' + data.error, true);
showBuildMessage('Build failed: ' + data.error, 'error');
buildAllBtn.disabled = false;
});
// ========================================
// Variant Manager
// ========================================