Files
kicad-manager/templates/index.html
brentperteet bcb2c70e93 Add create new part functionality to library browser
- Add UI to create parts from missing IPNs in spreadsheet
- "+ Add New Part" button switches to create part view
- Auto-populate fields from spreadsheet (description, class, manufacturer, MPN)
- Normalize class field (uppercase, replace special chars with underscores)
- Add optional datasheet URL field
- Exclude section header rows (no manufacturer/MPN)
- Cancel button returns to browser view
- Align logo banner and h1 header to left

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-23 09:30:19 -06:00

1333 lines
51 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UM KiCad Manager</title>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
width: 80%;
max-width: 1536px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
border-bottom: 3px solid #007bff;
padding-bottom: 10px;
text-align: left;
}
.arg-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-top: 20px;
}
.arg-item {
display: flex;
margin: 15px 0;
padding: 10px;
border-left: 4px solid #007bff;
background-color: #f8f9fa;
}
.arg-label {
font-weight: bold;
color: #007bff;
min-width: 150px;
margin-right: 20px;
}
.arg-value {
color: #333;
word-break: break-all;
}
.status {
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
border-radius: 5px;
font-weight: bold;
}
.status.connected {
background-color: #28a745;
color: white;
}
.status.disconnected {
background-color: #dc3545;
color: white;
}
.actions {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-top: 20px;
}
.btn {
background-color: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
.btn:hover {
background-color: #0056b3;
}
.btn:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.message {
margin-top: 15px;
padding: 10px;
border-radius: 5px;
display: none;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.message.info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.command-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-top: 20px;
}
.command-box {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 14px;
overflow-x: auto;
position: relative;
}
.copy-btn {
position: absolute;
top: 10px;
right: 10px;
background-color: #28a745;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.copy-btn:hover {
background-color: #218838;
}
.tabs {
display: flex;
border-bottom: 2px solid #007bff;
margin-bottom: 20px;
}
.tab {
padding: 12px 24px;
background-color: #e9ecef;
border: none;
cursor: pointer;
font-size: 16px;
font-weight: bold;
margin-right: 5px;
border-radius: 5px 5px 0 0;
}
.tab:hover {
background-color: #dee2e6;
}
.tab.active {
background-color: #007bff;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.variant-card {
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
background-color: #f8f9fa;
}
.variant-card.active-variant {
border-color: #007bff;
background-color: #e7f3ff;
}
.variant-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.variant-name {
font-size: 18px;
font-weight: bold;
color: #007bff;
}
.variant-description {
color: #666;
margin-bottom: 10px;
}
.variant-stats {
color: #666;
font-size: 14px;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
border-radius: 8px;
width: 50%;
max-width: 500px;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
.form-group {
margin: 15px 0;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #dee2e6;
border-radius: 5px;
font-size: 14px;
}
.parts-list {
max-height: 400px;
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 10px;
background-color: white;
}
.part-item {
padding: 8px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.part-item:last-child {
border-bottom: none;
}
.part-item.dnp {
background-color: #fff3cd;
}
</style>
</head>
<body>
<img src="{{ url_for('static', filename='logo_banner.png') }}" alt="Banner" style="width: 100%; max-width: 800px; display: block; margin: 0 0 20px 0;">
<div id="status" class="status disconnected">Disconnected</div>
<h1>KiCad Manager</h1>
{% if library_only_mode %}
<div class="tabs">
<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 %}
{% if not library_only_mode %}
<div id="debugTab" class="tab-content active">
<div class="command-container">
<h2>Invocation Command</h2>
<div class="command-box">
<button class="copy-btn" onclick="copyCommand()">Copy</button>
<code id="invocationCmd">{{ invocation_cmd }}</code>
</div>
</div>
<div class="arg-container">
{% for key, value in args.items() %}
<div class="arg-item">
<div class="arg-label">{{ key }}:</div>
<div class="arg-value">{{ value }}</div>
</div>
{% endfor %}
</div>
<div class="actions">
<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>
<div class="actions">
<h2>Window Testing</h2>
<button id="testWindowBtn" class="btn">Test: Save & Close Schematic Window</button>
<div id="windowTestMessage" class="message"></div>
</div>
<div class="actions">
<h2>System Initialization</h2>
<button id="initUserBtn" class="btn">Initialize User Environment</button>
<div id="initMessage" class="message"></div>
</div>
<div class="arg-container">
<h2>Settings</h2>
<div style="margin: 15px 0;">
<label for="partsSpreadsheet" style="display: block; font-weight: bold; color: #007bff; margin-bottom: 5px;">
Master Parts Spreadsheet (XLSX):
</label>
<input type="text" id="partsSpreadsheet" style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-family: 'Courier New', monospace;">
<button id="saveConfigBtn" class="btn" style="margin-top: 10px;">Save Settings</button>
<div id="configMessage" class="message"></div>
</div>
</div>
</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>
<!-- Browser View -->
<div id="libraryBrowserView">
<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>
<button id="showCreatePartBtn" class="btn" style="margin-left: 10px; background-color: #28a745;">+ Add New Part</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>
<!-- Create Part View -->
<div id="libraryCreateView" style="display: none;">
<div style="margin: 20px 0; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h3>Create New Part</h3>
<div id="createPartFormLoading" style="padding: 20px; text-align: center;">
<p>Loading missing IPNs from spreadsheet...</p>
</div>
<div id="createPartForm" style="display: none;">
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Select IPN:</label>
<select id="ipnDropdown" style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
<option value="">-- Select an IPN --</option>
</select>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Description:</label>
<input type="text" id="descriptionInput" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px; background-color: #e9ecef;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Class:</label>
<input type="text" id="classInput" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px; background-color: #e9ecef;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Manufacturer:</label>
<input type="text" id="manufacturerInput" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px; background-color: #e9ecef;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">MPN:</label>
<input type="text" id="mpnInput" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px; background-color: #e9ecef;">
</div>
<div style="margin-bottom: 15px;">
<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>
<button id="createPartBtn" class="btn" disabled>Create Part</button>
<button id="cancelCreatePartBtn" class="btn secondary" style="margin-left: 10px;">Cancel</button>
</div>
<div id="createPartMessage" class="message"></div>
</div>
</div>
</div>
</div><!-- End libraryTab -->
{% if not library_only_mode %}
<!-- Variant Manager Tab -->
<div id="variantsTab" class="tab-content">
<div class="container">
<h2>Project: <span id="projectName">Loading...</span></h2>
<button id="createVariantBtn" class="btn">+ Create New Variant</button>
<button id="syncFromSchematicBtn" class="btn secondary">Sync from Schematic</button>
<div id="variantMessage" class="message"></div>
</div>
<div class="container">
<h2>Variants</h2>
<div id="variantsList"></div>
</div>
</div><!-- End variantsTab -->
<!-- Create Variant Modal -->
<div id="createModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeCreateModal()">&times;</span>
<h2>Create New Variant</h2>
<div class="form-group">
<label for="variantName">Variant Name:</label>
<input type="text" id="variantName" placeholder="e.g., low-cost, premium">
</div>
<div class="form-group">
<label for="variantDescription">Description:</label>
<textarea id="variantDescription" rows="3" placeholder="Description of this variant"></textarea>
</div>
<div class="form-group">
<label for="basedOn">Based On:</label>
<select id="basedOn">
<option value="">Empty (no parts DNP)</option>
</select>
</div>
<button class="btn" onclick="createVariant()">Create</button>
<button class="btn secondary" onclick="closeCreateModal()">Cancel</button>
</div>
</div>
<!-- Edit Parts Modal -->
<div id="editPartsModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditPartsModal()">&times;</span>
<h2>Edit Parts: <span id="editVariantName"></span></h2>
<p>Toggle parts between Fitted and DNP (Do Not Place)</p>
<div class="parts-list" id="partsList"></div>
<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');
socket.on('connect', () => {
console.log('Connected to server');
statusEl.textContent = 'Connected';
statusEl.className = 'status connected';
});
socket.on('disconnect', () => {
console.log('Disconnected from server');
statusEl.textContent = 'Disconnected';
statusEl.className = 'status disconnected';
});
// Send heartbeat every 2 seconds
setInterval(() => {
if (socket.connected) {
socket.emit('heartbeat');
}
}, 2000);
socket.on('heartbeat_ack', () => {
console.log('Heartbeat acknowledged');
});
// Detect when page is about to close
window.addEventListener('beforeunload', () => {
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');
function showMessage(text, type) {
messageEl.textContent = text;
messageEl.className = 'message ' + type;
messageEl.style.display = 'block';
}
generatePdfBtn.addEventListener('click', () => {
generatePdfBtn.disabled = true;
showMessage('Requesting PDF generation...', 'info');
socket.emit('generate_pdf');
});
socket.on('pdf_status', (data) => {
showMessage(data.status, 'info');
});
socket.on('pdf_complete', (data) => {
showMessage('PDFs generated successfully! Downloading ZIP archive...', 'success');
generatePdfBtn.disabled = false;
// Trigger download
const link = document.createElement('a');
link.href = '/download/' + data.filename;
link.download = data.filename;
link.click();
});
socket.on('pdf_error', (data) => {
showMessage('Error: ' + data.error, 'error');
generatePdfBtn.disabled = false;
});
// Gerber Generation
const generateGerbersBtn = document.getElementById('generateGerbersBtn');
const gerberMessageEl = document.getElementById('gerberMessage');
function showGerberMessage(text, type) {
gerberMessageEl.textContent = text;
gerberMessageEl.className = 'message ' + type;
gerberMessageEl.style.display = 'block';
}
generateGerbersBtn.addEventListener('click', () => {
generateGerbersBtn.disabled = true;
showGerberMessage('Requesting gerber generation...', 'info');
socket.emit('generate_gerbers');
});
socket.on('gerber_status', (data) => {
showGerberMessage(data.status, 'info');
});
socket.on('gerber_complete', (data) => {
showGerberMessage('Gerbers generated successfully! Downloading ZIP archive...', 'success');
generateGerbersBtn.disabled = false;
// Trigger download
const link = document.createElement('a');
link.href = '/download/' + data.filename;
link.download = data.filename;
link.click();
});
socket.on('gerber_error', (data) => {
showGerberMessage('Error: ' + data.error, 'error');
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');
function showSyncMessage(text, type) {
syncMessageEl.textContent = text;
syncMessageEl.className = 'message ' + type;
syncMessageEl.style.display = 'block';
}
syncLibrariesBtn.addEventListener('click', () => {
syncLibrariesBtn.disabled = true;
showSyncMessage('Starting library synchronization...', 'info');
socket.emit('sync_libraries');
});
socket.on('sync_status', (data) => {
showSyncMessage(data.status, 'info');
});
socket.on('sync_complete', (data) => {
const output = data.output.replace(/\n/g, '<br>');
syncMessageEl.innerHTML = '<strong>Sync Complete!</strong><br><pre style="margin-top: 10px; background: #f8f9fa; padding: 10px; border-radius: 5px; overflow-x: auto;">' + output + '</pre>';
syncMessageEl.className = 'message success';
syncMessageEl.style.display = 'block';
syncLibrariesBtn.disabled = false;
});
socket.on('sync_error', (data) => {
const error = data.error.replace(/\n/g, '<br>');
syncMessageEl.innerHTML = '<strong>Error:</strong><br><pre style="margin-top: 10px; background: #f8d7da; padding: 10px; border-radius: 5px; overflow-x: auto;">' + error + '</pre>';
syncMessageEl.className = 'message error';
syncMessageEl.style.display = 'block';
syncLibrariesBtn.disabled = false;
});
// Database Synchronization
const syncDbBtn = document.getElementById('syncDbBtn');
const dbSyncMessageEl = document.getElementById('dbSyncMessage');
function showDbSyncMessage(text, type) {
dbSyncMessageEl.textContent = text;
dbSyncMessageEl.className = 'message ' + type;
dbSyncMessageEl.style.display = 'block';
}
syncDbBtn.addEventListener('click', () => {
syncDbBtn.disabled = true;
showDbSyncMessage('Starting database synchronization...', 'info');
socket.emit('sync_database');
});
socket.on('db_sync_status', (data) => {
showDbSyncMessage(data.status, 'info');
});
socket.on('db_sync_complete', (data) => {
const output = data.output.replace(/\n/g, '<br>');
dbSyncMessageEl.innerHTML = '<strong>Database Sync Complete!</strong><br><pre style="margin-top: 10px; background: #f8f9fa; padding: 10px; border-radius: 5px; overflow-x: auto;">' + output + '</pre>';
dbSyncMessageEl.className = 'message success';
dbSyncMessageEl.style.display = 'block';
syncDbBtn.disabled = false;
});
socket.on('db_sync_error', (data) => {
const error = data.error.replace(/\n/g, '<br>');
dbSyncMessageEl.innerHTML = '<strong>Error:</strong><br><pre style="margin-top: 10px; background: #f8d7da; padding: 10px; border-radius: 5px; overflow-x: auto;">' + error + '</pre>';
dbSyncMessageEl.className = 'message error';
dbSyncMessageEl.style.display = 'block';
syncDbBtn.disabled = false;
});
// User Initialization
const initUserBtn = document.getElementById('initUserBtn');
const initMessageEl = document.getElementById('initMessage');
function showInitMessage(text, type) {
initMessageEl.textContent = text;
initMessageEl.className = 'message ' + type;
initMessageEl.style.display = 'block';
}
initUserBtn.addEventListener('click', () => {
initUserBtn.disabled = true;
showInitMessage('Initializing user environment...', 'info');
socket.emit('init_user');
});
socket.on('init_status', (data) => {
showInitMessage(data.status, 'info');
});
socket.on('init_complete', (data) => {
const output = data.output.replace(/\n/g, '<br>');
initMessageEl.innerHTML = '<strong>Initialization Complete!</strong><br><pre style="margin-top: 10px; background: #f8f9fa; padding: 10px; border-radius: 5px; overflow-x: auto;">' + output + '</pre>';
initMessageEl.className = 'message success';
initMessageEl.style.display = 'block';
initUserBtn.disabled = false;
});
socket.on('init_error', (data) => {
const error = data.error.replace(/\n/g, '<br>');
initMessageEl.innerHTML = '<strong>Error:</strong><br><pre style="margin-top: 10px; background: #f8d7da; padding: 10px; border-radius: 5px; overflow-x: auto;">' + error + '</pre>';
initMessageEl.className = 'message error';
initMessageEl.style.display = 'block';
initUserBtn.disabled = false;
});
// Copy command to clipboard
function copyCommand() {
const cmdText = document.getElementById('invocationCmd').textContent;
navigator.clipboard.writeText(cmdText).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
});
}
// Configuration Management
const partsSpreadsheetInput = document.getElementById('partsSpreadsheet');
const saveConfigBtn = document.getElementById('saveConfigBtn');
const configMessageEl = document.getElementById('configMessage');
function showConfigMessage(text, type) {
configMessageEl.textContent = text;
configMessageEl.className = 'message ' + type;
configMessageEl.style.display = 'block';
}
// Load configuration on page load
fetch('/config')
.then(response => response.json())
.then(config => {
partsSpreadsheetInput.value = config.parts_spreadsheet_path || '';
});
// Save configuration
saveConfigBtn.addEventListener('click', () => {
const config = {
parts_spreadsheet_path: partsSpreadsheetInput.value
};
fetch('/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
})
.then(response => response.json())
.then(data => {
showConfigMessage('Settings saved successfully!', 'success');
setTimeout(() => {
configMessageEl.style.display = 'none';
}, 3000);
})
.catch(error => {
showConfigMessage('Error saving settings: ' + error, 'error');
});
});
// ========================================
// Tab Switching
// ========================================
function switchTab(tabName) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
// Show selected tab
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')[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');
});
// ========================================
// Create New Part
// ========================================
const libraryBrowserView = document.getElementById('libraryBrowserView');
const libraryCreateView = document.getElementById('libraryCreateView');
const showCreatePartBtn = document.getElementById('showCreatePartBtn');
const cancelCreatePartBtn = document.getElementById('cancelCreatePartBtn');
const createPartFormLoading = document.getElementById('createPartFormLoading');
const createPartForm = document.getElementById('createPartForm');
const createPartMessage = document.getElementById('createPartMessage');
const ipnDropdown = document.getElementById('ipnDropdown');
const descriptionInput = document.getElementById('descriptionInput');
const classInput = document.getElementById('classInput');
const manufacturerInput = document.getElementById('manufacturerInput');
const mpnInput = document.getElementById('mpnInput');
const datasheetInput = document.getElementById('datasheetInput');
const createPartBtn = document.getElementById('createPartBtn');
let missingPartsData = [];
function showCreatePartMessage(text, type) {
createPartMessage.textContent = text;
createPartMessage.className = 'message ' + type;
createPartMessage.style.display = 'block';
}
// Show create part view
showCreatePartBtn.addEventListener('click', () => {
libraryBrowserView.style.display = 'none';
libraryCreateView.style.display = 'block';
createPartFormLoading.style.display = 'block';
createPartForm.style.display = 'none';
createPartMessage.style.display = 'none';
socket.emit('get_missing_ipns');
});
// Cancel and return to browser
cancelCreatePartBtn.addEventListener('click', () => {
libraryCreateView.style.display = 'none';
libraryBrowserView.style.display = 'block';
// Clear form
ipnDropdown.innerHTML = '<option value="">-- Select an IPN --</option>';
descriptionInput.value = '';
classInput.value = '';
manufacturerInput.value = '';
mpnInput.value = '';
datasheetInput.value = '';
createPartBtn.disabled = true;
missingPartsData = [];
});
socket.on('missing_ipns_result', (data) => {
createPartFormLoading.style.display = 'none';
if (data.count === 0) {
showCreatePartMessage('No missing IPNs found. All parts from spreadsheet are in database!', 'success');
createPartForm.style.display = 'none';
return;
}
missingPartsData = data.parts;
// Populate dropdown
ipnDropdown.innerHTML = '<option value="">-- Select an IPN --</option>';
for (const part of data.parts) {
const option = document.createElement('option');
option.value = part.ipn;
option.textContent = `${part.ipn} - ${part.description}`;
ipnDropdown.appendChild(option);
}
createPartForm.style.display = 'block';
showCreatePartMessage(`Found ${data.count} missing IPN(s). Select one to create in database.`, 'success');
});
ipnDropdown.addEventListener('change', () => {
const selectedIpn = ipnDropdown.value;
if (!selectedIpn) {
descriptionInput.value = '';
classInput.value = '';
manufacturerInput.value = '';
mpnInput.value = '';
createPartBtn.disabled = true;
return;
}
const part = missingPartsData.find(p => p.ipn === selectedIpn);
if (part) {
descriptionInput.value = part.description;
classInput.value = part.class;
manufacturerInput.value = part.manufacturer;
mpnInput.value = part.mpn;
createPartBtn.disabled = false;
}
});
createPartBtn.addEventListener('click', () => {
const selectedIpn = ipnDropdown.value;
if (!selectedIpn) return;
const part = missingPartsData.find(p => p.ipn === selectedIpn);
if (!part) return;
createPartBtn.disabled = true;
showCreatePartMessage('Creating part in database...', 'info');
socket.emit('create_part', {
ipn: part.ipn,
description: part.description,
class: part.class,
manufacturer: part.manufacturer,
mpn: part.mpn,
datasheet: datasheetInput.value.trim()
});
});
socket.on('part_created', (data) => {
showCreatePartMessage(data.message, 'success');
createPartBtn.disabled = false;
// Remove created part from dropdown
const selectedIpn = ipnDropdown.value;
missingPartsData = missingPartsData.filter(p => p.ipn !== selectedIpn);
ipnDropdown.innerHTML = '<option value="">-- Select an IPN --</option>';
for (const part of missingPartsData) {
const option = document.createElement('option');
option.value = part.ipn;
option.textContent = `${part.ipn} - ${part.description}`;
ipnDropdown.appendChild(option);
}
descriptionInput.value = '';
classInput.value = '';
manufacturerInput.value = '';
mpnInput.value = '';
datasheetInput.value = '';
createPartBtn.disabled = true;
if (missingPartsData.length === 0) {
createPartForm.style.display = 'none';
showCreatePartMessage('All missing parts have been created!', 'success');
}
});
// ========================================
// Variant Manager
// ========================================
let currentVariant = '';
let allParts = [];
function showVariantMessage(text, type) {
const messageEl = document.getElementById('variantMessage');
messageEl.textContent = text;
messageEl.className = 'message ' + type;
messageEl.style.display = 'block';
setTimeout(() => {
messageEl.style.display = 'none';
}, 5000);
}
function loadVariants() {
socket.emit('get_variants');
}
socket.on('variants_data', (data) => {
document.getElementById('projectName').textContent = data.project_name;
currentVariant = data.active_variant;
allParts = data.all_parts || [];
renderVariants(data.variants, data.active_variant);
updateBasedOnSelect(data.variants);
});
function renderVariants(variants, activeVariant) {
const list = document.getElementById('variantsList');
list.innerHTML = '';
for (const [name, variant] of Object.entries(variants)) {
const isActive = name === activeVariant;
const card = document.createElement('div');
card.className = 'variant-card' + (isActive ? ' active-variant' : '');
const dnpCount = variant.dnp_parts.length;
const fittedCount = allParts.length - dnpCount;
card.innerHTML = `
<div class="variant-header">
<div class="variant-name">${variant.name}${isActive ? ' (Active)' : ''}</div>
<div>
${!isActive ? `<button class="btn success" onclick="activateVariant('${name}')">Activate</button>` : ''}
<button class="btn" onclick="editParts('${name}')">Edit Parts</button>
${name !== 'default' ? `<button class="btn danger" onclick="deleteVariant('${name}')">Delete</button>` : ''}
</div>
</div>
<div class="variant-description">${variant.description || 'No description'}</div>
<div class="variant-stats">
Fitted: ${fittedCount} | DNP: ${dnpCount} | Total: ${allParts.length}
</div>
`;
list.appendChild(card);
}
}
function updateBasedOnSelect(variants) {
const select = document.getElementById('basedOn');
select.innerHTML = '<option value="">Empty (no parts DNP)</option>';
for (const name of Object.keys(variants)) {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
}
}
document.getElementById('createVariantBtn').addEventListener('click', () => {
document.getElementById('createModal').style.display = 'block';
});
function closeCreateModal() {
document.getElementById('createModal').style.display = 'none';
document.getElementById('variantName').value = '';
document.getElementById('variantDescription').value = '';
document.getElementById('basedOn').value = '';
}
function createVariant() {
const name = document.getElementById('variantName').value.trim();
const description = document.getElementById('variantDescription').value.trim();
const basedOn = document.getElementById('basedOn').value;
if (!name) {
alert('Please enter a variant name');
return;
}
socket.emit('create_variant', { name, description, based_on: basedOn });
closeCreateModal();
}
function deleteVariant(name) {
if (confirm(`Are you sure you want to delete variant "${name}"?`)) {
socket.emit('delete_variant', { name });
}
}
function activateVariant(name) {
if (confirm(`Activate variant "${name}"? This will apply DNP settings to the schematic.`)) {
socket.emit('activate_variant', { name });
}
}
function editParts(variantName) {
currentVariant = variantName;
document.getElementById('editVariantName').textContent = variantName;
socket.emit('get_variant_parts', { variant: variantName });
}
function closeEditPartsModal() {
document.getElementById('editPartsModal').style.display = 'none';
}
socket.on('variant_parts_data', (data) => {
const list = document.getElementById('partsList');
list.innerHTML = '';
for (const part of data.parts) {
const item = document.createElement('div');
item.className = 'part-item' + (part.is_dnp ? ' dnp' : '');
item.innerHTML = `
<div>
<strong>${part.reference}</strong> - ${part.value || 'N/A'}
${part.is_dnp ? '<span style="color: #856404; margin-left: 10px;">[DNP]</span>' : ''}
</div>
<button class="btn ${part.is_dnp ? 'success' : 'secondary'}"
onclick="togglePartDNP('${part.uuid}', ${!part.is_dnp})">
${part.is_dnp ? 'Fit' : 'DNP'}
</button>
`;
list.appendChild(item);
}
document.getElementById('editPartsModal').style.display = 'block';
});
function togglePartDNP(uuid, isDNP) {
socket.emit('set_part_dnp', {
variant: currentVariant,
uuid: uuid,
is_dnp: isDNP
});
}
socket.on('variant_status', (data) => {
showVariantMessage(data.status, 'info');
});
socket.on('variant_updated', (data) => {
showVariantMessage(data.message, 'success');
loadVariants();
});
socket.on('variant_error', (data) => {
showVariantMessage(data.error, 'error');
});
document.getElementById('syncFromSchematicBtn').addEventListener('click', () => {
if (confirm('Sync variant from current schematic state? This will update DNP settings based on the schematic.')) {
socket.emit('sync_from_schematic');
}
});
socket.on('sync_complete', (data) => {
showVariantMessage(data.message, 'success');
loadVariants();
});
// Window Testing
const testWindowBtn = document.getElementById('testWindowBtn');
const windowTestMessageEl = document.getElementById('windowTestMessage');
function showWindowTestMessage(text, type) {
windowTestMessageEl.textContent = text;
windowTestMessageEl.className = 'message ' + type;
windowTestMessageEl.style.display = 'block';
}
testWindowBtn.addEventListener('click', () => {
testWindowBtn.disabled = true;
showWindowTestMessage('Testing window interaction...', 'info');
socket.emit('test_window_interaction');
});
socket.on('window_test_status', (data) => {
showWindowTestMessage(data.status, 'info');
});
socket.on('window_test_complete', (data) => {
showWindowTestMessage(data.message, 'success');
testWindowBtn.disabled = false;
});
socket.on('window_test_error', (data) => {
showWindowTestMessage('Error: ' + data.error, 'error');
testWindowBtn.disabled = false;
});
// Close modals when clicking outside
window.onclick = function(event) {
if (event.target.className === 'modal') {
event.target.style.display = 'none';
}
}
} // End of if (!libraryOnlyMode)
</script>
</body>
</html>