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:
parent
6eca121125
commit
3f7395ea8d
@ -1124,13 +1124,28 @@
|
||||
<!-- Sourcing Activity Banner -->
|
||||
<div id="sourcing-activity-banner" style="margin-bottom:1rem"></div>
|
||||
|
||||
<!-- Sub-tabs: Standards | Formfaktoren -->
|
||||
<div style="display:flex;gap:0;margin-bottom:1.25rem;border-bottom:2px solid var(--border)">
|
||||
<button id="std-sub-btn-standards" onclick="switchStdSubtab('standards')"
|
||||
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">
|
||||
Standards
|
||||
</button>
|
||||
<button id="std-sub-btn-formfaktoren" onclick="switchStdSubtab('formfaktoren')"
|
||||
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">
|
||||
Formfaktoren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sub-tab: Standards -->
|
||||
<div id="std-subtab-standards">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<input type="text" id="std-search" placeholder="Search standards (e.g. 400G, QSFP-DD, ZR)..."
|
||||
<input type="text" id="std-search" placeholder="Suche: 400G, QSFP-DD, ZR, Kurzstrecke…"
|
||||
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="filterStandardsTable()">
|
||||
<select id="std-speed-filter" onchange="filterStandardsTable()"
|
||||
style="padding:8px 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="">Alle Speeds</option>
|
||||
<option value="1">1G</option>
|
||||
<option value="10">10G</option>
|
||||
<option value="25">25G</option>
|
||||
<option value="40">40G</option>
|
||||
@ -1145,32 +1160,42 @@
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="min-width:160px">Standard Name</th><th>Speed</th><th>Form Factor(s)</th>
|
||||
<th>Max Reach</th><th>Fiber</th><th>Wavelength</th>
|
||||
<th>IEEE Ref</th><th>Body · Year</th><th>Status</th><th>Transceivers</th>
|
||||
<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>
|
||||
|
||||
<!-- Form Factors Reference -->
|
||||
<div style="margin-top:1.5rem">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.75rem;flex-wrap:wrap">
|
||||
<h3 style="font-size:0.95rem;font-weight:700;color:var(--text-bright);margin:0">Modul-Bauformen (Form Factors)</h3>
|
||||
<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>
|
||||
<!-- Sub-tab: Formfaktoren -->
|
||||
<div id="std-subtab-formfaktoren" class="hidden">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<input type="text" id="ff-search" placeholder="Suche: QSFP28, SFP+, 100G, Aktuell…"
|
||||
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()"
|
||||
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="SFP family">SFP-Familie</option>
|
||||
<option value="QSFP family">QSFP-Familie</option>
|
||||
<option value="OSFP family">OSFP-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="obsolete">Veraltet</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="ff-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,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 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">
|
||||
<span class="loading pulse">Lade Bauformen…</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -4648,22 +4673,46 @@ async function loadStandardsList() {
|
||||
_allStandards = data.data || [];
|
||||
}
|
||||
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() {
|
||||
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 {}; });
|
||||
_allFormFactors = data.data || [];
|
||||
renderFormFactors();
|
||||
}
|
||||
|
||||
function filterFormFactors() {
|
||||
var family = el('ff-family-filter') ? el('ff-family-filter').value : '';
|
||||
var filtered = family
|
||||
? _allFormFactors.filter(function(f) { return (f.family || '') === family; })
|
||||
: _allFormFactors;
|
||||
var q = (el('ff-search') ? el('ff-search').value.toLowerCase() : '');
|
||||
var family = (el('ff-family-filter') ? el('ff-family-filter').value : '');
|
||||
var status = (el('ff-status-filter') ? el('ff-status-filter').value : '');
|
||||
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);
|
||||
}
|
||||
|
||||
@ -4715,7 +4764,86 @@ function renderFormFactors(items) {
|
||||
}).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; });
|
||||
if (!f) return;
|
||||
var fCl = { 'SFP family': '#0ea5e9', 'QSFP family': '#6366f1', 'OSFP family': '#FF8100', 'CFP family': '#2d6a4f', 'legacy': '#888' }[f.family] || '#888';
|
||||
@ -4724,58 +4852,146 @@ function openFormFactorDetail(name) {
|
||||
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 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 = '';
|
||||
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>';
|
||||
h += '<span style="font-size:0.8rem;color:var(--text-dim)">' + esc(f.full_name || '') + '</span>';
|
||||
|
||||
// ── Header ─────────────────────────────────────────────────────────────────
|
||||
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>';
|
||||
// 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) {
|
||||
h += '<div class="card" style="padding:1rem;border-left:3px solid ' + fCl + ';margin-bottom:1rem">';
|
||||
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 class="card" style="padding:0.85rem 1rem;border-left:3px solid ' + fCl + ';margin:0.85rem 0">';
|
||||
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>';
|
||||
if (descEN) {
|
||||
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>';
|
||||
}
|
||||
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>';
|
||||
}
|
||||
// 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 = [
|
||||
['Max. Geschwindigkeit', maxSpd],
|
||||
['Kanäle', f.channels ? f.channels + ' × ' + (f.channel_rate_gbps || '?') + 'G' : '—'],
|
||||
['Familie', f.family || '—'],
|
||||
['Status', statusLabels[f.status] || f.status || '—'],
|
||||
['Hot-swap', f.hot_swap ? 'Ja — im laufenden Betrieb tauschbar' : 'Nein'],
|
||||
['Stecker (typisch)', f.connector_type || '—'],
|
||||
['Auf dem Markt seit', f.year_introduced ? String(f.year_introduced) : '—'],
|
||||
['Ersetzt durch', f.superseded_by || '—'],
|
||||
['Größe (B×T)', (f.physical_width_mm && f.physical_height_mm) ? f.physical_width_mm + 'mm × ' + f.physical_height_mm + 'mm' : '—']
|
||||
];
|
||||
h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;margin-bottom:1rem">';
|
||||
['Max. Speed', maxSpd],
|
||||
['Kanäle', f.channels ? f.channels + ' × ' + (f.channel_rate_gbps || '?') + 'G' : null],
|
||||
['Stecker', f.connector_type || null],
|
||||
['Hot-swap', f.hot_swap ? 'Ja — im Betrieb tauschbar' : 'Nein'],
|
||||
['Auf Markt seit', f.year_introduced ? String(f.year_introduced) : null],
|
||||
['Größe (B×T)', (f.physical_width_mm && f.physical_height_mm) ? f.physical_width_mm + '×' + f.physical_height_mm + 'mm' : null],
|
||||
['Familie', f.family || null],
|
||||
['Ersetzt durch', f.superseded_by || null]
|
||||
].filter(function(sp){ return sp[1]; });
|
||||
h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.4rem;margin-bottom:0.85rem">';
|
||||
specs.forEach(function(sp) {
|
||||
if (!sp[1] || sp[1] === '—') return;
|
||||
h += '<div style="background:var(--surface3);padding:0.5rem 0.75rem;border-radius:8px">'
|
||||
+ '<div style="font-size:0.65rem;color:var(--text-dim);margin-bottom:2px">' + esc(sp[0]) + '</div>'
|
||||
h += '<div style="background:var(--surface3);padding:0.45rem 0.65rem;border-radius:7px">'
|
||||
+ '<div style="font-size:0.62rem;color:var(--text-dim);margin-bottom:1px">' + esc(sp[0]) + '</div>'
|
||||
+ '<div style="font-size:0.78rem;font-weight:600;color:var(--text-bright)">' + esc(String(sp[1])) + '</div>'
|
||||
+ '</div>';
|
||||
});
|
||||
h += '</div>';
|
||||
|
||||
// ── Supersedes chain ───────────────────────────────────────────────────────
|
||||
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="display:flex;gap:0.4rem;flex-wrap:wrap">';
|
||||
supersedes.forEach(function(s) { h += '<span class="b b-blue" style="font-size:0.78rem">' + esc(s) + '</span>'; });
|
||||
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.4rem">Ersetzt:</div>';
|
||||
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>';
|
||||
}
|
||||
if (f.transceiver_count > 0) {
|
||||
h += '<button class="btn" style="background:' + fCl + ';color:#fff;font-size:0.8rem" '
|
||||
|
||||
// ── Use Cases ─────────────────────────────────────────────────────────────
|
||||
if (useCases.length) {
|
||||
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()">'
|
||||
+ 'Transceiver mit ' + esc(f.name) + ' anzeigen →</button>';
|
||||
+ '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) {
|
||||
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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user