feat: Switch→Transceiver Finder tab in dashboard

- New 'Finder' tab between Switches and Blog Engine
- Search by switch model (free text), filter by speed
- Quick-access buttons: Nexus 93180, Nexus 9332D, Arista 7280R3A, Juniper QFX5120
- Results grouped by speed class (400G QSFP-DD, 100G QSFP28, etc.)
- Shows: part number, vendor, reach, fiber type, connector, verified price, stock status
- Flexoptix products highlighted with orange left border + FLEXOPTIX badge
- Buy link → flexoptix.net for each result
- Uses existing /api/finder endpoint with 33,993 compatibility entries
This commit is contained in:
Rene Fichtmueller 2026-04-01 17:30:49 +02:00
parent 174078efdb
commit 6a6a22d303

View File

@ -678,6 +678,7 @@
<div class="tab" data-tab="transceivers">Transceivers</div> <div class="tab" data-tab="transceivers">Transceivers</div>
<div class="tab" data-tab="switches">Switches</div> <div class="tab" data-tab="switches">Switches</div>
<div class="tab" data-tab="news">News</div> <div class="tab" data-tab="news">News</div>
<div class="tab" data-tab="finder">Finder</div>
<div class="tab" data-tab="blog">Blog Engine</div> <div class="tab" data-tab="blog">Blog Engine</div>
</div> </div>
@ -859,6 +860,39 @@
<div class="card"><div id="news-list"></div></div> <div class="card"><div id="news-list"></div></div>
</div> </div>
<!-- FINDER -->
<div id="tab-finder" class="hidden">
<div style="margin-bottom:1.2rem">
<h3 style="font-size:1rem;font-weight:600;margin-bottom:0.3rem">Switch → Transceiver Finder <span style="font-size:0.7rem;color:var(--text-dim);font-weight:400">Find the right Flexoptix transceiver for your switch</span></h3>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
<input id="finder-switch-input" type="text" placeholder="Enter switch model, e.g. N9K-C93180YC-FX3 or Nexus 93180..."
style="flex:1;min-width:280px;padding:10px 14px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.9rem"
onkeydown="if(event.key==='Enter') runFinder()">
<select id="finder-speed-filter" style="padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.85rem">
<option value="">All Speeds</option>
<option value="10">10G</option>
<option value="25">25G</option>
<option value="40">40G</option>
<option value="100">100G</option>
<option value="400">400G</option>
<option value="800">800G</option>
</select>
<button onclick="runFinder()" style="background:var(--accent);color:white;border:none;padding:10px 20px;border-radius:8px;cursor:pointer;font-weight:600;font-size:0.9rem">Find Transceivers</button>
</div>
<!-- Quick examples -->
<div style="margin-top:0.5rem;display:flex;gap:0.4rem;flex-wrap:wrap">
<span style="font-size:0.7rem;color:var(--text-dim)">Quick:</span>
<button onclick="finderQuick('N9K-C93180YC-FX3')" class="b b-dim" style="font-size:0.7rem;padding:2px 8px;border-radius:4px">Nexus 93180YC-FX3</button>
<button onclick="finderQuick('N9K-C9332D-GX2B')" class="b b-dim" style="font-size:0.7rem;padding:2px 8px;border-radius:4px">Nexus 9332D-GX2B</button>
<button onclick="finderQuick('7280R3A-48D5')" class="b b-dim" style="font-size:0.7rem;padding:2px 8px;border-radius:4px">Arista 7280R3A</button>
<button onclick="finderQuick('QFX5120-48Y')" class="b b-dim" style="font-size:0.7rem;padding:2px 8px;border-radius:4px">Juniper QFX5120</button>
</div>
</div>
<!-- Results area -->
<div id="finder-results"></div>
</div>
<!-- BLOG --> <!-- BLOG -->
<div id="tab-blog" class="hidden"> <div id="tab-blog" class="hidden">
<div style="margin-bottom:0.8rem;display:flex;justify-content:space-between;align-items:center"> <div style="margin-bottom:0.8rem;display:flex;justify-content:space-between;align-items:center">
@ -1154,6 +1188,7 @@ function goToTab(tabName) {
if (tabName === 'switches') searchSwitches(); if (tabName === 'switches') searchSwitches();
if (tabName === 'news') loadNews(); if (tabName === 'news') loadNews();
if (tabName === 'blog') loadBlogDrafts(); if (tabName === 'blog') loadBlogDrafts();
if (tabName === 'finder') document.getElementById('finder-switch-input').focus();
} }
document.querySelectorAll('.tab').forEach(function(tab) { document.querySelectorAll('.tab').forEach(function(tab) {
@ -2320,6 +2355,125 @@ function pollBlogLlm(id, attempt) {
// Hot topics loaded dynamically via hot-topics.js // Hot topics loaded dynamically via hot-topics.js
// ══════════════════════════════════════════════════════
// FINDER — Switch → Transceiver
// ══════════════════════════════════════════════════════
function finderQuick(model) {
document.getElementById('finder-switch-input').value = model;
runFinder();
}
async function runFinder() {
var model = (document.getElementById('finder-switch-input').value || '').trim();
var speed = document.getElementById('finder-speed-filter').value;
var results = document.getElementById('finder-results');
if (!model) { results.innerHTML = '<p style="color:var(--text-dim)">Enter a switch model to search.</p>'; return; }
results.innerHTML = '<div class="loading pulse">Searching compatibility database...</div>';
var url = '/api/finder?switch=' + encodeURIComponent(model) + (speed ? '&speed=' + speed : '');
try {
var data = await api(url);
if (data.error) {
results.innerHTML = '<div class="card" style="border-left:3px solid #c1121f"><b>Not found:</b> ' + data.error +
(data.suggestion ? '<br><span style="color:var(--text-dim);font-size:0.85rem">' + data.suggestion + '</span>' : '') + '</div>';
return;
}
var sw = data.switch;
var transceivers = data.compatible_transceivers || [];
var total = data.total || 0;
// Switch info header
var swHtml = '<div class="card mb" style="display:flex;gap:1rem;align-items:center">' +
(sw.image_url ? '<img src="' + sw.image_url + '" style="height:60px;border-radius:6px;object-fit:contain" onerror="this.style.display=\'none\'">' : '') +
'<div style="flex:1">' +
'<div style="font-size:1.1rem;font-weight:700">' + sw.vendor + ' ' + sw.model + '</div>' +
'<div style="color:var(--text-dim);font-size:0.8rem">' +
(sw.series ? sw.series + ' · ' : '') +
'Max speed: ' + (sw.max_speed_gbps || '?') + 'G' +
'</div>' +
'</div>' +
'<div style="text-align:right;font-size:0.8rem;color:var(--text-dim)">' +
'<b style="font-size:1.1rem;color:var(--text)">' + total + '</b> compatible transceivers' +
'</div>' +
'</div>';
if (transceivers.length === 0) {
results.innerHTML = swHtml + '<div class="card" style="color:var(--text-dim)">No compatible transceivers found for this switch. Try removing the speed filter.</div>';
return;
}
// Group by speed class
var bySpeed = {};
for (var t of transceivers) {
var key = t.speed_gbps + 'G ' + t.form_factor;
if (!bySpeed[key]) bySpeed[key] = [];
bySpeed[key].push(t);
}
var speedColors = { 800: '#c1121f', 400: '#FF8100', 100: '#e6a800', 25: '#2d6a4f', 10: '#888' };
var tcvrHtml = Object.entries(bySpeed).sort(function(a, b) {
return parseInt(b[0]) - parseInt(a[0]);
}).map(function(entry) {
var speedClass = entry[0];
var items = entry[1];
var speedGbps = items[0].speed_gbps;
var color = speedColors[speedGbps] || '#888';
var cards = items.slice(0, 12).map(function(t) {
var isFlexoptix = (t.vendor || '').toUpperCase() === 'FLEXOPTIX';
var hasPrice = t.price != null;
var priceHtml = hasPrice
? '<span style="color:var(--accent);font-weight:700">' + (t.currency || 'EUR') + ' ' + parseFloat(t.price).toFixed(2) + '</span>'
: '<span style="color:var(--text-dim)">Price on request</span>';
var stockHtml = t.stock === 'in_stock' ? '<span style="color:#2d6a4f;font-size:0.65rem">● In Stock</span>'
: t.stock === 'limited' ? '<span style="color:#e6a800;font-size:0.65rem">● Limited</span>'
: '';
var partNum = t.part_number || t.slug || t.id;
return '<div class="card" style="padding:0.8rem;' + (isFlexoptix ? 'border-left:3px solid var(--accent)' : '') + '">' +
'<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:0.5rem">' +
'<div style="flex:1;min-width:0">' +
(isFlexoptix ? '<span style="font-size:0.6rem;background:var(--accent);color:white;padding:1px 5px;border-radius:3px;margin-right:4px">FLEXOPTIX</span>' : '') +
'<span style="font-size:0.7rem;color:var(--text-dim)">' + (t.vendor || '') + '</span><br>' +
'<b style="font-size:0.85rem;word-break:break-all">' + partNum + '</b><br>' +
'<span style="font-size:0.75rem;color:var(--text-dim)">' +
(t.reach || '') + (t.fiber_type ? ' · ' + t.fiber_type : '') +
(t.connector ? ' · ' + t.connector : '') +
'</span>' +
'</div>' +
'<div style="text-align:right;flex-shrink:0">' +
priceHtml + '<br>' + stockHtml +
'</div>' +
'</div>' +
(t.buy_url ? '<a href="' + t.buy_url + '" target="_blank" style="display:inline-block;margin-top:0.5rem;font-size:0.7rem;color:var(--accent)">Buy at Flexoptix →</a>' : '') +
'</div>';
}).join('');
return '<div style="margin-bottom:1.2rem">' +
'<div style="font-size:0.8rem;font-weight:700;color:' + color + ';margin-bottom:0.5rem;text-transform:uppercase;letter-spacing:0.05em">' +
speedClass + ' <span style="color:var(--text-dim);font-weight:400">(' + items.length + ' options)</span>' +
'</div>' +
'<div class="grid g3">' + cards + '</div>' +
'</div>';
}).join('');
results.innerHTML = swHtml +
'<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.8rem">' +
'Showing ' + Math.min(transceivers.length, total) + ' of ' + total + ' compatible transceivers · ' +
'<span style="color:#2d6a4f">Orange border = Flexoptix product</span>' +
'</div>' +
tcvrHtml;
} catch(e) {
results.innerHTML = '<div class="card" style="border-left:3px solid #c1121f">Error: ' + e.message + '</div>';
}
}
async function loadBlogDrafts() { async function loadBlogDrafts() {