Files
kicad-manager/templates/index.html
brentperteet aa7f720194 Fix Three.js module loading for OrbitControls and STLLoader
- Switch from legacy script includes to ES6 modules with import map
- Import OrbitControls and STLLoader from three/addons/
- Update constructor calls to use imported classes directly
- Remove THREE namespace prefix for OrbitControls and STLLoader
- Fixes "THREE.OrbitControls is not a constructor" error

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 13:58:14 -06:00

1911 lines
79 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>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.159.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.159.0/examples/jsm/"
}
}
</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">
<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 -->
{% 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/Edit Part View -->
<div id="libraryCreateView" style="display: none;">
<div style="margin: 20px 0; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h3 id="formTitle">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 id="ipnDropdownContainer" 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 id="ipnDisplayContainer" style="margin-bottom: 15px; display: none;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">IPN:</label>
<input type="text" id="ipnDisplay" 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;">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>
<!-- 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>
<!-- 3D Model Viewer -->
<div id="modelViewerContainer" style="margin-bottom: 15px; display: none;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">3D Model:</label>
<div id="modelViewer" style="border: 1px solid #dee2e6; border-radius: 5px; background: #1a1a1a; width: 100%; height: 300px; position: relative;">
<div id="modelViewerCanvas"></div>
<div id="modelViewerMessage" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #999; text-align: center;">
Loading 3D model...
</div>
</div>
<div style="margin-top: 5px; font-size: 12px; color: #6c757d;">
Use mouse to rotate (left-click drag), zoom (scroll), and pan (right-click drag)
</div>
</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 type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
// Make THREE available globally for debugging
window.THREE = THREE;
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>
<th style="padding: 10px; text-align: center; border: 1px solid #dee2e6;">Datasheet</th>
</tr>
</thead>
<tbody>
`;
for (const part of data.parts) {
const datasheetCell = part.datasheet
? `<a href="${part.datasheet}" target="_blank" style="color: #007bff; text-decoration: none;">Datasheet</a>`
: '';
html += `
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 8px; border: 1px solid #dee2e6;">
<a href="#" onclick="editPart('${part.ipn}'); return false;" style="color: #007bff; text-decoration: none;">${part.ipn}</a>
</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>
<td style="padding: 8px; text-align: center; border: 1px solid #dee2e6;">${datasheetCell}</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');
// 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 = '';
let isEditMode = false;
let editingIpn = '';
const formTitle = document.getElementById('formTitle');
const ipnDropdownContainer = document.getElementById('ipnDropdownContainer');
const ipnDisplayContainer = document.getElementById('ipnDisplayContainer');
const ipnDisplay = document.getElementById('ipnDisplay');
function showCreatePartMessage(text, type) {
createPartMessage.textContent = text;
createPartMessage.className = 'message ' + type;
createPartMessage.style.display = 'block';
}
// Edit part function (called from search results)
window.editPart = function(ipn) {
isEditMode = true;
editingIpn = ipn;
formTitle.textContent = 'Edit Part';
ipnDropdownContainer.style.display = 'none';
ipnDisplayContainer.style.display = 'block';
ipnDisplay.value = ipn;
createPartBtn.textContent = 'Update Part';
libraryBrowserView.style.display = 'none';
libraryCreateView.style.display = 'block';
createPartFormLoading.style.display = 'block';
createPartForm.style.display = 'none';
createPartMessage.style.display = 'none';
// Load symbol and footprint libraries
socket.emit('get_symbol_libraries');
socket.emit('get_footprint_libraries');
// Request part details
socket.emit('get_part_details', { ipn: ipn });
};
// Show create part view
showCreatePartBtn.addEventListener('click', () => {
isEditMode = false;
editingIpn = '';
formTitle.textContent = 'Create New Part';
ipnDropdownContainer.style.display = 'block';
ipnDisplayContainer.style.display = 'none';
createPartBtn.textContent = 'Create Part';
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');
// Load symbol and footprint libraries
socket.emit('get_symbol_libraries');
socket.emit('get_footprint_libraries');
});
// 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 = '';
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 = '';
document.getElementById('modelViewerContainer').style.display = 'none';
return;
}
footprintPreview.innerHTML = '<span style="color: #999;">Loading...</span>';
socket.emit('render_footprint', {
library_path: selectedFootprintLibraryPath,
footprint_name: footprintName
});
// Request 3D model
console.log('Requesting 3D model for:', footprintName, 'at', selectedFootprintLibraryPath);
socket.emit('get_footprint_model', {
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}`;
});
let modelScene = null;
let modelRenderer = null;
let modelCamera = null;
let modelControls = null;
let modelMesh = null;
function init3DViewer() {
const canvasContainer = document.getElementById('modelViewerCanvas');
const width = canvasContainer.clientWidth || 800;
const height = 300;
// Scene
modelScene = new THREE.Scene();
modelScene.background = new THREE.Color(0x1a1a1a);
// Camera
modelCamera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
modelCamera.position.set(0, 0, 50);
// Renderer
modelRenderer = new THREE.WebGLRenderer({ antialias: true });
modelRenderer.setSize(width, height);
canvasContainer.innerHTML = '';
canvasContainer.appendChild(modelRenderer.domElement);
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
modelScene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight1.position.set(1, 1, 1);
modelScene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.3);
directionalLight2.position.set(-1, -1, -1);
modelScene.add(directionalLight2);
// Controls
modelControls = new OrbitControls(modelCamera, modelRenderer.domElement);
modelControls.enableDamping = true;
modelControls.dampingFactor = 0.05;
// Animation loop
function animate() {
requestAnimationFrame(animate);
modelControls.update();
modelRenderer.render(modelScene, modelCamera);
}
animate();
}
function load3DModel(stlData) {
if (!modelScene) {
init3DViewer();
}
// Remove old mesh if exists
if (modelMesh) {
modelScene.remove(modelMesh);
}
// Decode base64 STL data
const binaryString = atob(stlData);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// Load STL
const loader = new STLLoader();
const geometry = loader.parse(bytes.buffer);
// Center and scale the geometry
geometry.center();
geometry.computeBoundingBox();
const size = new THREE.Vector3();
geometry.boundingBox.getSize(size);
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 30 / maxDim;
geometry.scale(scale, scale, scale);
// Create mesh
const material = new THREE.MeshPhongMaterial({
color: 0x00aa00,
specular: 0x111111,
shininess: 50
});
modelMesh = new THREE.Mesh(geometry, material);
modelScene.add(modelMesh);
// Adjust camera
modelCamera.position.set(0, 0, 50);
modelControls.reset();
}
socket.on('model_result', (data) => {
console.log('Received model_result:', data);
const container = document.getElementById('modelViewerContainer');
const messageEl = document.getElementById('modelViewerMessage');
if (data.has_model) {
console.log('Model found, displaying viewer');
container.style.display = 'block';
messageEl.style.display = 'none';
// Load and display the 3D model
try {
load3DModel(data.data);
} catch (error) {
console.error('Error loading 3D model:', error);
messageEl.style.display = 'block';
messageEl.innerHTML = `<span style="color: #dc3545;">Error loading 3D model: ${error.message}</span>`;
}
} else {
console.log('No model found');
container.style.display = 'none';
}
});
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', () => {
if (isEditMode) {
// Edit mode - update existing part
createPartBtn.disabled = true;
showCreatePartMessage('Updating part in database...', 'info');
socket.emit('update_part', {
ipn: editingIpn,
description: descriptionInput.value.trim(),
class: classInput.value.trim(),
manufacturer: manufacturerInput.value.trim(),
mpn: mpnInput.value.trim(),
datasheet: datasheetInput.value.trim(),
symbol: selectedSymbol,
footprint: selectedFootprint
});
} else {
// Create mode - add new part
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: descriptionInput.value.trim(),
class: classInput.value.trim(),
manufacturer: manufacturerInput.value.trim(),
mpn: mpnInput.value.trim(),
datasheet: datasheetInput.value.trim(),
symbol: selectedSymbol,
footprint: selectedFootprint
});
}
});
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 = '';
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) {
createPartForm.style.display = 'none';
showCreatePartMessage('All missing parts have been created!', 'success');
}
});
socket.on('part_updated', (data) => {
showCreatePartMessage(data.message, 'success');
createPartBtn.disabled = false;
// Return to browser view after delay
setTimeout(() => {
libraryCreateView.style.display = 'none';
libraryBrowserView.style.display = 'block';
}, 1500);
});
socket.on('part_details_result', (data) => {
createPartFormLoading.style.display = 'none';
createPartForm.style.display = 'block';
// Populate form with part details
descriptionInput.value = data.part.description || '';
classInput.value = data.part.class || '';
manufacturerInput.value = data.part.manufacturer || '';
mpnInput.value = data.part.mpn || '';
datasheetInput.value = data.part.datasheet || '';
// Use resolved symbol info from server
if (data.symbol_info) {
selectedSymbol = data.part.symbol;
const libPath = data.symbol_info.library_path;
const symbolName = data.symbol_info.symbol_name;
// Find and select the library by path
const lib = symbolLibraries.find(l => l.path === libPath);
if (lib) {
symbolLibrarySelect.value = lib.path;
selectedSymbolLibraryPath = lib.path;
socket.emit('get_symbols_in_library', { library_path: lib.path });
// Symbol will be selected when symbols_in_library_result is received
setTimeout(() => {
symbolSelect.value = symbolName;
socket.emit('render_symbol', { library_path: lib.path, symbol_name: symbolName });
}, 500);
}
}
// Use resolved footprint info from server
if (data.footprint_info) {
selectedFootprint = data.part.footprint;
const libPath = data.footprint_info.library_path;
const footprintName = data.footprint_info.footprint_name;
// Find and select the library by path
const lib = footprintLibraries.find(l => l.path === libPath);
if (lib) {
footprintLibrarySelect.value = lib.path;
selectedFootprintLibraryPath = lib.path;
socket.emit('get_footprints_in_library', { library_path: lib.path });
// Footprint will be selected when footprints_in_library_result is received
setTimeout(() => {
footprintSelect.value = footprintName;
socket.emit('render_footprint', { library_path: lib.path, footprint_name: footprintName });
// Also request 3D model
console.log('Requesting 3D model for loaded part:', footprintName, 'at', lib.path);
socket.emit('get_footprint_model', { library_path: lib.path, footprint_name: footprintName });
}, 500);
}
}
createPartBtn.disabled = false;
});
// ========================================
// 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
// ========================================
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>