Add library browser, BOM generation, and UI improvements

- Add parts library browser with ODBC database search
- Implement hierarchical BOM generation with PCB side detection
- Add STEP export and PCB rendering functionality
- Adjust page width to 80% up to 1536px max
- Add library-only mode for standalone library browsing

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
brentperteet
2026-02-22 19:26:36 -06:00
parent ab8d5c0c14
commit 55235f222b
3 changed files with 1026 additions and 23 deletions

View File

@@ -8,7 +8,8 @@
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
width: 80%;
max-width: 1536px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
@@ -270,12 +271,21 @@
<h1>KiCad Manager</h1>
{% if library_only_mode %}
<div class="tabs">
<button class="tab active" onclick="switchTab('main')">Main</button>
<button class="tab" onclick="switchTab('variants')">Variant Manager</button>
<button class="tab active" onclick="switchTab('library')">Library</button>
</div>
{% else %}
<div class="tabs">
<button class="tab active" onclick="switchTab('debug')">Debug</button>
<button class="tab" onclick="switchTab('publish')">Publish</button>
<button class="tab" onclick="switchTab('library')">Library</button>
<button class="tab" onclick="switchTab('variants')">Variants</button>
</div>
{% endif %}
<div id="mainTab" class="tab-content active">
{% if not library_only_mode %}
<div id="debugTab" class="tab-content active">
<div class="command-container">
<h2>Invocation Command</h2>
@@ -298,10 +308,16 @@
<h2>Actions</h2>
<button id="generatePdfBtn" class="btn">Generate All PDFs (Schematic + Board Layers)</button>
<button id="generateGerbersBtn" class="btn" style="margin-left: 10px;">Generate Gerbers & Drill Files</button>
<button id="exportStepBtn" class="btn" style="margin-left: 10px;">Export PCB to STEP</button>
<button id="renderPcbBtn" class="btn" style="margin-left: 10px;">Render PCB Image</button>
<button id="generateBomBtn" class="btn" style="margin-left: 10px;">Generate BOM (All Variants)</button>
<button id="syncLibrariesBtn" class="btn" style="margin-left: 10px;">Sync Symbol Libraries</button>
<button id="syncDbBtn" class="btn" style="margin-left: 10px;">Sync Parts to Database (R & C)</button>
<div id="message" class="message"></div>
<div id="gerberMessage" class="message"></div>
<div id="stepMessage" class="message"></div>
<div id="renderMessage" class="message"></div>
<div id="bomMessage" class="message"></div>
<div id="syncMessage" class="message"></div>
<div id="dbSyncMessage" class="message"></div>
</div>
@@ -330,8 +346,51 @@
</div>
</div>
</div><!-- End mainTab -->
</div><!-- End debugTab -->
<!-- Publish Tab -->
<div id="publishTab" class="tab-content">
<div class="container">
<h2>Build & Publish</h2>
<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>
</div>
</div>
</div><!-- End publishTab -->
{% endif %}
<!-- Library Tab -->
<div id="libraryTab" class="tab-content {% if library_only_mode %}active{% endif %}">
<div class="container">
<h2>Parts Library Browser</h2>
<div style="margin: 20px 0;">
<input type="text" id="librarySearchInput" placeholder="Search by IPN, MPN, Manufacturer, or Description..."
style="width: 70%; padding: 10px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
<button id="librarySearchBtn" class="btn" style="margin-left: 10px;">Search</button>
<button id="libraryClearBtn" class="btn secondary" style="margin-left: 5px;">Clear</button>
</div>
<div id="libraryMessage" class="message"></div>
<div id="libraryResults" style="margin-top: 20px;">
<p style="color: #6c757d;">Enter a search query or click Search to browse all parts</p>
</div>
</div>
</div><!-- End libraryTab -->
{% if not library_only_mode %}
<!-- Variant Manager Tab -->
<div id="variantsTab" class="tab-content">
<div class="container">
@@ -381,8 +440,10 @@
<button class="btn" onclick="closeEditPartsModal()">Done</button>
</div>
</div>
{% endif %}
<script>
const libraryOnlyMode = {{ 'true' if library_only_mode else 'false' }};
const socket = io();
const statusEl = document.getElementById('status');
@@ -414,6 +475,8 @@
socket.disconnect();
});
// Only initialize project-specific features if not in library-only mode
if (!libraryOnlyMode) {
// PDF Generation
const generatePdfBtn = document.getElementById('generatePdfBtn');
const messageEl = document.getElementById('message');
@@ -486,6 +549,114 @@
generateGerbersBtn.disabled = false;
});
// STEP Export
const exportStepBtn = document.getElementById('exportStepBtn');
const stepMessageEl = document.getElementById('stepMessage');
function showStepMessage(text, type) {
stepMessageEl.textContent = text;
stepMessageEl.className = 'message ' + type;
stepMessageEl.style.display = 'block';
}
exportStepBtn.addEventListener('click', () => {
exportStepBtn.disabled = true;
showStepMessage('Exporting PCB to STEP...', 'info');
socket.emit('export_step');
});
socket.on('step_status', (data) => {
showStepMessage(data.status, 'info');
});
socket.on('step_complete', (data) => {
showStepMessage('STEP file generated successfully! Downloading...', 'success');
exportStepBtn.disabled = false;
// Trigger download
const link = document.createElement('a');
link.href = '/download/' + data.filename;
link.download = data.filename;
link.click();
});
socket.on('step_error', (data) => {
showStepMessage('Error: ' + data.error, 'error');
exportStepBtn.disabled = false;
});
// PCB Render
const renderPcbBtn = document.getElementById('renderPcbBtn');
const renderMessageEl = document.getElementById('renderMessage');
function showRenderMessage(text, type) {
renderMessageEl.textContent = text;
renderMessageEl.className = 'message ' + type;
renderMessageEl.style.display = 'block';
}
renderPcbBtn.addEventListener('click', () => {
renderPcbBtn.disabled = true;
showRenderMessage('Rendering PCB image...', 'info');
socket.emit('render_pcb');
});
socket.on('render_status', (data) => {
showRenderMessage(data.status, 'info');
});
socket.on('render_complete', (data) => {
showRenderMessage('PCB image rendered successfully! Downloading...', 'success');
renderPcbBtn.disabled = false;
// Trigger download
const link = document.createElement('a');
link.href = '/download/' + data.filename;
link.download = data.filename;
link.click();
});
socket.on('render_error', (data) => {
showRenderMessage('Error: ' + data.error, 'error');
renderPcbBtn.disabled = false;
});
// BOM Generation
const generateBomBtn = document.getElementById('generateBomBtn');
const bomMessageEl = document.getElementById('bomMessage');
function showBomMessage(text, type) {
bomMessageEl.textContent = text;
bomMessageEl.className = 'message ' + type;
bomMessageEl.style.display = 'block';
}
generateBomBtn.addEventListener('click', () => {
generateBomBtn.disabled = true;
showBomMessage('Generating BOMs...', 'info');
socket.emit('generate_bom');
});
socket.on('bom_status', (data) => {
showBomMessage(data.status, 'info');
});
socket.on('bom_complete', (data) => {
showBomMessage('BOMs generated successfully! Downloading ZIP archive...', 'success');
generateBomBtn.disabled = false;
// Trigger download
const link = document.createElement('a');
link.href = '/download/' + data.filename;
link.download = data.filename;
link.click();
});
socket.on('bom_error', (data) => {
showBomMessage('Error: ' + data.error, 'error');
generateBomBtn.disabled = false;
});
// Library Synchronization
const syncLibrariesBtn = document.getElementById('syncLibrariesBtn');
const syncMessageEl = document.getElementById('syncMessage');
@@ -663,16 +834,98 @@
});
// Show selected tab
if (tabName === 'main') {
document.getElementById('mainTab').classList.add('active');
if (tabName === 'debug') {
document.getElementById('debugTab').classList.add('active');
document.querySelectorAll('.tab')[0].classList.add('active');
} else if (tabName === 'publish') {
document.getElementById('publishTab').classList.add('active');
document.querySelectorAll('.tab')[1].classList.add('active');
} else if (tabName === 'library') {
document.getElementById('libraryTab').classList.add('active');
document.querySelectorAll('.tab')[2].classList.add('active');
} else if (tabName === 'variants') {
document.getElementById('variantsTab').classList.add('active');
document.querySelectorAll('.tab')[1].classList.add('active');
document.querySelectorAll('.tab')[3].classList.add('active');
loadVariants();
}
}
// ========================================
// Library Browser
// ========================================
const librarySearchInput = document.getElementById('librarySearchInput');
const librarySearchBtn = document.getElementById('librarySearchBtn');
const libraryClearBtn = document.getElementById('libraryClearBtn');
const libraryMessageEl = document.getElementById('libraryMessage');
const libraryResultsEl = document.getElementById('libraryResults');
function showLibraryMessage(text, type) {
libraryMessageEl.textContent = text;
libraryMessageEl.className = 'message ' + type;
libraryMessageEl.style.display = 'block';
}
function searchParts() {
const query = librarySearchInput.value.trim();
showLibraryMessage('Searching...', 'info');
socket.emit('search_parts', { query: query });
}
librarySearchBtn.addEventListener('click', searchParts);
librarySearchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchParts();
}
});
libraryClearBtn.addEventListener('click', () => {
librarySearchInput.value = '';
libraryResultsEl.innerHTML = '<p style="color: #6c757d;">Enter a search query or click Search to browse all parts</p>';
libraryMessageEl.style.display = 'none';
});
socket.on('library_search_results', (data) => {
libraryMessageEl.style.display = 'none';
if (data.parts.length === 0) {
libraryResultsEl.innerHTML = '<p style="color: #6c757d;">No parts found</p>';
return;
}
let html = `
<p style="margin-bottom: 10px;"><strong>Found ${data.count} part(s)</strong></p>
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
<thead>
<tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">IPN</th>
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">MPN</th>
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">Manufacturer</th>
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">Description</th>
</tr>
</thead>
<tbody>
`;
for (const part of data.parts) {
html += `
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.ipn}</td>
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.mpn}</td>
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.manufacturer}</td>
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.description}</td>
</tr>
`;
}
html += '</tbody></table>';
libraryResultsEl.innerHTML = html;
});
socket.on('library_error', (data) => {
showLibraryMessage('Error: ' + data.error, 'error');
});
// ========================================
// Variant Manager
// ========================================
@@ -821,6 +1074,10 @@
});
}
socket.on('variant_status', (data) => {
showVariantMessage(data.status, 'info');
});
socket.on('variant_updated', (data) => {
showVariantMessage(data.message, 'success');
loadVariants();
@@ -877,6 +1134,7 @@
event.target.style.display = 'none';
}
}
} // End of if (!libraryOnlyMode)
</script>
</body>
</html>