feat: sub-tabs Standards/Formfaktoren + rich form factor detail panel

Standards tab now has two sub-tabs:
  - Standards: existing table (default)
  - Formfaktoren: full-page grid with search/family/status filters

Form factor detail panel (openFormFactorDetail) rebuilt as async:
  - Mini inline hype cycle curve SVG with dot at exact position
  - Buy signal per form factor (BUY_NOW / CONSIDER / WAIT / HOLD / AVOID)
  - Typed use-case bullets per form factor (20 mapped)
  - Top-10 transceiver mini-list (live fetch from /api/transceivers?form_factor=)
  - Clicking a transceiver row opens its full detail panel
  - Supersedes chain as clickable badges (navigate to that form factor)
  - flexoptix.net search link

FF_HYPE data: curated hype cycle positions for all 20 form factors
FF_USE_CASES: tailored use case lists per form factor
filterFormFactors: now also filters by search text + status dropdown
This commit is contained in:
Rene Fichtmueller 2026-04-25 21:19:13 +02:00
parent 6eca121125
commit 3f7395ea8d

View File

@ -1124,53 +1124,78 @@
<!-- Sourcing Activity Banner --> <!-- Sourcing Activity Banner -->
<div id="sourcing-activity-banner" style="margin-bottom:1rem"></div> <div id="sourcing-activity-banner" style="margin-bottom:1rem"></div>
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;flex-wrap:wrap"> <!-- Sub-tabs: Standards | Formfaktoren -->
<input type="text" id="std-search" placeholder="Search standards (e.g. 400G, QSFP-DD, ZR)..." <div style="display:flex;gap:0;margin-bottom:1.25rem;border-bottom:2px solid var(--border)">
style="flex:1;min-width:200px;padding:8px 12px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.85rem" <button id="std-sub-btn-standards" onclick="switchStdSubtab('standards')"
oninput="filterStandardsTable()"> style="padding:8px 20px;font-size:0.85rem;font-weight:600;border:none;background:none;color:var(--accent);border-bottom:2px solid var(--accent);margin-bottom:-2px;cursor:pointer">
<select id="std-speed-filter" onchange="filterStandardsTable()" Standards
style="padding:8px 10px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.85rem"> </button>
<option value="">All Speeds</option> <button id="std-sub-btn-formfaktoren" onclick="switchStdSubtab('formfaktoren')"
<option value="10">10G</option> style="padding:8px 20px;font-size:0.85rem;font-weight:600;border:none;background:none;color:var(--text-dim);border-bottom:2px solid transparent;margin-bottom:-2px;cursor:pointer">
<option value="25">25G</option> Formfaktoren
<option value="40">40G</option> </button>
<option value="100">100G</option>
<option value="200">200G</option>
<option value="400">400G</option>
<option value="800">800G</option>
<option value="1600">1.6T</option>
</select>
</div> </div>
<div class="card">
<div class="table-wrap"> <!-- Sub-tab: Standards -->
<table> <div id="std-subtab-standards">
<thead><tr> <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;flex-wrap:wrap">
<th style="min-width:160px">Standard Name</th><th>Speed</th><th>Form Factor(s)</th> <input type="text" id="std-search" placeholder="Suche: 400G, QSFP-DD, ZR, Kurzstrecke…"
<th>Max Reach</th><th>Fiber</th><th>Wavelength</th> style="flex:1;min-width:200px;padding:8px 12px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.85rem"
<th>IEEE Ref</th><th>Body · Year</th><th>Status</th><th>Transceivers</th> oninput="filterStandardsTable()">
</tr></thead> <select id="std-speed-filter" onchange="filterStandardsTable()"
<tbody id="std-table"><tr><td colspan="10" class="loading pulse">Loading…</td></tr></tbody> style="padding:8px 10px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.85rem">
</table> <option value="">Alle Speeds</option>
<option value="1">1G</option>
<option value="10">10G</option>
<option value="25">25G</option>
<option value="40">40G</option>
<option value="100">100G</option>
<option value="200">200G</option>
<option value="400">400G</option>
<option value="800">800G</option>
<option value="1600">1.6T</option>
</select>
</div>
<div class="card">
<div class="table-wrap">
<table>
<thead><tr>
<th style="min-width:160px">Standard · Beschreibung</th><th>Speed</th><th>Bauform(en)</th>
<th>Max Reichweite</th><th>Faser</th><th>Wellenlänge</th>
<th>IEEE Ref</th><th>Org · Jahr</th><th>Status</th><th>Transceiver</th>
</tr></thead>
<tbody id="std-table"><tr><td colspan="10" class="loading pulse">Loading…</td></tr></tbody>
</table>
</div>
</div> </div>
</div> </div>
<!-- Form Factors Reference --> <!-- Sub-tab: Formfaktoren -->
<div style="margin-top:1.5rem"> <div id="std-subtab-formfaktoren" class="hidden">
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.75rem;flex-wrap:wrap"> <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;flex-wrap:wrap">
<h3 style="font-size:0.95rem;font-weight:700;color:var(--text-bright);margin:0">Modul-Bauformen (Form Factors)</h3> <input type="text" id="ff-search" placeholder="Suche: QSFP28, SFP+, 100G, Aktuell…"
<span style="font-size:0.72rem;color:var(--text-dim);background:var(--surface3);padding:2px 8px;border-radius:6px">Erklärung für nicht-technische Kolleg:innen</span> style="flex:1;min-width:200px;padding:8px 12px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.85rem"
oninput="filterFormFactors()">
<select id="ff-family-filter" onchange="filterFormFactors()" <select id="ff-family-filter" onchange="filterFormFactors()"
style="margin-left:auto;padding:6px 10px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.8rem"> style="padding:8px 10px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.8rem">
<option value="">Alle Familien</option> <option value="">Alle Familien</option>
<option value="SFP family">SFP-Familie</option> <option value="SFP family">SFP-Familie</option>
<option value="QSFP family">QSFP-Familie</option> <option value="QSFP family">QSFP-Familie</option>
<option value="OSFP family">OSFP-Familie</option> <option value="OSFP family">OSFP-Familie</option>
<option value="CFP family">CFP-Familie</option> <option value="CFP family">CFP-Familie</option>
<option value="legacy">Legacy / Veraltet</option>
</select>
<select id="ff-status-filter" onchange="filterFormFactors()"
style="padding:8px 10px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.8rem">
<option value="">Alle Status</option>
<option value="current">Aktuell</option>
<option value="emerging">Neu / Emerging</option>
<option value="legacy">Legacy</option> <option value="legacy">Legacy</option>
<option value="obsolete">Veraltet</option>
</select> </select>
</div> </div>
<div id="ff-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:0.75rem"> <div id="ff-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(310px,1fr));gap:0.75rem">
<div class="card" style="padding:1rem;text-align:center;color:var(--text-dim);font-size:0.85rem" id="ff-loading"> <div class="card" style="padding:1rem;text-align:center;color:var(--text-dim);font-size:0.85rem">
<span class="loading pulse">Lade Bauformen…</span> <span class="loading pulse">Lade Bauformen…</span>
</div> </div>
</div> </div>
@ -4648,22 +4673,46 @@ async function loadStandardsList() {
_allStandards = data.data || []; _allStandards = data.data || [];
} }
filterStandardsTable(); filterStandardsTable();
// Also load form factors (lazy) }
if (_allFormFactors.length === 0) loadFormFactors();
// ── SUB-TAB SWITCHING ───────────────────────────────────────────────────────
function switchStdSubtab(tab) {
['standards','formfaktoren'].forEach(function(t) {
var content = el('std-subtab-' + t);
var btn = el('std-sub-btn-' + t);
if (!content || !btn) return;
var active = t === tab;
content.classList.toggle('hidden', !active);
btn.style.color = active ? 'var(--accent)' : 'var(--text-dim)';
btn.style.borderBottom = active ? '2px solid var(--accent)' : '2px solid transparent';
});
if (tab === 'formfaktoren') {
if (_allFormFactors.length === 0) loadFormFactors(); else renderFormFactors();
}
} }
async function loadFormFactors() { async function loadFormFactors() {
if (_allFormFactors.length > 0) { renderFormFactors(); return; } if (_allFormFactors.length > 0) { renderFormFactors(); return; }
var grid = el('ff-grid');
if (grid) grid.innerHTML = '<div class="card" style="padding:1rem;color:var(--text-dim);font-size:0.85rem;grid-column:1/-1"><span class="loading pulse">Lade Bauformen…</span></div>';
var data = await api('/api/form-factors').catch(function() { return {}; }); var data = await api('/api/form-factors').catch(function() { return {}; });
_allFormFactors = data.data || []; _allFormFactors = data.data || [];
renderFormFactors(); renderFormFactors();
} }
function filterFormFactors() { function filterFormFactors() {
var family = el('ff-family-filter') ? el('ff-family-filter').value : ''; var q = (el('ff-search') ? el('ff-search').value.toLowerCase() : '');
var filtered = family var family = (el('ff-family-filter') ? el('ff-family-filter').value : '');
? _allFormFactors.filter(function(f) { return (f.family || '') === family; }) var status = (el('ff-status-filter') ? el('ff-status-filter').value : '');
: _allFormFactors; var filtered = _allFormFactors.filter(function(f) {
var matchQ = !q || (f.name || '').toLowerCase().includes(q)
|| (f.full_name || '').toLowerCase().includes(q)
|| (f.description || '').toLowerCase().includes(q)
|| String(f.max_speed_gbps || '').includes(q);
var matchFamily = !family || (f.family || '') === family;
var matchStatus = !status || (f.status || '') === status;
return matchQ && matchFamily && matchStatus;
});
renderFormFactors(filtered); renderFormFactors(filtered);
} }
@ -4715,67 +4764,234 @@ function renderFormFactors(items) {
}).join(''); }).join('');
} }
function openFormFactorDetail(name) { // ── FORM FACTOR HYPE CYCLE DATA (static, curated per form factor) ────────────
var FF_HYPE = {
'CXP': { phase:'LEGACY_DECLINE', pct:98, signal:'AVOID', sigCol:'#c1121f', sigLbl:'Veraltet — nicht mehr einsetzen' },
'XFP': { phase:'LEGACY_DECLINE', pct:93, signal:'AVOID', sigCol:'#c1121f', sigLbl:'Legacy — durch SFP+ vollständig ersetzt' },
'CFP': { phase:'LEGACY_DECLINE', pct:90, signal:'MIGRATE', sigCol:'#888', sigLbl:'Migration zu CFP2/CFP4 planen' },
'SFP': { phase:'PLATEAU_OF_PRODUCTIVITY', pct:88, signal:'BUY_NOW', sigCol:'#2d6a4f', sigLbl:'Stabil & günstig — jetzt kaufen' },
'SFP+': { phase:'PLATEAU_OF_PRODUCTIVITY', pct:84, signal:'BUY_NOW', sigCol:'#2d6a4f', sigLbl:'Bewährt & preislich ausgereift' },
'QSFP+': { phase:'PLATEAU_OF_PRODUCTIVITY', pct:80, signal:'BUY_NOW', sigCol:'#2d6a4f', sigLbl:'40G-Standard — stabile Preise' },
'SFP28': { phase:'PLATEAU_OF_PRODUCTIVITY', pct:77, signal:'BUY_NOW', sigCol:'#2d6a4f', sigLbl:'25G-Standard — bestes Preis-Leistungs-Verhältnis' },
'QSFP28': { phase:'PLATEAU_OF_PRODUCTIVITY', pct:74, signal:'BUY_NOW', sigCol:'#2d6a4f', sigLbl:'100G-Standard — Top Preis-Leistung' },
'CFP4': { phase:'SLOPE_OF_ENLIGHTENMENT', pct:68, signal:'BUY_NOW', sigCol:'#4287f5', sigLbl:'Kompaktes 100G-WDM — Preise stabil' },
'CFP2': { phase:'SLOPE_OF_ENLIGHTENMENT', pct:65, signal:'BUY_NOW', sigCol:'#4287f5', sigLbl:'Kohärent WDM — Reifephase' },
'QSFP56': { phase:'SLOPE_OF_ENLIGHTENMENT', pct:60, signal:'CONSIDER', sigCol:'#4287f5', sigLbl:'200G — Preise fallen noch' },
'SFP56': { phase:'SLOPE_OF_ENLIGHTENMENT', pct:56, signal:'CONSIDER', sigCol:'#4287f5', sigLbl:'50G — noch frühe Marktphase' },
'SFP56-DD': { phase:'SLOPE_OF_ENLIGHTENMENT', pct:53, signal:'CONSIDER', sigCol:'#4287f5', sigLbl:'100G SFP-Dichte — noch teuer' },
'QSFP-DD': { phase:'SLOPE_OF_ENLIGHTENMENT', pct:50, signal:'CONSIDER', sigCol:'#4287f5', sigLbl:'400G — stark wachsend, Preise sinken' },
'OSFP': { phase:'SLOPE_OF_ENLIGHTENMENT', pct:47, signal:'CONSIDER', sigCol:'#4287f5', sigLbl:'400G/800G — Reifung in Gang' },
'QSFP112': { phase:'TROUGH_OF_DISILLUSIONMENT', pct:40, signal:'WAIT', sigCol:'#e6a800', sigLbl:'400G QP112 — Preise noch hoch' },
'QSFP-DD800': { phase:'PEAK_OF_INFLATED_EXPECTATIONS', pct:32, signal:'WAIT', sigCol:'#FF8100', sigLbl:'800G QSFP-DD — hoher Hype, hohe Preise' },
'OSFP112': { phase:'PEAK_OF_INFLATED_EXPECTATIONS', pct:22, signal:'HOLD', sigCol:'#FF8100', sigLbl:'800G/1.6T — sehr früh, nur wenn nötig' },
'SFP112': { phase:'PEAK_OF_INFLATED_EXPECTATIONS', pct:18, signal:'HOLD', sigCol:'#FF8100', sigLbl:'100G SFP-Dichte — Pilotphase' },
'OSFP224': { phase:'INNOVATION_TRIGGER', pct:5, signal:'HOLD', sigCol:'#7c3aed', sigLbl:'1.6T — zu früh, noch kein Massenmarkt' }
};
// ── FORM FACTOR USE CASES ─────────────────────────────────────────────────────
var FF_USE_CASES = {
'SFP': ['Endgeräte: PCs, IP-Telefone, Drucker, Kameras', '1G Management-Ports an Switches & Routern', 'Out-of-Band Management (Konsolen-Server)', 'Kleine Büronetzwerke & Campus LAN', 'IoT-Geräte und Sensorknoten'],
'SFP+': ['10G Server → Top-of-Rack-Switch (klassischer Enterprise-Standard)', 'Campus-Aggregation und Enterprise-Zugangslayer', 'Storage-Netzwerke (iSCSI, NFS over 10G)', 'VMware vSphere / vSAN Cluster Fabric', 'Uplinks zwischen Switches in mittleren RZs'],
'SFP28': ['25G Server-NIC → ToR-Switch (Standard seit 2017)', 'Hyperscaler Server-zu-Switch in modernen RZs', 'NVMe-oF Storage-Fabric (25G)', 'ToR-zu-Aggregation in Leaf/Spine-Architekturen', '25G BreakOut aus QSFP28 (4×25G)'],
'SFP56': ['50G Server-NICs (selten, Nischenmarkt)', 'High-Density 50G Zugangslayer', 'BreakOut aus QSFP56 (4×50G)', 'Übergangsformat zwischen 25G und 100G'],
'SFP56-DD': ['100G in SFP-Portdichte (Hochdichte ToR)', 'Alternativer 100G-Slot ohne QSFP28-Hardware', 'Selten — QSFP28 meist bevorzugt'],
'SFP112': ['Zukünftig: 100G im kleinen SFP-Formfaktor', 'Ultra-Hochdichte Server-Fabric', 'Noch nicht weit verfügbar (2024+)'],
'XFP': ['Sehr alte 10G Switches und Router (Legacy)', '10G WAN-Verbindungen in alter Telko-Infrastruktur', 'Ersatz nur für bestehende Slots nötig — keine Neuinstallation'],
'QSFP+': ['40G Spine-Uplinks (Legacy — wird durch 100G abgelöst)', '40G Blade-Server-Anbindung', 'Bonding aus 4×10G SFP+-Verbindungen', '40G BreakOut: 4×10G SFP+ über ein Kabel'],
'QSFP28': ['100G Spine/Leaf in modernen Rechenzentren', '100G Server-Uplinks (AI/ML Cluster, Standard)', '100G Campus-Core und WAN-Edge', 'BreakOut: 4×25G SFP28 oder 2×50G SFP56', 'Dominant für 100G weltweit — bestes Preis-Leistungs-Verhältnis'],
'QSFP56': ['200G Spine-Uplinks und Aggregation', 'Hochdichte 200G Server-Fabric', '200G als BreakOut: 2×100G QSFP28', 'Übergangsformat auf dem Weg zu 400G'],
'QSFP112': ['400G mit maximaler QSFP-Portdichte', 'Next-Gen Hyperscale ToR-Switches', 'BreakOut: 4×100G mit einem Modul', 'Alternative zu QSFP-DD bei höherer Dichte'],
'QSFP-DD': ['400G Spine-Fabric in Hyperscale RZs', 'AI/ML Training-Cluster-Interconnect (400G)', '400G WAN-Edge und Carrier-Grade-Verbindungen', 'BreakOut: 4×100G QSFP28 oder 8×50G SFP56', 'Standard 400G neben OSFP — breite Unterstützung'],
'QSFP-DD800': ['800G AI/ML GPU-Cluster (NVIDIA H100, H200, Grace-Hopper)', 'Nächste Generation Hyperscale Spine-Switches', '800G BreakOut: 2×400G oder 8×100G', 'Hohes Wachstum durch AI-Infrastruktur-Boom'],
'OSFP': ['400G/800G Spine (Cisco Nexus 9000, Arista 7800)', 'Kohärente 400G/800G ZR/ZR+ WAN-Module (mehr Platz für DCO)', 'High-Power-Optiken: mehr Kühlung als QSFP-DD möglich', 'AI/ML Cluster in Cisco-basierten Setups'],
'OSFP112': ['800G und zukünftige 1.6T AI-Cluster', 'Nächste Generation GPU-Fabric (NVIDIA Scale-out)', 'Ultra-High-Bandwidth Spine für KI-Infrastruktur', '2×800G BreakOut in einem Modul'],
'OSFP224': ['1.6T AI/ML Mega-Cluster (zukünftig)', 'NVIDIA GB200 NVLink 5.0 / Spectrum-X Scale-out', 'Next-Gen 800G/1.6T Hyperscale Fabrics (2025/2026)'],
'CFP': ['Historisch: Erste 100G WAN-Verbindungen (veraltet)', 'Nur noch Ersatz für bestehende Slots'],
'CFP2': ['Kohärente 100G/200G WAN (DWDM Transponder)', 'Tunable DWDM für Metro und Long-Haul-Netze', 'Telko-Backbone und Provider-Core-Equipment', 'Pluggable Coherent für 400G-ZR (CFP2-DCO)'],
'CFP4': ['Kompaktes tunable 100G DWDM (doppelte CFP2-Dichte)', 'Provider-Edge-Equipment und Metro-Netze', 'DWDM wo mehr Ports als CFP2 nötig sind'],
'CXP': ['Praktisch nicht mehr im Einsatz', 'Nur in allerersten 100G-Pilotinstallationen (2010-2012)', 'Ersatz: QSFP28 verwenden']
};
function _ffMiniHypeSVG(pct, col) {
// Compact 240×54px hype cycle curve SVG with a dot
var W = 240, H = 52;
// Approximate Gartner curve via cubic bezier (normalized 0-1, scaled to W×H)
// key points: start(0,0.45), peak(0.28,0.05), trough(0.52,0.70), slope(0.74,0.25), plateau(1.0,0.30)
var pts = [[0,0.46],[0.14,0.35],[0.22,0.07],[0.28,0.05],[0.34,0.12],[0.44,0.60],[0.52,0.70],[0.61,0.55],[0.72,0.24],[0.80,0.22],[1.0,0.25]];
// Convert to SVG coords (Y flipped, add 4px padding)
var pad = 6;
function sx(x){ return pad + x * (W - 2*pad); }
function sy(y){ return pad + y * (H - 2*pad - 6); }
// Build smooth polyline
var d = 'M ' + sx(pts[0][0]) + ' ' + sy(pts[0][1]);
for (var i=1; i<pts.length-1; i++) {
var cx = (pts[i][0]+pts[i+1][0])/2, cy = (pts[i][1]+pts[i+1][1])/2;
d += ' Q ' + sx(pts[i][0]) + ' ' + sy(pts[i][1]) + ' ' + sx(cx) + ' ' + sy(cy);
}
d += ' L ' + sx(pts[pts.length-1][0]) + ' ' + sy(pts[pts.length-1][1]);
// Find dot position by interpolating the curve at pct (0-100)
var t = pct / 100;
var seg = 0;
while (seg < pts.length-2 && pts[seg+1][0] < t) seg++;
var p0 = pts[Math.min(seg, pts.length-2)], p1 = pts[Math.min(seg+1, pts.length-1)];
var localT = p1[0]===p0[0] ? 0.5 : (t - p0[0]) / (p1[0] - p0[0]);
var dx = sx(p0[0] + localT*(p1[0]-p0[0])), dy = sy(p0[1] + localT*(p1[1]-p0[1]));
return '<svg width="' + W + '" height="' + H + '" style="display:block">'
+ '<path d="' + d + '" fill="none" stroke="var(--border)" stroke-width="2.5" stroke-linecap="round"/>'
+ '<circle cx="' + dx + '" cy="' + dy + '" r="6" fill="' + col + '" stroke="#fff" stroke-width="2"/>'
+ '<circle cx="' + dx + '" cy="' + dy + '" r="10" fill="' + col + '" opacity="0.2"/>'
+ '</svg>';
}
async function openFormFactorDetail(name) {
var f = _allFormFactors.find(function(x) { return x.name === name; }); var f = _allFormFactors.find(function(x) { return x.name === name; });
if (!f) return; if (!f) return;
var fCl = { 'SFP family': '#0ea5e9', 'QSFP family': '#6366f1', 'OSFP family': '#FF8100', 'CFP family': '#2d6a4f', 'legacy': '#888' }[f.family] || '#888'; var fCl = { 'SFP family': '#0ea5e9', 'QSFP family': '#6366f1', 'OSFP family': '#FF8100', 'CFP family': '#2d6a4f', 'legacy': '#888' }[f.family] || '#888';
var descFull = f.description || ''; var descFull = f.description || '';
var descDE = descFull.split('//')[0].trim(); var descDE = descFull.split('//')[0].trim();
var descEN = descFull.includes('//') ? descFull.split('//')[1].trim() : ''; var descEN = descFull.includes('//') ? descFull.split('//')[1].trim() : '';
var maxSpd = f.max_speed_gbps >= 1000 ? (f.max_speed_gbps/1000) + 'T' : (f.max_speed_gbps || '?') + 'G'; var maxSpd = f.max_speed_gbps >= 1000 ? (f.max_speed_gbps/1000) + 'T' : (f.max_speed_gbps || '?') + 'G';
var supersedes = Array.isArray(f.supersedes) ? f.supersedes.filter(Boolean) : []; var supersedes = Array.isArray(f.supersedes) ? f.supersedes.filter(Boolean) : [];
var hype = FF_HYPE[f.name] || { phase:'—', pct:50, signal:'—', sigCol:'#888', sigLbl:'Keine Daten' };
var useCases = FF_USE_CASES[f.name] || [];
var phaseLabels = { INNOVATION_TRIGGER:'Innovation Trigger', PEAK_OF_INFLATED_EXPECTATIONS:'Peak of Inflated Expectations',
TROUGH_OF_DISILLUSIONMENT:'Trough of Disillusionment', SLOPE_OF_ENLIGHTENMENT:'Slope of Enlightenment',
PLATEAU_OF_PRODUCTIVITY:'Plateau of Productivity', LEGACY_DECLINE:'Legacy / Decline' };
// Open panel immediately with skeleton
openPanel('<div style="padding:0.5rem;color:var(--text-dim);font-size:0.85rem" class="loading pulse">Lade ' + esc(name) + '…</div>');
// Fetch transceivers for this form factor
var txData = await api('/api/transceivers?form_factor=' + encodeURIComponent(f.name) + '&limit=10').catch(function(){ return {}; });
var txRows = txData.data || [];
var h = ''; var h = '';
h += '<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.75rem">';
h += '<span style="font-size:1.2rem;font-weight:700;color:var(--text-bright)">' + esc(f.name) + '</span>'; // ── Header ─────────────────────────────────────────────────────────────────
h += '<span style="font-size:0.8rem;color:var(--text-dim)">' + esc(f.full_name || '') + '</span>'; h += '<div style="display:flex;align-items:flex-start;gap:0.6rem;flex-wrap:wrap;margin-bottom:0.1rem">';
h += '<div>';
h += '<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap">';
h += '<span style="font-size:1.15rem;font-weight:700;color:var(--text-bright)">' + esc(f.name) + '</span>';
h += '<span style="background:' + fCl + '22;color:' + fCl + ';padding:1px 8px;border-radius:6px;font-size:0.7rem;font-weight:700">' + maxSpd + '</span>';
var sLbl = { current:'Aktuell', emerging:'Neu', legacy:'Legacy', obsolete:'Veraltet' }[f.status] || f.status || '';
var sCl = { current:'#2d6a4f', emerging:'#e6a800', legacy:'#888', obsolete:'#c1121f' }[f.status] || '#888';
h += '<span style="background:' + sCl + '22;color:' + sCl + ';padding:1px 8px;border-radius:6px;font-size:0.7rem;font-weight:600">' + esc(sLbl) + '</span>';
h += '</div>'; h += '</div>';
// Plain-language description h += '<div style="font-size:0.78rem;color:var(--text-dim);margin-top:3px">' + esc(f.full_name || '') + '</div>';
h += '</div>';
h += '</div>';
// ── Plain-language description ─────────────────────────────────────────────
if (descDE) { if (descDE) {
h += '<div class="card" style="padding:1rem;border-left:3px solid ' + fCl + ';margin-bottom:1rem">'; h += '<div class="card" style="padding:0.85rem 1rem;border-left:3px solid ' + fCl + ';margin:0.85rem 0">';
h += '<div style="font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-dim);margin-bottom:0.5rem">Was ist das?</div>'; h += '<div style="font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-dim);margin-bottom:0.4rem">Was ist das?</div>';
h += '<div style="font-size:0.83rem;color:var(--text);line-height:1.6">' + esc(descDE) + '</div>'; h += '<div style="font-size:0.83rem;color:var(--text);line-height:1.6">' + esc(descDE) + '</div>';
if (descEN) { if (descEN) h += '<div style="font-size:0.73rem;color:var(--text-dim);margin-top:0.5rem;border-top:1px solid var(--border);padding-top:0.45rem;font-style:italic">' + esc(descEN) + '</div>';
h += '<div style="font-size:0.75rem;color:var(--text-dim);margin-top:0.5rem;border-top:1px solid var(--border);padding-top:0.5rem;font-style:italic">' + esc(descEN) + '</div>';
}
h += '</div>'; h += '</div>';
} }
// Specs grid
var statusLabels = { current: 'Aktuell', emerging: 'Neu / Emerging', legacy: 'Legacy', obsolete: 'Veraltet / Obsolete' }; // ── Hype Cycle Position ────────────────────────────────────────────────────
h += '<div class="card" style="padding:0.85rem 1rem;margin-bottom:0.85rem">';
h += '<div style="font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-dim);margin-bottom:0.6rem">Hype Cycle Position</div>';
h += '<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">';
h += '<div>' + _ffMiniHypeSVG(hype.pct, hype.sigCol) + '</div>';
h += '<div>';
h += '<div style="font-size:0.78rem;font-weight:700;color:' + hype.sigCol + '">' + esc(phaseLabels[hype.phase] || hype.phase) + '</div>';
h += '<div style="font-size:0.72rem;color:var(--text-dim);margin-top:3px;max-width:140px;line-height:1.4">' + esc(hype.sigLbl) + '</div>';
// Buy signal badge
var bsLabel = { BUY_NOW:'✓ Jetzt kaufen', CONSIDER:'→ In Betracht ziehen', WAIT:'⏳ Warten', HOLD:'⚠ Halten', MIGRATE:'↗ Migration planen', AVOID:'✗ Vermeiden' };
h += '<div style="margin-top:0.5rem;display:inline-block;background:' + hype.sigCol + '22;color:' + hype.sigCol + ';padding:3px 10px;border-radius:8px;font-size:0.72rem;font-weight:700">' + esc(bsLabel[hype.signal] || hype.signal) + '</div>';
h += '</div>';
h += '</div>';
h += '</div>';
// ── Key Specs ──────────────────────────────────────────────────────────────
var statusLabels = { current:'Aktuell', emerging:'Neu / Emerging', legacy:'Legacy', obsolete:'Veraltet' };
var specs = [ var specs = [
['Max. Geschwindigkeit', maxSpd], ['Max. Speed', maxSpd],
['Kanäle', f.channels ? f.channels + ' × ' + (f.channel_rate_gbps || '?') + 'G' : '—'], ['Kanäle', f.channels ? f.channels + ' × ' + (f.channel_rate_gbps || '?') + 'G' : null],
['Familie', f.family || '—'], ['Stecker', f.connector_type || null],
['Status', statusLabels[f.status] || f.status || '—'], ['Hot-swap', f.hot_swap ? 'Ja — im Betrieb tauschbar' : 'Nein'],
['Hot-swap', f.hot_swap ? 'Ja — im laufenden Betrieb tauschbar' : 'Nein'], ['Auf Markt seit', f.year_introduced ? String(f.year_introduced) : null],
['Stecker (typisch)', f.connector_type || '—'], ['Größe (B×T)', (f.physical_width_mm && f.physical_height_mm) ? f.physical_width_mm + '×' + f.physical_height_mm + 'mm' : null],
['Auf dem Markt seit', f.year_introduced ? String(f.year_introduced) : '—'], ['Familie', f.family || null],
['Ersetzt durch', f.superseded_by || '—'], ['Ersetzt durch', f.superseded_by || null]
['Größe (B×T)', (f.physical_width_mm && f.physical_height_mm) ? f.physical_width_mm + 'mm × ' + f.physical_height_mm + 'mm' : '—'] ].filter(function(sp){ return sp[1]; });
]; h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.4rem;margin-bottom:0.85rem">';
h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;margin-bottom:1rem">';
specs.forEach(function(sp) { specs.forEach(function(sp) {
if (!sp[1] || sp[1] === '—') return; h += '<div style="background:var(--surface3);padding:0.45rem 0.65rem;border-radius:7px">'
h += '<div style="background:var(--surface3);padding:0.5rem 0.75rem;border-radius:8px">' + '<div style="font-size:0.62rem;color:var(--text-dim);margin-bottom:1px">' + esc(sp[0]) + '</div>'
+ '<div style="font-size:0.65rem;color:var(--text-dim);margin-bottom:2px">' + esc(sp[0]) + '</div>'
+ '<div style="font-size:0.78rem;font-weight:600;color:var(--text-bright)">' + esc(String(sp[1])) + '</div>' + '<div style="font-size:0.78rem;font-weight:600;color:var(--text-bright)">' + esc(String(sp[1])) + '</div>'
+ '</div>'; + '</div>';
}); });
h += '</div>'; h += '</div>';
// ── Supersedes chain ───────────────────────────────────────────────────────
if (supersedes.length) { if (supersedes.length) {
h += '<div style="margin-bottom:1rem"><div style="font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-dim);margin-bottom:0.4rem">Ersetzt diese Bauform(en):</div>'; h += '<div style="margin-bottom:0.85rem">';
h += '<div style="display:flex;gap:0.4rem;flex-wrap:wrap">'; h += '<div style="font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-dim);margin-bottom:0.4rem">Ersetzt:</div>';
supersedes.forEach(function(s) { h += '<span class="b b-blue" style="font-size:0.78rem">' + esc(s) + '</span>'; }); h += '<div style="display:flex;gap:0.35rem;flex-wrap:wrap">';
supersedes.forEach(function(s) {
h += '<span class="b b-blue" style="font-size:0.78rem;cursor:pointer" onclick="openFormFactorDetail(\'' + esc(s) + '\')">' + esc(s) + '</span>';
});
h += '</div></div>'; h += '</div></div>';
} }
if (f.transceiver_count > 0) {
h += '<button class="btn" style="background:' + fCl + ';color:#fff;font-size:0.8rem" ' // ── Use Cases ─────────────────────────────────────────────────────────────
+ 'onclick="goToTab(\'transceivers\');el(\'tx-search\').value=\'' + esc(f.name) + '\';searchTransceivers();closePanel()">' if (useCases.length) {
+ 'Transceiver mit ' + esc(f.name) + ' anzeigen →</button>'; h += '<div style="margin-bottom:0.85rem">';
h += '<div style="font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-dim);margin-bottom:0.45rem">Typische Einsatzgebiete</div>';
useCases.forEach(function(uc) {
h += '<div style="font-size:0.8rem;color:var(--text);display:flex;align-items:flex-start;gap:0.45rem;margin-bottom:0.3rem">'
+ '<span style="color:' + fCl + ';flex-shrink:0;margin-top:1px"></span>' + esc(uc) + '</div>';
});
h += '</div>';
} }
// ── Transceivers in Catalog ────────────────────────────────────────────────
h += '<div style="margin-bottom:0.85rem">';
h += '<div style="font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-dim);margin-bottom:0.45rem">';
h += 'Unsere Transceiver (' + (f.transceiver_count || txRows.length) + ' in Datenbank)';
h += '</div>';
if (txRows.length) {
var speedColors2 = { 1600:'#7c3aed', 800:'#c1121f', 400:'#FF8100', 200:'#e6a800', 100:'#2d6a4f', 40:'#4287f5', 25:'#0ea5e9', 10:'#888', 1:'#555' };
h += '<div style="display:flex;flex-direction:column;gap:0.3rem">';
txRows.slice(0,8).forEach(function(t) {
var spCol = speedColors2[t.speed_gbps] || '#888';
var reach = t.reach_label || (t.reach_meters ? (t.reach_meters >= 1000 ? (t.reach_meters/1000)+'km' : t.reach_meters+'m') : '');
h += '<div style="display:flex;align-items:center;gap:0.5rem;padding:0.35rem 0.6rem;background:var(--surface3);border-radius:7px;cursor:pointer" onclick="closePanel();openTxDetail(\'' + esc(t.id) + '\')">'
+ '<span style="background:' + spCol + '22;color:' + spCol + ';padding:1px 6px;border-radius:5px;font-size:0.68rem;font-weight:700;white-space:nowrap">' + (t.speed_gbps || '?') + 'G</span>'
+ '<span style="font-size:0.78rem;font-weight:600;color:var(--text-bright);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(t.part_number || '—') + '</span>'
+ (t.vendor_name ? '<span style="font-size:0.68rem;color:var(--text-dim);white-space:nowrap">' + esc(t.vendor_name) + '</span>' : '')
+ (reach ? '<span style="font-size:0.68rem;color:var(--cyan);white-space:nowrap">' + esc(reach) + '</span>' : '')
+ '</div>';
});
h += '</div>';
if ((f.transceiver_count || 0) > 8) {
h += '<button class="btn" style="width:100%;margin-top:0.5rem;font-size:0.78rem;background:var(--surface3);color:var(--text)" '
+ 'onclick="goToTab(\'transceivers\');el(\'tx-search\').value=\'' + esc(f.name) + '\';searchTransceivers();closePanel()">'
+ 'Alle ' + (f.transceiver_count || '') + ' ' + esc(f.name) + '-Transceiver anzeigen →</button>';
}
} else {
h += '<div style="font-size:0.8rem;color:var(--text-dim);padding:0.5rem 0">Noch keine Transceiver in der Datenbank.</div>';
}
h += '</div>';
// ── Action buttons ─────────────────────────────────────────────────────────
h += '<div style="display:flex;gap:0.5rem;flex-wrap:wrap">';
h += '<button class="btn" style="flex:1;background:' + fCl + ';color:#fff;font-size:0.8rem" '
+ 'onclick="goToTab(\'transceivers\');el(\'tx-search\').value=\'' + esc(f.name) + '\';searchTransceivers();closePanel()">'
+ 'Alle Transceiver anzeigen →</button>';
h += '<a href="https://www.flexoptix.net/en/search/ajax/suggest/?q=' + encodeURIComponent(f.name) + '" target="_blank" rel="noopener" class="btn" style="font-size:0.8rem;text-decoration:none">flexoptix.net ↗</a>';
h += '</div>';
// ── Technical notes ────────────────────────────────────────────────────────
if (f.notes) { if (f.notes) {
h += '<div class="card" style="margin-top:0.75rem;padding:0.75rem;font-size:0.75rem;color:var(--text-dim);line-height:1.55"><strong style="color:var(--text)">Technische Hinweise:</strong> ' + esc(f.notes) + '</div>'; h += '<div class="card" style="margin-top:0.75rem;padding:0.7rem;font-size:0.73rem;color:var(--text-dim);line-height:1.5">'
+ '<strong style="color:var(--text)">Technisch:</strong> ' + esc(f.notes) + '</div>';
} }
openPanel(f.name + ' — Modul-Bauform', h);
buildDOM(el('panel-content'), h);
} }
function filterStandardsTable() { function filterStandardsTable() {