feat: switch Flexoptix recommendations, switch verified labels, stronger verification check
- getCompatibleTransceivers: adds vendor_name, price, verification fields; Flexoptix sorted first - Switch detail: data quality bar (Image/Product Page/Datasheet confirmed) - Switch detail: Flexoptix Recommended section with prices, verified badges, shop links - Switch detail: other vendors section shows 100% badge on slugs - Transceiver detail: verification condition explicit === true (cache-safe) - Transceiver detail: fallback text when no verification data exists yet
This commit is contained in:
parent
3811b3b953
commit
f91d2a15b9
@ -240,11 +240,22 @@ export async function getSwitchDocuments(switchId: string) {
|
|||||||
|
|
||||||
export async function getCompatibleTransceivers(switchId: string) {
|
export async function getCompatibleTransceivers(switchId: string) {
|
||||||
const result = await pool.query(
|
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
|
FROM compatibility c
|
||||||
JOIN transceivers t ON c.transceiver_id = t.id
|
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'
|
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]
|
[switchId]
|
||||||
);
|
);
|
||||||
return result.rows;
|
return result.rows;
|
||||||
|
|||||||
@ -1856,12 +1856,16 @@ async function openTxDetail(id) {
|
|||||||
h += '<div class="panel-stat"><div class="panel-stat-label">Fiber</div><div class="panel-stat-val" style="font-size:1rem">' + esc(t.fiber_type || '—') + '</div></div>';
|
h += '<div class="panel-stat"><div class="panel-stat-label">Fiber</div><div class="panel-stat-val" style="font-size:1rem">' + esc(t.fiber_type || '—') + '</div></div>';
|
||||||
h += '</div>';
|
h += '</div>';
|
||||||
|
|
||||||
// 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 = [];
|
var verItems = [];
|
||||||
if (t.price_verified) verItems.push('<span style="color:#2d6a4f;font-size:0.75rem">✓ Price</span>');
|
if (pVer) verItems.push('<span style="color:#2d6a4f;font-size:0.75rem;font-weight:600">✓ Price</span>');
|
||||||
if (t.image_verified) verItems.push('<span style="color:#2d6a4f;font-size:0.75rem">✓ Image</span>');
|
if (iVer) verItems.push('<span style="color:#2d6a4f;font-size:0.75rem;font-weight:600">✓ Image</span>');
|
||||||
if (t.details_verified) verItems.push('<span style="color:#2d6a4f;font-size:0.75rem">✓ Details</span>');
|
if (dVer) verItems.push('<span style="color:#2d6a4f;font-size:0.75rem;font-weight:600">✓ Details</span>');
|
||||||
if (t.fully_verified) {
|
if (fVer) {
|
||||||
h += '<div style="display:flex;align-items:center;gap:0.6rem;flex-wrap:wrap;margin:0.8rem 0;padding:0.5rem 0.75rem;background:linear-gradient(135deg,#1b4332,#2d6a4f);border-radius:8px">'
|
h += '<div style="display:flex;align-items:center;gap:0.6rem;flex-wrap:wrap;margin:0.8rem 0;padding:0.5rem 0.75rem;background:linear-gradient(135deg,#1b4332,#2d6a4f);border-radius:8px">'
|
||||||
+ '<span style="color:#fff;font-size:0.8rem;font-weight:700;letter-spacing:0.03em">★ 100% VERIFIED</span>'
|
+ '<span style="color:#fff;font-size:0.8rem;font-weight:700;letter-spacing:0.03em">★ 100% VERIFIED</span>'
|
||||||
+ '<span style="color:rgba(255,255,255,0.6);font-size:0.7rem">–</span>'
|
+ '<span style="color:rgba(255,255,255,0.6);font-size:0.7rem">–</span>'
|
||||||
@ -1872,6 +1876,9 @@ async function openTxDetail(id) {
|
|||||||
h += '<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;margin:0.6rem 0;padding:0.4rem 0.6rem;background:rgba(45,106,79,0.08);border:1px solid rgba(45,106,79,0.2);border-radius:6px">'
|
h += '<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;margin:0.6rem 0;padding:0.4rem 0.6rem;background:rgba(45,106,79,0.08);border:1px solid rgba(45,106,79,0.2);border-radius:6px">'
|
||||||
+ verItems.join('<span style="color:#aaa;font-size:0.7rem">·</span>')
|
+ verItems.join('<span style="color:#aaa;font-size:0.7rem">·</span>')
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
|
} else {
|
||||||
|
// No verification data yet — show neutral indicator
|
||||||
|
h += '<div style="font-size:0.72rem;color:#aaa;margin:0.4rem 0;padding:0.3rem 0.5rem;border-left:2px solid #ddd">Data not yet verified from official sources</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: render a spec section as a clean table (like Flexoptix spec tables)
|
// Helper: render a spec section as a clean table (like Flexoptix spec tables)
|
||||||
@ -2223,7 +2230,19 @@ async function openSwitchDetail(id) {
|
|||||||
h += '</div>';
|
h += '</div>';
|
||||||
|
|
||||||
h += '<div class="panel-title">' + esc(s.model) + '</div>';
|
h += '<div class="panel-title">' + esc(s.model) + '</div>';
|
||||||
h += '<div class="panel-sub">' + esc(s.vendor_name || '') + ' — ' + esc(s.series || '') + '</div>';
|
h += '<div class="panel-sub">' + esc(s.vendor_name || '') + (s.series ? ' — ' + esc(s.series) : '') + '</div>';
|
||||||
|
|
||||||
|
// Data quality indicators for switch
|
||||||
|
var swQual = [];
|
||||||
|
if (s.image_url && !s.image_url.includes('placeholder')) swQual.push('<span style="color:#2d6a4f;font-size:0.75rem">✓ Image</span>');
|
||||||
|
if (s.product_page_url) swQual.push('<span style="color:#2d6a4f;font-size:0.75rem">✓ Product Page</span>');
|
||||||
|
else swQual.push('<span style="color:#aaa;font-size:0.75rem">⚠ Estimated URL</span>');
|
||||||
|
if (s.datasheet_r2_key) swQual.push('<span style="color:#2d6a4f;font-size:0.75rem">✓ Datasheet</span>');
|
||||||
|
if (swQual.length > 0) {
|
||||||
|
h += '<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;margin:0.6rem 0;padding:0.4rem 0.6rem;background:rgba(45,106,79,0.07);border:1px solid rgba(45,106,79,0.18);border-radius:6px">'
|
||||||
|
+ swQual.join('<span style="color:#ccc;font-size:0.7rem">·</span>')
|
||||||
|
+ '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
h += '<div class="panel-grid">';
|
h += '<div class="panel-grid">';
|
||||||
h += '<div class="panel-stat"><div class="panel-stat-label">Category</div><div class="panel-stat-val" style="font-size:1rem">' + esc(s.category || '—') + '</div></div>';
|
h += '<div class="panel-stat"><div class="panel-stat-label">Category</div><div class="panel-stat-val" style="font-size:1rem">' + esc(s.category || '—') + '</div></div>';
|
||||||
@ -2304,20 +2323,64 @@ async function openSwitchDetail(id) {
|
|||||||
var txList = cdata.data || cdata.transceivers || [];
|
var txList = cdata.data || cdata.transceivers || [];
|
||||||
if (txList.length === 0) return;
|
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 += '<div class="panel-section" style="color:#ff6600;margin-top:1rem">Flexoptix Recommended <span style="background:#ff660018;color:#ff6600;font-size:0.7rem;font-weight:700;padding:2px 8px;border-radius:10px;margin-left:0.4rem">' + foList.length + '</span></div>';
|
||||||
|
ch += '<div style="font-size:0.72rem;color:var(--text-dim);margin-bottom:0.5rem">Directly available from Flexoptix — FlexBox coding supported</div>';
|
||||||
|
|
||||||
|
// 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 += '<div style="margin:0.5rem 0 0.3rem;font-weight:600;font-size:0.78rem;color:var(--text-bright)">' + esc(key) + ' <span class="dim" style="font-weight:400;font-size:0.72rem">(' + items.length + ')</span></div>';
|
||||||
|
ch += '<div style="display:flex;flex-direction:column;gap:0.3rem">';
|
||||||
|
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)
|
||||||
|
? '<span style="color:#2d6a4f;font-size:0.64rem;font-weight:600;margin-left:0.3rem">✓ Verified</span>' : '';
|
||||||
|
var fullyBadge = (t.fully_verified === true)
|
||||||
|
? '<span style="background:linear-gradient(135deg,#1b4332,#2d6a4f);color:#fff;font-size:0.6rem;font-weight:700;padding:1px 5px;border-radius:4px;margin-left:0.3rem">★ 100%</span>' : '';
|
||||||
|
var foUrl = t.product_page_url
|
||||||
|
? '<a href="' + esc(t.product_page_url) + '" target="_blank" rel="noopener" style="color:var(--accent);font-size:0.65rem;margin-left:0.4rem;text-decoration:none">Shop ↗</a>' : '';
|
||||||
|
ch += '<div style="display:flex;align-items:center;padding:0.3rem 0.5rem;background:rgba(255,102,0,0.05);border:1px solid rgba(255,102,0,0.15);border-radius:6px;cursor:pointer;gap:0.3rem" onclick="openTxDetail(\'' + esc(t.id) + '\')">'
|
||||||
|
+ '<span style="font-weight:600;font-size:0.78rem;color:var(--text-bright)">' + esc(t.part_number || t.standard_name || t.slug) + '</span>'
|
||||||
|
+ '<span style="color:var(--text-dim);font-size:0.7rem">' + esc(t.reach_label || '') + priceStr + '</span>'
|
||||||
|
+ fullyBadge + verBadge + foUrl
|
||||||
|
+ '</div>';
|
||||||
|
});
|
||||||
|
if (items.length > 8) ch += '<div style="font-size:0.7rem;color:var(--text-dim);padding:0.2rem 0">+' + (items.length - 8) + ' more Flexoptix options</div>';
|
||||||
|
ch += '</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ALL COMPATIBLE (other vendors) ────────────────────────────────────
|
||||||
|
ch += '<div class="panel-section">Compatible Transceivers <span class="b b-green" style="margin-left:0.5rem">' + txList.length + '</span></div>';
|
||||||
var groups = {};
|
var groups = {};
|
||||||
txList.forEach(function(t) {
|
otherList.forEach(function(t) {
|
||||||
var key = (t.form_factor || '?') + ' ' + (t.speed || '?');
|
var key = (t.form_factor || '?') + ' ' + (t.speed || '?');
|
||||||
if (!groups[key]) groups[key] = [];
|
if (!groups[key]) groups[key] = [];
|
||||||
groups[key].push(t);
|
groups[key].push(t);
|
||||||
});
|
});
|
||||||
|
|
||||||
var ch = '<div class="panel-section">Compatible Transceivers <span class="b b-green" style="margin-left:0.5rem">' + txList.length + '</span></div>';
|
|
||||||
Object.keys(groups).sort().forEach(function(key) {
|
Object.keys(groups).sort().forEach(function(key) {
|
||||||
var items = groups[key];
|
var items = groups[key];
|
||||||
ch += '<div style="margin:0.6rem 0 0.3rem;font-weight:600;font-size:0.8rem;color:var(--accent)">' + esc(key) + ' <span class="dim" style="font-weight:400">(' + items.length + ')</span></div>';
|
ch += '<div style="margin:0.6rem 0 0.3rem;font-weight:600;font-size:0.8rem;color:var(--accent)">' + esc(key) + ' <span class="dim" style="font-weight:400">(' + items.length + ')</span></div>';
|
||||||
ch += '<div style="display:flex;flex-wrap:wrap;gap:0.3rem">';
|
ch += '<div style="display:flex;flex-wrap:wrap;gap:0.3rem">';
|
||||||
items.slice(0, 12).forEach(function(t) {
|
items.slice(0, 12).forEach(function(t) {
|
||||||
ch += '<span class="b b-neutral" style="cursor:pointer;font-size:0.7rem" onclick="openTxDetail(\'' + esc(t.id) + '\')">' + esc(t.standard_name || t.slug || t.part_number) + '</span>';
|
var fullyBadge = (t.fully_verified === true) ? '★ ' : '';
|
||||||
|
ch += '<span class="b b-neutral" style="cursor:pointer;font-size:0.7rem" onclick="openTxDetail(\'' + esc(t.id) + '\')" title="' + esc(t.vendor_name || '') + (t.reach_label ? ' · ' + t.reach_label : '') + '">'
|
||||||
|
+ fullyBadge + esc(t.standard_name || t.slug || t.part_number) + '</span>';
|
||||||
});
|
});
|
||||||
if (items.length > 12) ch += '<span class="dim" style="font-size:0.7rem">+' + (items.length - 12) + ' more</span>';
|
if (items.length > 12) ch += '<span class="dim" style="font-size:0.7rem">+' + (items.length - 12) + ' more</span>';
|
||||||
ch += '</div>';
|
ch += '</div>';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user