fix: performance improvements, shared UI, and cross-reference data quality

Backend (server.py):
- Fix N+1 query in /api/diagrams/<id>/parts with batch cross-ref query
- Add LIMIT safety nets to 15 endpoints (50-5000 per data type)
- Add pagination to /api/vehicles, /api/model-year-engine, /api/vehicles/<id>/parts, /api/admin/export
- Optimize search_vehicles() EXISTS subquery to JOIN
- Restrict static route to /static/* subdir (security fix)
- Add detailed=true support to /api/brands and /api/models

Frontend:
- Extract shared CSS into shared.css (variables, reset, buttons, forms, scrollbar)
- Create shared nav.js component (logo + navigation links, auto-highlights)
- Update all 4 HTML pages to use shared CSS and nav
- Update JS to handle paginated API responses

Data quality:
- Fix cross-reference source field: map 72K records from catalog names to actual brands
- Fix aftermarket_parts manufacturer_id: correct 8K records with wrong brand attribution
- Delete 98MB backup file, orphan records, and garbage cross-references
- Add import scripts for DAR, FRAM, WIX, MOOG, Cartek catalogs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 03:09:22 +00:00
parent 3ea2de61e2
commit 7ecf1295a5
17 changed files with 6605 additions and 848 deletions

View File

@@ -115,6 +115,9 @@ function showSection(sectionId) {
case 'fitment':
loadFitment();
break;
case 'diagrams':
// Just show section, user uses search
break;
}
}
@@ -1175,11 +1178,12 @@ async function loadVehiclesForSelect(selectId) {
if (!select) return;
try {
const response = await fetch('/api/model-year-engine');
const vehicles = await response.json();
const response = await fetch('/api/model-year-engine?per_page=100');
const result = await response.json();
const vehicles = result.data || result;
select.innerHTML = '<option value="">Selecciona vehículo...</option>' +
vehicles.slice(0, 100).map(v =>
vehicles.map(v =>
`<option value="${v.id}">${v.brand} ${v.model} ${v.year} - ${v.engine}</option>`
).join('');
} catch (e) {
@@ -1558,8 +1562,9 @@ async function loadBulkEngines() {
const engines = await response.json();
// Get MYE IDs for each engine
const myeResponse = await fetch(`/api/model-year-engine?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&year=${year}`);
const myeData = await myeResponse.json();
const myeResponse = await fetch(`/api/model-year-engine?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&year=${year}&per_page=100`);
const myeResult = await myeResponse.json();
const myeData = myeResult.data || myeResult;
engineSelect.innerHTML = '<option value="">Selecciona motor...</option>' +
myeData.map(mye => `<option value="${mye.id}">${mye.engine}</option>`).join('');
@@ -1707,3 +1712,258 @@ showSection = function(sectionId) {
initBulkEditor();
}
};
// ============================================================================
// Diagram Hotspot Editor
// ============================================================================
let currentEditorDiagramId = null;
let currentEditorHotspots = [];
let partSearchTimeout = null;
async function searchDiagramsAdmin() {
const q = document.getElementById('diagramSearchInput').value.trim();
const container = document.getElementById('diagramSearchResults');
if (!q) {
container.innerHTML = '<p style="color:var(--text-secondary);grid-column:1/-1">Ingresa un código de diagrama para buscar</p>';
return;
}
container.innerHTML = '<p style="color:var(--text-secondary);grid-column:1/-1"><i class="fas fa-spinner fa-spin"></i> Buscando...</p>';
try {
const res = await fetch(`/api/diagrams/search?q=${encodeURIComponent(q)}`);
const diagrams = await res.json();
if (diagrams.length === 0) {
container.innerHTML = '<p style="color:var(--text-secondary);grid-column:1/-1">No se encontraron diagramas</p>';
return;
}
container.innerHTML = diagrams.map(d => {
const imgSrc = d.image_path ? '/' + d.image_path : `/static/diagrams/moog/${d.name}.jpg`;
return `
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:8px;overflow:hidden;cursor:pointer;transition:border-color 0.2s"
onclick="openHotspotEditor(${d.id})"
onmouseover="this.style.borderColor='var(--accent)'"
onmouseout="this.style.borderColor='var(--border)'">
<img src="${imgSrc}" alt="${d.name}" style="width:100%;height:120px;object-fit:contain;background:#f0f0f0;display:block"
onerror="this.style.display='none'">
<div style="padding:0.5rem 0.65rem">
<div style="font-weight:600;color:var(--accent)">${d.name}</div>
<div style="font-size:0.8rem;color:var(--text-secondary)">${d.name_es || d.source || ''}</div>
</div>
</div>`;
}).join('');
} catch (e) {
container.innerHTML = '<p style="color:#e74c3c;grid-column:1/-1">Error al buscar diagramas</p>';
}
}
async function openHotspotEditor(diagramId) {
currentEditorDiagramId = diagramId;
document.getElementById('hotspotEditorArea').style.display = 'block';
try {
const res = await fetch(`/api/diagrams/${diagramId}`);
const diagram = await res.json();
document.getElementById('hotspotEditorTitle').textContent = `${diagram.name} - ${diagram.name_es || diagram.group_name || ''}`;
const imgSrc = diagram.image_url || (diagram.image_path ? '/' + diagram.image_path : '');
document.getElementById('hotspotEditorImg').src = imgSrc;
currentEditorHotspots = diagram.hotspots || [];
renderEditorHotspots();
clearHotspotForm();
// Auto-set next callout number
const maxCallout = currentEditorHotspots.reduce((max, h) => Math.max(max, h.callout_number || 0), 0);
document.getElementById('hsCallout').value = maxCallout + 1;
// Scroll to editor
document.getElementById('hotspotEditorArea').scrollIntoView({ behavior: 'smooth' });
} catch (e) {
showAlert('Error al cargar diagrama', 'error');
}
}
function closeHotspotEditor() {
document.getElementById('hotspotEditorArea').style.display = 'none';
currentEditorDiagramId = null;
currentEditorHotspots = [];
}
function onHotspotImageClick(event) {
const img = event.target;
const rect = img.getBoundingClientRect();
const xPct = ((event.clientX - rect.left) / rect.width * 100).toFixed(2);
const yPct = ((event.clientY - rect.top) / rect.height * 100).toFixed(2);
document.getElementById('hsCoords').value = `${xPct},${yPct}`;
// Show temporary marker
renderEditorHotspots();
const container = document.getElementById('hotspotMarkersContainer');
const tempMarker = document.createElement('div');
tempMarker.style.cssText = `position:absolute;left:${xPct}%;top:${yPct}%;width:24px;height:24px;border-radius:50%;background:rgba(46,204,113,0.5);border:2px solid #2ecc71;transform:translate(-50%,-50%);pointer-events:none;z-index:10`;
container.appendChild(tempMarker);
}
function renderEditorHotspots() {
const container = document.getElementById('hotspotMarkersContainer');
const list = document.getElementById('hotspotsList');
// Markers on image
container.innerHTML = currentEditorHotspots.map(h => {
const coords = (h.coords || '').split(',');
if (coords.length < 2) return '';
return `<div style="position:absolute;left:${coords[0]}%;top:${coords[1]}%;width:24px;height:24px;border-radius:50%;background:rgba(231,76,60,0.4);border:2px solid #e74c3c;transform:translate(-50%,-50%);display:flex;align-items:center;justify-content:center;font-size:0.6rem;font-weight:700;color:white;pointer-events:auto;cursor:pointer" onclick="editHotspot(${h.id})" title="${h.label || h.part_name || ''}">${h.callout_number || ''}</div>`;
}).join('');
// List
if (currentEditorHotspots.length === 0) {
list.innerHTML = '<p style="color:var(--text-secondary);font-size:0.85rem">No hay hotspots</p>';
return;
}
list.innerHTML = currentEditorHotspots.map(h => `
<div style="background:var(--bg-hover);border:1px solid var(--border);border-radius:6px;padding:0.5rem;margin-bottom:0.4rem;display:flex;align-items:center;gap:0.5rem">
<span style="background:var(--accent);color:white;width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.65rem;font-weight:700;flex-shrink:0">${h.callout_number || '?'}</span>
<div style="flex:1;min-width:0">
<div style="font-size:0.82rem;font-weight:500">${h.part_name || h.label || 'Sin parte'}</div>
<div style="font-size:0.72rem;color:var(--text-secondary)">${h.part_number || ''} | ${h.coords}</div>
</div>
<button class="btn btn-secondary" style="padding:0.2rem 0.5rem;font-size:0.75rem" onclick="editHotspot(${h.id})">Editar</button>
<button class="btn" style="padding:0.2rem 0.5rem;font-size:0.75rem;background:#e74c3c;color:white;border:none;border-radius:4px;cursor:pointer" onclick="deleteHotspot(${h.id})">Borrar</button>
</div>
`).join('');
}
function editHotspot(hotspotId) {
const hs = currentEditorHotspots.find(h => h.id === hotspotId);
if (!hs) return;
document.getElementById('hsEditId').value = hs.id;
document.getElementById('hsCoords').value = hs.coords || '';
document.getElementById('hsCallout').value = hs.callout_number || '';
document.getElementById('hsLabel').value = hs.label || '';
document.getElementById('hsPartId').value = hs.part_id || '';
document.getElementById('hsPartSearch').value = hs.part_name ? `${hs.part_number} - ${hs.part_name}` : '';
}
function clearHotspotForm() {
document.getElementById('hsEditId').value = '';
document.getElementById('hsCoords').value = '';
document.getElementById('hsLabel').value = '';
document.getElementById('hsPartId').value = '';
document.getElementById('hsPartSearch').value = '';
document.getElementById('hsPartSelect').style.display = 'none';
// Keep callout at next number
const maxCallout = currentEditorHotspots.reduce((max, h) => Math.max(max, h.callout_number || 0), 0);
document.getElementById('hsCallout').value = maxCallout + 1;
}
async function searchPartsForHotspot(query) {
clearTimeout(partSearchTimeout);
const select = document.getElementById('hsPartSelect');
if (!query || query.length < 2) {
select.style.display = 'none';
return;
}
partSearchTimeout = setTimeout(async () => {
try {
const res = await fetch(`/api/parts?search=${encodeURIComponent(query)}&per_page=20`);
const data = await res.json();
const parts = data.data || data;
if (parts.length === 0) {
select.innerHTML = '<option disabled>Sin resultados</option>';
} else {
select.innerHTML = parts.map(p =>
`<option value="${p.id}">${p.oem_part_number} - ${p.name_es || p.name}</option>`
).join('');
}
select.style.display = 'block';
select.onchange = function() {
const opt = select.options[select.selectedIndex];
document.getElementById('hsPartId').value = opt.value;
document.getElementById('hsPartSearch').value = opt.textContent;
select.style.display = 'none';
};
} catch (e) {
select.innerHTML = '<option disabled>Error buscando</option>';
select.style.display = 'block';
}
}, 300);
}
async function saveHotspot() {
const editId = document.getElementById('hsEditId').value;
const coords = document.getElementById('hsCoords').value.trim();
const callout = parseInt(document.getElementById('hsCallout').value) || null;
const partId = parseInt(document.getElementById('hsPartId').value) || null;
const label = document.getElementById('hsLabel').value.trim();
if (!coords) {
showAlert('Haz clic en la imagen para seleccionar posición', 'error');
return;
}
const body = {
diagram_id: currentEditorDiagramId,
coords: coords,
callout_number: callout,
part_id: partId,
label: label,
shape: 'circle',
color: '#e74c3c'
};
try {
let res;
if (editId) {
res = await fetch(`/api/admin/hotspots/${editId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
} else {
res = await fetch('/api/admin/hotspots', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
}
const result = await res.json();
if (!res.ok) throw new Error(result.error || 'Error al guardar');
showAlert(editId ? 'Hotspot actualizado' : 'Hotspot creado');
// Reload diagram to refresh hotspots
await openHotspotEditor(currentEditorDiagramId);
} catch (e) {
showAlert(e.message, 'error');
}
}
async function deleteHotspot(hotspotId) {
if (!confirm('Eliminar este hotspot?')) return;
try {
const res = await fetch(`/api/admin/hotspots/${hotspotId}`, { method: 'DELETE' });
const result = await res.json();
if (!res.ok) throw new Error(result.error || 'Error al eliminar');
showAlert('Hotspot eliminado');
await openHotspotEditor(currentEditorDiagramId);
} catch (e) {
showAlert(e.message, 'error');
}
}