diff --git a/packages/api/src/db/queries.ts b/packages/api/src/db/queries.ts index ef62570..2f657eb 100644 --- a/packages/api/src/db/queries.ts +++ b/packages/api/src/db/queries.ts @@ -240,11 +240,22 @@ export async function getSwitchDocuments(switchId: string) { export async function getCompatibleTransceivers(switchId: string) { const result = await pool.query( - `SELECT t.*, c.status, c.verified_by, c.notes as compat_notes + `SELECT t.*, c.status, c.verified_by, c.notes as compat_notes, + v.name AS vendor_name, v.type AS vendor_type, v.website AS vendor_website, + -- Latest verified price + COALESCE(t.price_verified_eur, + (SELECT po.price FROM price_observations po WHERE po.transceiver_id = t.id ORDER BY po.time DESC LIMIT 1) + ) AS latest_price, + CASE WHEN t.price_verified_eur IS NOT NULL THEN 'EUR' + ELSE (SELECT po.currency FROM price_observations po WHERE po.transceiver_id = t.id ORDER BY po.time DESC LIMIT 1) + END AS latest_currency FROM compatibility c JOIN transceivers t ON c.transceiver_id = t.id + LEFT JOIN vendors v ON t.vendor_id = v.id WHERE c.switch_id = $1 AND c.status = 'compatible' - ORDER BY t.speed_gbps DESC`, + ORDER BY + CASE WHEN LOWER(v.name) = 'flexoptix' THEN 0 ELSE 1 END, + t.speed_gbps DESC`, [switchId] ); return result.rows; diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html index 33cf28b..643bdd0 100644 --- a/packages/dashboard/index.html +++ b/packages/dashboard/index.html @@ -1856,12 +1856,16 @@ async function openTxDetail(id) { h += '
Fiber
' + esc(t.fiber_type || '—') + '
'; h += ''; - // Verification summary bar + // Verification summary bar — explicit === true to handle any type coercion + var pVer = t.price_verified === true; + var iVer = t.image_verified === true; + var dVer = t.details_verified === true; + var fVer = t.fully_verified === true; var verItems = []; - if (t.price_verified) verItems.push('✓ Price'); - if (t.image_verified) verItems.push('✓ Image'); - if (t.details_verified) verItems.push('✓ Details'); - if (t.fully_verified) { + if (pVer) verItems.push('✓ Price'); + if (iVer) verItems.push('✓ Image'); + if (dVer) verItems.push('✓ Details'); + if (fVer) { h += '
' + '★ 100% VERIFIED' + '' @@ -1872,6 +1876,9 @@ async function openTxDetail(id) { h += '
' + verItems.join('·') + '
'; + } else { + // No verification data yet — show neutral indicator + h += '
Data not yet verified from official sources
'; } // Helper: render a spec section as a clean table (like Flexoptix spec tables) @@ -2223,7 +2230,19 @@ async function openSwitchDetail(id) { h += '
'; h += '
' + esc(s.model) + '
'; - h += '
' + esc(s.vendor_name || '') + ' — ' + esc(s.series || '') + '
'; + h += '
' + esc(s.vendor_name || '') + (s.series ? ' — ' + esc(s.series) : '') + '
'; + + // Data quality indicators for switch + var swQual = []; + if (s.image_url && !s.image_url.includes('placeholder')) swQual.push('✓ Image'); + if (s.product_page_url) swQual.push('✓ Product Page'); + else swQual.push('⚠ Estimated URL'); + if (s.datasheet_r2_key) swQual.push('✓ Datasheet'); + if (swQual.length > 0) { + h += '
' + + swQual.join('·') + + '
'; + } h += '
'; h += '
Category
' + esc(s.category || '—') + '
'; @@ -2304,20 +2323,64 @@ async function openSwitchDetail(id) { var txList = cdata.data || cdata.transceivers || []; if (txList.length === 0) return; + // Split: Flexoptix vs others + var foList = txList.filter(function(t) { return (t.vendor_name || '').toLowerCase() === 'flexoptix'; }); + var otherList = txList.filter(function(t) { return (t.vendor_name || '').toLowerCase() !== 'flexoptix'; }); + + var ch = ''; + + // ── FLEXOPTIX RECOMMENDED ────────────────────────────────────────────── + if (foList.length > 0) { + ch += '
Flexoptix Recommended ' + foList.length + '
'; + ch += '
Directly available from Flexoptix — FlexBox coding supported
'; + + // Group Flexoptix by speed class + var foGroups = {}; + foList.forEach(function(t) { + var key = (t.speed || '?') + ' ' + (t.form_factor || '?'); + if (!foGroups[key]) foGroups[key] = []; + foGroups[key].push(t); + }); + + Object.keys(foGroups).sort().forEach(function(key) { + var items = foGroups[key]; + ch += '
' + esc(key) + ' (' + items.length + ')
'; + ch += '
'; + items.slice(0, 8).forEach(function(t) { + var priceStr = t.latest_price ? ' — ' + (t.latest_currency || 'EUR') + ' ' + parseFloat(t.latest_price).toLocaleString('de-DE', {minimumFractionDigits:2,maximumFractionDigits:2}) : ''; + var verBadge = (t.price_verified === true) + ? '✓ Verified' : ''; + var fullyBadge = (t.fully_verified === true) + ? '★ 100%' : ''; + var foUrl = t.product_page_url + ? 'Shop ↗' : ''; + ch += '
' + + '' + esc(t.part_number || t.standard_name || t.slug) + '' + + '' + esc(t.reach_label || '') + priceStr + '' + + fullyBadge + verBadge + foUrl + + '
'; + }); + if (items.length > 8) ch += '
+' + (items.length - 8) + ' more Flexoptix options
'; + ch += '
'; + }); + } + + // ── ALL COMPATIBLE (other vendors) ──────────────────────────────────── + ch += '
Compatible Transceivers ' + txList.length + '
'; var groups = {}; - txList.forEach(function(t) { + otherList.forEach(function(t) { var key = (t.form_factor || '?') + ' ' + (t.speed || '?'); if (!groups[key]) groups[key] = []; groups[key].push(t); }); - - var ch = '
Compatible Transceivers ' + txList.length + '
'; Object.keys(groups).sort().forEach(function(key) { var items = groups[key]; ch += '
' + esc(key) + ' (' + items.length + ')
'; ch += '
'; items.slice(0, 12).forEach(function(t) { - ch += '' + esc(t.standard_name || t.slug || t.part_number) + ''; + var fullyBadge = (t.fully_verified === true) ? '★ ' : ''; + ch += '' + + fullyBadge + esc(t.standard_name || t.slug || t.part_number) + ''; }); if (items.length > 12) ch += '+' + (items.length - 12) + ' more'; ch += '
';