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 += '
';