Rene Fichtmueller c6308e93c0 feat: massive scraper expansion + hype cycle engine + lifecycle prediction
New scrapers:
- GBICS.com (BigCommerce, GBP prices, 10 categories, 78 products)
- Juniper HCT (Next.js SSR parser, 475 transceivers with specs/EOL)
- SFPcables.com (Magento store, 16 categories, 78 products)
- Fluxlight (BigCommerce, 6 pages, 118 products)
- Champion ONE (compatible vendor scraper)

Scraper fixes:
- 10Gtek: rewritten to parse HTML spec tables (152 products)
- Flexoptix: fix price extraction from Magento Hyva HTML
- Register all scrapers in CLI (--gbics, --juniper, --sfpcables, etc.)

Hype Cycle Engine enhancements:
- Data-driven enrichment from scraped vendor/price data
- Revenue lifecycle prediction (peak year, decline, revenue index)
- Regional adoption model (NA, China, APAC, Europe, RoW with lag coefficients)
- New API endpoints: /enriched, /lifecycle, /regional/:tech

DB growth: 89 → 1,168 transceivers, 0 → 416 prices, 6 vendors
Qdrant: 1,162 products embedded with nomic-embed-text

Research: Norton-Bass model, standards-to-market timelines, hype signals
2026-03-28 02:30:19 +13:00

901 lines
40 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TIP — Transceiver Intelligence Platform</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0c0f16;
--surface: #13161f;
--surface2: #1a1e2a;
--surface3: #222736;
--border: #2a3040;
--border-hover: #3b4560;
--text: #d4d8e3;
--text-bright: #f0f2f5;
--text-dim: #7a829a;
--accent: #4f8ff7;
--accent-dim: rgba(79,143,247,0.12);
--green: #22c55e;
--green-dim: rgba(34,197,94,0.12);
--yellow: #eab308;
--yellow-dim: rgba(234,179,8,0.12);
--red: #ef4444;
--red-dim: rgba(239,68,68,0.12);
--purple: #a78bfa;
--purple-dim: rgba(167,139,250,0.12);
--orange: #f97316;
--orange-dim: rgba(249,115,22,0.12);
--mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
font-size: 14px;
line-height: 1.5;
}
/* HEADER — clean, no gradient nonsense */
.header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 1.5rem;
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left { display: flex; align-items: center; gap: 1.5rem; }
.header h1 {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-bright);
letter-spacing: -0.01em;
}
.header-stats {
display: flex;
gap: 1rem;
font-size: 0.75rem;
font-family: var(--mono);
color: var(--text-dim);
}
.header-stats .val { color: var(--text); font-weight: 500; }
.header .status {
display: flex;
gap: 0.75rem;
align-items: center;
font-size: 0.75rem;
color: var(--text-dim);
}
.dot {
width: 6px; height: 6px; border-radius: 50%;
display: inline-block;
margin-right: 3px;
}
.dot-ok { background: var(--green); }
.dot-err { background: var(--red); }
/* TABS */
.tabs {
display: flex;
gap: 0;
border-bottom: 1px solid var(--border);
padding: 0 1.5rem;
background: var(--surface);
}
.tab {
padding: 0.6rem 1rem;
cursor: pointer;
border-bottom: 2px solid transparent;
color: var(--text-dim);
font-size: 0.8rem;
font-weight: 500;
transition: color 0.15s;
}
.tab:hover { color: var(--text); }
.tab.active { color: var(--text-bright); border-bottom-color: var(--accent); }
/* LAYOUT */
.main { padding: 1.25rem 1.5rem; max-width: 1600px; margin: 0 auto; }
.grid { display: grid; gap: 0.75rem; }
.g4 { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.g3 { grid-template-columns: repeat(3, 1fr); }
.g2 { grid-template-columns: 1fr 1fr; }
.g2-1 { grid-template-columns: 2fr 1fr; }
/* CARDS */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem;
}
.card-sm { padding: 0.75rem; }
.card-label {
font-size: 0.7rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-dim);
margin-bottom: 0.25rem;
}
.card-num {
font-size: 1.5rem;
font-weight: 700;
font-family: var(--mono);
color: var(--text-bright);
letter-spacing: -0.02em;
}
.card-num small {
font-size: 0.7rem;
font-weight: 400;
color: var(--text-dim);
margin-left: 4px;
}
/* BADGES */
.b { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 0.7rem; font-weight: 600; font-family: var(--mono); }
.b-blue { background: var(--accent-dim); color: var(--accent); }
.b-green { background: var(--green-dim); color: var(--green); }
.b-yellow { background: var(--yellow-dim); color: var(--yellow); }
.b-red { background: var(--red-dim); color: var(--red); }
.b-purple { background: var(--purple-dim); color: var(--purple); }
.b-orange { background: var(--orange-dim); color: var(--orange); }
.b-neutral { background: var(--surface3); color: var(--text-dim); }
/* TABLES */
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
th {
text-align: left; padding: 0.5rem 0.6rem;
border-bottom: 1px solid var(--border);
color: var(--text-dim); font-weight: 500;
font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
position: sticky; top: 0; background: var(--surface); z-index: 1;
}
td { padding: 0.45rem 0.6rem; border-bottom: 1px solid rgba(42,48,64,0.5); }
tr.clickable { cursor: pointer; }
tr.clickable:hover td { background: var(--accent-dim); }
.table-wrap { max-height: 70vh; overflow-y: auto; }
/* SEARCH */
.search-row {
display: flex; gap: 0.5rem; margin-bottom: 1rem;
}
.search-row input, .search-row select {
background: var(--surface2); border: 1px solid var(--border);
border-radius: 4px; padding: 0.5rem 0.75rem;
color: var(--text); font-size: 0.85rem;
}
.search-row input { flex: 1; }
.search-row input:focus { outline: none; border-color: var(--accent); }
.search-row select { min-width: 120px; }
.btn {
background: var(--accent); color: #fff; border: none;
border-radius: 4px; padding: 0.5rem 1rem;
font-weight: 600; font-size: 0.8rem; cursor: pointer;
}
.btn:hover { opacity: 0.9; }
.btn-ghost {
background: transparent; border: 1px solid var(--border);
color: var(--text); cursor: pointer; border-radius: 4px;
padding: 0.4rem 0.75rem; font-size: 0.75rem; font-weight: 500;
}
.btn-ghost:hover { border-color: var(--border-hover); background: var(--surface2); }
/* RESULT ITEMS */
.ri {
border-bottom: 1px solid var(--border);
padding: 0.75rem 0;
cursor: pointer;
}
.ri:last-child { border-bottom: none; }
.ri:hover { background: var(--accent-dim); margin: 0 -1rem; padding: 0.75rem 1rem; }
.ri-title { font-weight: 500; font-size: 0.85rem; color: var(--text-bright); }
.ri-body { font-size: 0.8rem; color: var(--text-dim); margin-top: 0.25rem; }
.ri-meta { font-size: 0.7rem; color: var(--text-dim); margin-top: 0.25rem; display: flex; gap: 0.5rem; align-items: center; }
/* HYPE CYCLE */
.hype-svg-wrap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem 1.5rem;
overflow-x: auto;
}
.hype-svg-wrap svg { display: block; margin: 0 auto; }
.hype-dot { cursor: pointer; transition: filter 0.15s; }
.hype-dot:hover { filter: brightness(1.4); }
.hype-label { font-size: 10px; fill: var(--text); pointer-events: none; font-family: 'Inter', sans-serif; }
.hype-phase-label { font-size: 9px; fill: var(--text-dim); text-anchor: middle; font-family: 'Inter', sans-serif; }
.hype-bar { height: 6px; background: var(--surface3); border-radius: 3px; overflow: hidden; width: 100%; }
.hype-fill { height: 100%; border-radius: 3px; transition: width 0.5s; }
/* TOOLTIPS */
.tip { position: relative; cursor: help; }
.tip::after {
content: attr(data-tip);
position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
background: #1a1a2e; color: #e0e0e0; border: 1px solid var(--border);
padding: 0.5rem 0.75rem; border-radius: 6px;
font-size: 0.72rem; line-height: 1.4; font-weight: 400; white-space: normal; width: max-content; max-width: 280px;
opacity: 0; pointer-events: none; transition: opacity 0.2s;
z-index: 500; box-shadow: 0 4px 16px rgba(0,0,0,0.5);
}
.tip::before {
content: ''; position: absolute; bottom: calc(100% + 3px); left: 50%; transform: translateX(-50%);
border: 5px solid transparent; border-top-color: var(--border);
opacity: 0; pointer-events: none; transition: opacity 0.2s; z-index: 501;
}
.tip:hover::after, .tip:hover::before { opacity: 1; }
th.tip::after { left: 0; transform: none; }
/* DETAIL PANEL (shared) */
.panel {
position: fixed; top: 0; right: 0;
width: 480px; height: 100vh;
background: var(--surface); border-left: 1px solid var(--border);
box-shadow: -4px 0 24px rgba(0,0,0,0.4);
z-index: 1000; overflow-y: auto;
transform: translateX(100%); transition: transform 0.25s ease;
padding: 1.25rem;
}
.panel.open { transform: translateX(0); }
.panel-close {
position: absolute; top: 0.75rem; right: 0.75rem;
background: var(--surface2); border: 1px solid var(--border);
color: var(--text-dim); width: 28px; height: 28px; border-radius: 4px;
cursor: pointer; font-size: 1rem;
display: flex; align-items: center; justify-content: center;
}
.panel-close:hover { background: var(--border); color: var(--text); }
.panel-title { font-size: 1.1rem; font-weight: 700; color: var(--text-bright); margin-bottom: 0.15rem; }
.panel-sub { font-size: 0.8rem; color: var(--text-dim); margin-bottom: 1rem; }
.panel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 1rem; }
.panel-stat {
background: var(--surface2); border-radius: 4px; padding: 0.6rem;
}
.panel-stat-label { font-size: 0.65rem; text-transform: uppercase; color: var(--text-dim); letter-spacing: 0.04em; }
.panel-stat-val { font-size: 1.2rem; font-weight: 700; font-family: var(--mono); margin-top: 0.15rem; color: var(--text-bright); }
.panel-section { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); margin: 1rem 0 0.5rem; }
.panel-row {
display: flex; justify-content: space-between; align-items: center;
padding: 0.35rem 0; border-bottom: 1px solid rgba(42,48,64,0.5);
font-size: 0.8rem;
}
.panel-row:last-child { border-bottom: none; }
.panel-row-label { color: var(--text-dim); }
.panel-row-val { font-weight: 500; font-family: var(--mono); color: var(--text); }
.forecast-bar {
display: flex; align-items: center; gap: 0.5rem;
margin-bottom: 0.3rem; font-size: 0.8rem;
}
.forecast-bar .yr { width: 36px; color: var(--text-dim); font-family: var(--mono); font-size: 0.75rem; }
.forecast-bar .track { flex: 1; height: 5px; background: var(--surface3); border-radius: 3px; overflow: hidden; }
.forecast-bar .fill { height: 100%; border-radius: 3px; }
.forecast-bar .pct { width: 40px; text-align: right; font-family: var(--mono); font-size: 0.75rem; color: var(--text-dim); }
/* BLOG */
.gen-card {
background: var(--surface2); border: 1px solid var(--border);
border-radius: 6px; padding: 0.75rem; cursor: pointer;
transition: border-color 0.15s;
}
.gen-card:hover { border-color: var(--accent); }
.gen-card-title { font-weight: 600; font-size: 0.85rem; color: var(--text-bright); }
.gen-card-sub { font-size: 0.75rem; color: var(--text-dim); margin-top: 0.2rem; }
/* UTILITIES */
.hidden { display: none !important; }
.loading { text-align: center; padding: 2rem; color: var(--text-dim); font-size: 0.85rem; }
.mono { font-family: var(--mono); }
.dim { color: var(--text-dim); }
.mt { margin-top: 0.75rem; }
.mb { margin-bottom: 0.75rem; }
/* TOAST */
.toast {
position: fixed; top: 1rem; right: 1rem; z-index: 9999;
background: var(--surface); border: 1px solid var(--green);
border-radius: 6px; padding: 0.75rem 1.25rem; max-width: 380px;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
transform: translateX(120%); transition: transform 0.25s ease;
}
.toast.show { transform: translateX(0); }
.toast.error { border-color: var(--red); }
.toast-title { font-weight: 600; font-size: 0.8rem; color: var(--text-bright); }
.toast-body { font-size: 0.75rem; color: var(--text-dim); margin-top: 0.1rem; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.5} }
.pulse { animation: pulse 2s infinite; }
@media (max-width: 900px) {
.g2, .g2-1, .g3 { grid-template-columns: 1fr; }
.panel { width: 100%; }
.header-stats { display: none; }
}
</style>
</head>
<body>
<div id="toast" class="toast"><div class="toast-title"></div><div class="toast-body"></div></div>
<!-- HEADER -->
<div class="header">
<div class="header-left">
<h1>TIP</h1>
<div class="header-stats">
<span><span class="val" id="stat-transceivers"></span> transceivers</span>
<span><span class="val" id="stat-vendors"></span> vendors</span>
<span><span class="val" id="stat-standards"></span> standards</span>
<span><span class="val" id="stat-news"></span> news</span>
</div>
</div>
<div class="status">
<span><span class="dot dot-ok" id="api-status"></span>API</span>
<span><span class="dot dot-ok" id="db-status"></span>DB</span>
<span><span class="dot dot-ok" id="qdrant-status"></span>Qdrant</span>
<span class="mono dim" id="version-label"></span>
</div>
</div>
<!-- TABS -->
<div class="tabs">
<div class="tab active" data-tab="overview">Overview</div>
<div class="tab" data-tab="search">Search</div>
<div class="tab" data-tab="hype">Hype Cycle</div>
<div class="tab" data-tab="transceivers">Transceivers</div>
<div class="tab" data-tab="news">News</div>
<div class="tab" data-tab="blog">Blog Engine</div>
</div>
<div class="main">
<!-- OVERVIEW -->
<div id="tab-overview">
<div class="grid g2 mb">
<div class="card">
<div class="card-label">Vector Collections</div>
<div id="collections-list" class="mt" style="font-size:0.8rem"></div>
</div>
<div class="card">
<div class="card-label">Recent News</div>
<div id="recent-news" class="mt"></div>
</div>
</div>
<div class="card">
<div class="card-label">API Endpoints</div>
<div id="endpoints-list" class="mt" style="font-family:var(--mono);font-size:0.75rem;color:var(--text-dim);line-height:1.7;columns:2"></div>
</div>
</div>
<!-- SEARCH -->
<div id="tab-search" class="hidden">
<div class="search-row">
<input type="text" id="search-input" placeholder="Search transceivers, datasheets, FAQ, troubleshooting...">
<select id="search-collection">
<option value="product_embeddings">Products</option>
<option value="faq_embeddings">FAQ</option>
<option value="troubleshooting_embeddings">Troubleshooting</option>
<option value="datasheet_chunks">Datasheets</option>
<option value="news_embeddings">News</option>
</select>
<button class="btn" id="search-btn">Search</button>
</div>
<div class="card"><div id="search-results"></div></div>
</div>
<!-- HYPE CYCLE -->
<div id="tab-hype" class="hidden">
<div class="hype-svg-wrap">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem">
<div>
<span style="font-weight:600;font-size:0.9rem;color:var(--text-bright)">Optical Transceiver Hype Cycle</span>
<span class="mono dim" style="margin-left:0.5rem" id="hype-year">2026</span>
<div style="font-size:0.7rem;color:var(--text-dim);margin-top:0.15rem">Norton-Bass Model &mdash; click any technology for details</div>
</div>
<div style="display:flex;gap:0.75rem;font-size:0.65rem;color:var(--text-dim)">
<span><span class="dot dot-ok" style="background:#4f8ff7"></span>Innovation</span>
<span><span class="dot" style="background:#eab308"></span>Peak</span>
<span><span class="dot" style="background:#ef4444"></span>Trough</span>
<span><span class="dot" style="background:#a78bfa"></span>Slope</span>
<span><span class="dot" style="background:#22c55e"></span>Plateau</span>
</div>
</div>
<div id="hype-svg-container"></div>
</div>
<div class="card mt">
<div class="table-wrap">
<table>
<thead><tr><th>Technology</th><th class="tip" data-tip="Current phase in the Gartner-style technology adoption lifecycle. Technologies move from initial hype through disillusionment to mature productivity.">Phase</th><th class="tip" data-tip="Position on the hype curve (0-100%). 0% = just emerging, 50% = mid-cycle, 100% = fully mature.">Position</th><th class="tip" data-tip="Market adoption rate as percentage of total addressable market. Based on Norton-Bass diffusion model forecast.">Adoption</th><th class="tip" data-tip="Estimated year when this technology reaches peak hype / maximum media attention before reality sets in.">Peak</th><th class="tip" data-tip="Estimated years until this technology reaches the Plateau of Productivity — stable, mainstream deployment.">To Plateau</th></tr></thead>
<tbody id="hype-table"></tbody>
</table>
</div>
</div>
</div>
<!-- TRANSCEIVERS -->
<div id="tab-transceivers" class="hidden">
<div class="search-row">
<input type="text" id="tx-search" placeholder="Filter: 100G LR4, QSFP28, coherent, SMF...">
<select id="tx-ff-filter">
<option value="">All Form Factors</option>
<option value="SFP">SFP</option>
<option value="SFP+">SFP+</option>
<option value="SFP28">SFP28</option>
<option value="QSFP+">QSFP+</option>
<option value="QSFP28">QSFP28</option>
<option value="QSFP-DD">QSFP-DD</option>
<option value="OSFP">OSFP</option>
<option value="CFP2">CFP2</option>
</select>
<button class="btn" id="tx-search-btn">Search</button>
</div>
<div class="card">
<div class="table-wrap">
<table>
<thead><tr><th>Name</th><th>Form Factor</th><th>Speed</th><th>Reach</th><th>Fiber</th><th>Connector</th><th>WDM</th><th>Category</th></tr></thead>
<tbody id="tx-table"></tbody>
</table>
</div>
</div>
</div>
<!-- NEWS -->
<div id="tab-news" class="hidden">
<div class="card">
<div id="news-list"></div>
</div>
</div>
<!-- BLOG -->
<div id="tab-blog" class="hidden">
<div class="grid g3 mb">
<div class="gen-card" id="gen-hype">
<div class="gen-card-title">Hype Cycle Analysis</div>
<div class="gen-card-sub">800G technology position article</div>
</div>
<div class="gen-card" id="gen-comparison">
<div class="gen-card-title">Product Comparison</div>
<div class="gen-card-sub">400G transceiver comparison</div>
</div>
<div class="gen-card" id="gen-tutorial">
<div class="gen-card-title">Tutorial</div>
<div class="gen-card-sub">Transceiver troubleshooting guide</div>
</div>
</div>
<div class="card"><div id="blog-list"></div></div>
</div>
</div>
<!-- DETAIL PANEL (shared for hype cycle + transceivers) -->
<div id="detail-panel" class="panel">
<button class="panel-close" id="panel-close">&times;</button>
<div id="panel-content"></div>
</div>
<script>
var API = window.location.hostname === 'localhost' ? 'http://localhost:3201' : window.location.origin;
function esc(str) {
if (str == null) return '';
var d = document.createElement('div');
d.textContent = String(str);
return d.innerHTML;
}
function el(id) { return document.getElementById(id); }
function api(path) { return fetch(API + path).then(function(r) { return r.json(); }); }
function showToast(title, body, isError) {
var t = el('toast');
t.querySelector('.toast-title').textContent = title;
t.querySelector('.toast-body').textContent = body;
t.className = 'toast' + (isError ? ' error' : '');
void t.offsetWidth;
t.classList.add('show');
clearTimeout(showToast._timer);
showToast._timer = setTimeout(function() { t.classList.remove('show'); }, 4000);
}
function buildDOM(parent, html) {
parent.textContent = '';
var t = document.createElement('template');
t.innerHTML = html;
parent.appendChild(t.content.cloneNode(true));
}
function openPanel(html) {
var p = el('detail-panel');
buildDOM(el('panel-content'), html);
p.classList.add('open');
}
function closePanel() { el('detail-panel').classList.remove('open'); }
el('panel-close').addEventListener('click', closePanel);
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closePanel(); });
// TABS
document.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
document.querySelectorAll('[id^="tab-"]').forEach(function(p) { p.classList.add('hidden'); });
el('tab-' + tab.dataset.tab).classList.remove('hidden');
if (tab.dataset.tab === 'hype') loadHypeCycle();
if (tab.dataset.tab === 'transceivers') searchTransceivers();
if (tab.dataset.tab === 'news') loadNews();
if (tab.dataset.tab === 'blog') loadBlogDrafts();
});
});
// OVERVIEW
async function loadOverview() {
try {
var h = await api('/api/health');
el('stat-transceivers').textContent = h.database.stats.transceiver_count;
el('stat-vendors').textContent = h.database.stats.vendor_count;
el('stat-standards').textContent = h.database.stats.standard_count;
el('stat-news').textContent = h.database.stats.news_count;
el('version-label').textContent = 'v' + h.version;
el('api-status').className = 'dot ' + (h.success ? 'dot-ok' : 'dot-err');
el('db-status').className = 'dot ' + (h.database.connected ? 'dot-ok' : 'dot-err');
} catch(e) { el('api-status').className = 'dot dot-err'; }
try {
var stats = await api('/api/search/stats');
buildDOM(el('collections-list'), stats.collections.map(function(c) {
return '<div style="display:flex;justify-content:space-between;padding:0.35rem 0;border-bottom:1px solid var(--border)">'
+ '<span class="mono" style="font-size:0.75rem">' + esc(c.collection) + '</span>'
+ '<span class="b ' + (c.pointsCount > 0 ? 'b-green' : 'b-yellow') + '">' + esc(c.pointsCount) + '</span>'
+ '</div>';
}).join(''));
el('qdrant-status').className = 'dot dot-ok';
} catch(e) { el('qdrant-status').className = 'dot dot-err'; }
try {
var root = await api('/');
buildDOM(el('endpoints-list'), (root.endpoints || []).map(function(e) {
return '<div style="padding:0.1rem 0">' + esc(e) + '</div>';
}).join(''));
} catch(e) {}
try {
var news = await api('/api/search?q=transceiver+optics+data+center&collection=news_embeddings&limit=5');
buildDOM(el('recent-news'), (news.results || []).map(function(n) {
return '<div class="ri">'
+ '<div class="ri-title">' + esc(n.title) + '</div>'
+ '<div class="ri-meta"><span class="b b-blue">' + esc(n.source) + '</span> ' + (n.published_at ? new Date(n.published_at).toLocaleDateString() : '') + '</div>'
+ '</div>';
}).join('') || '<div class="loading">No news</div>');
} catch(e) {}
}
// SEARCH
function doSearch() {
var q = el('search-input').value;
var col = el('search-collection').value;
if (!q) return;
el('search-results').textContent = 'Searching...';
api('/api/search?q=' + encodeURIComponent(q) + '&collection=' + col + '&limit=15').then(function(data) {
buildDOM(el('search-results'), (data.results || []).map(function(r) {
var title = r.standard_name || r.title || r.question || r.symptom || (r.text ? r.text.slice(0,80) : 'Result');
var body = r.answer || r.solution || r.summary || (r.text ? r.text.slice(0,300) : '');
return '<div class="ri">'
+ '<div style="display:flex;justify-content:space-between;align-items:center">'
+ '<div class="ri-title">' + esc(title) + '</div>'
+ '<span class="mono dim" style="font-size:0.7rem">' + (r.score * 100).toFixed(1) + '%</span>'
+ '</div>'
+ '<div class="ri-body">' + esc(body) + '</div>'
+ '<div class="ri-meta">'
+ (r.form_factor ? '<span class="b b-blue">' + esc(r.form_factor) + '</span>' : '')
+ (r.speed ? '<span class="b b-purple">' + esc(r.speed) + '</span>' : '')
+ (r.category ? '<span class="b b-yellow">' + esc(r.category) + '</span>' : '')
+ (r.severity ? '<span class="b b-red">' + esc(r.severity) + '</span>' : '')
+ (r.vendor ? '<span class="dim">' + esc(r.vendor) + '</span>' : '')
+ '</div></div>';
}).join('') || '<div class="loading">No results</div>');
});
}
el('search-btn').addEventListener('click', doSearch);
el('search-input').addEventListener('keydown', function(e) { if (e.key === 'Enter') doSearch(); });
// HYPE CYCLE
var PC = {
'Innovation Trigger': '#4f8ff7',
'Peak of Inflated Expectations': '#eab308',
'Trough of Disillusionment': '#ef4444',
'Slope of Enlightenment': '#a78bfa',
'Plateau of Productivity': '#22c55e'
};
var PHASE_MAP = {
'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'
};
var PHASE_DESC = {
'Innovation Trigger': 'Early-stage technology breakthrough. First proof-of-concept demos, limited vendor support. High risk, high potential. Expect interop issues and premium pricing.',
'Peak of Inflated Expectations': 'Maximum hype and media attention. Vendors announce products, but real-world deployments are rare. Expectations exceed what the technology can deliver today.',
'Trough of Disillusionment': 'Reality check. Early deployments reveal limitations — interop failures, cost overruns, performance gaps. Interest wanes. Only committed adopters remain.',
'Slope of Enlightenment': 'Practical benefits become clear. Second and third-generation products fix early issues. Multi-vendor support grows. Best practices emerge from real deployments.',
'Plateau of Productivity': 'Mainstream adoption. Stable pricing, broad vendor support, proven reliability. The technology is a standard part of network infrastructure.'
};
function curveY(x, w, h) {
var t = x / w;
if (t < 0.15) return h - (t / 0.15) * h * 0.85;
if (t < 0.22) return h * 0.15 + ((t - 0.15) / 0.07) * h * 0.02;
if (t < 0.42) return h * 0.17 + ((t - 0.22) / 0.20) * h * 0.55;
if (t < 0.48) return h * 0.72 - ((t - 0.42) / 0.06) * h * 0.02;
if (t < 0.80) return h * 0.70 - ((t - 0.48) / 0.32) * h * 0.35;
return h * 0.35 - ((t - 0.80) / 0.20) * h * 0.02;
}
function renderHypeSvg(techs) {
var W = 1000, H = 300, P = 35;
var cw = W - P * 2, ch = H - P * 2;
var pts = [];
for (var i = 0; i <= cw; i += 2) pts.push((i + P) + ',' + (curveY(i, cw, ch) + P));
var svg = '<svg width="' + W + '" height="' + (H + 40) + '" viewBox="0 0 ' + W + ' ' + (H + 40) + '" xmlns="http://www.w3.org/2000/svg">';
var rb = [0, 0.15, 0.28, 0.50, 0.78, 1.0];
var rc = ['#4f8ff7', '#eab308', '#ef4444', '#a78bfa', '#22c55e'];
for (var r = 0; r < 5; r++) {
svg += '<rect x="' + (rb[r]*cw+P) + '" y="' + P + '" width="' + ((rb[r+1]-rb[r])*cw) + '" height="' + ch + '" fill="' + rc[r] + '" opacity="0.03" />';
}
var pl = [
{l:'Innovation\\nTrigger',x:0.07,k:'Innovation Trigger'},
{l:'Peak of Inflated\\nExpectations',x:0.18,k:'Peak of Inflated Expectations'},
{l:'Trough of\\nDisillusionment',x:0.42,k:'Trough of Disillusionment'},
{l:'Slope of\\nEnlightenment',x:0.64,k:'Slope of Enlightenment'},
{l:'Plateau of\\nProductivity',x:0.90,k:'Plateau of Productivity'}
];
for (var p = 0; p < pl.length; p++) {
var px = pl[p].x * cw + P;
var ll = pl[p].l.split('\\n');
var phaseW = (rb[p+1]-rb[p])*cw;
svg += '<g style="cursor:help"><title>' + esc(PHASE_DESC[pl[p].k] || '') + '</title>';
svg += '<rect x="' + (rb[p]*cw+P) + '" y="' + (H+2) + '" width="' + phaseW + '" height="28" fill="transparent" />';
for (var li = 0; li < ll.length; li++) {
svg += '<text x="' + px + '" y="' + (H + 12 + li * 12) + '" class="hype-phase-label">' + esc(ll[li]) + '</text>';
}
svg += '</g>';
}
svg += '<polyline points="' + pts.join(' ') + '" fill="none" stroke="#333b4d" stroke-width="2" />';
var placed = [];
for (var ti = 0; ti < techs.length; ti++) {
var t = techs[ti], color = PC[t.phase] || '#6b7280';
var tx = (t.positionPct / 100) * cw, ty = curveY(tx, cw, ch);
tx += P; ty += P;
var ly = ty - 12, lx = tx + 7, anc = 'start';
for (var pi = 0; pi < placed.length; pi++) {
if (Math.abs(placed[pi].x - tx) < 45 && Math.abs(placed[pi].y - ly) < 14) ly = placed[pi].y + 14;
}
if (tx > W - 150) { lx = tx - 7; anc = 'end'; }
placed.push({x:tx, y:ly});
svg += '<circle cx="' + tx + '" cy="' + ty + '" r="6" fill="' + color + '" class="hype-dot" data-tech="' + esc(t.technology) + '" />';
svg += '<text x="' + lx + '" y="' + (ly + 3) + '" class="hype-label" text-anchor="' + anc + '" font-weight="600">' + esc(t.technology) + '</text>';
}
svg += '</svg>';
return svg;
}
async function openHypeDetail(name) {
openPanel('<div class="loading pulse">Loading...</div>');
try {
var d = await api('/api/hype-cycle/' + encodeURIComponent(name));
var c = PC[d.phaseLabel] || '#6b7280';
var h = '<div class="panel-title">' + esc(d.technology) + '</div>';
h += '<div class="panel-sub"><span class="b" style="background:' + c + '22;color:' + c + '">' + esc(d.phaseLabel) + '</span></div>';
h += '<div class="panel-grid">';
h += '<div class="panel-stat"><div class="panel-stat-label">Adoption</div><div class="panel-stat-val" style="color:' + c + '">' + (d.adoptionPct != null ? (d.adoptionPct*100).toFixed(0) + '%' : '—') + '</div></div>';
h += '<div class="panel-stat"><div class="panel-stat-label">Position</div><div class="panel-stat-val">' + (d.positionPct||0) + '%</div></div>';
h += '<div class="panel-stat"><div class="panel-stat-label">Peak Year</div><div class="panel-stat-val">' + esc(d.forecast && d.forecast.peakShipmentYear || '—') + '</div></div>';
h += '<div class="panel-stat"><div class="panel-stat-label">To Plateau</div><div class="panel-stat-val">' + (d.forecast && d.forecast.yearsToPlateauFromNow != null ? d.forecast.yearsToPlateauFromNow + 'y' : '—') + '</div></div>';
h += '</div>';
if (d.forecast && d.forecast.fiveYearProjection && d.forecast.fiveYearProjection.length) {
h += '<div class="panel-section">5-Year Forecast</div>';
for (var i = 0; i < d.forecast.fiveYearProjection.length; i++) {
var f = d.forecast.fiveYearProjection[i];
var fc = PC[PHASE_MAP[f.phase] || ''] || '#6b7280';
h += '<div class="forecast-bar"><span class="yr">' + f.year + '</span><div class="track"><div class="fill" style="width:' + (f.adoptionPct||0) + '%;background:' + fc + '"></div></div><span class="pct">' + (f.adoptionPct||0) + '%</span></div>';
}
}
h += '<div class="panel-section">Composite Score</div>';
h += '<div style="padding:0.6rem;background:var(--surface2);border-radius:4px">';
h += '<span class="mono" style="font-size:1.5rem;font-weight:700;color:' + c + '">' + (d.compositeScore||0) + '</span><span class="dim" style="font-size:0.8rem"> / 100</span>';
h += '</div>';
buildDOM(el('panel-content'), h);
} catch(e) { el('panel-content').textContent = 'Error: ' + e.message; }
}
async function loadHypeCycle() {
var data = await api('/api/hype-cycle');
var techs = data.technologies || [];
el('hype-year').textContent = data.year;
var c = el('hype-svg-container');
buildDOM(c, renderHypeSvg(techs));
c.querySelectorAll('.hype-dot').forEach(function(dot) {
dot.addEventListener('click', function() { openHypeDetail(this.getAttribute('data-tech')); });
});
buildDOM(el('hype-table'), techs.map(function(t) {
var color = PC[t.phase] || '#374151';
return '<tr class="clickable" data-tech="' + esc(t.technology) + '">'
+ '<td style="font-weight:500">' + esc(t.technology) + '</td>'
+ '<td><span class="b tip" data-tip="' + esc(PHASE_DESC[t.phase] || '') + '" style="background:' + color + '22;color:' + color + '">' + esc(t.phase) + '</span></td>'
+ '<td><div class="hype-bar"><div class="hype-fill" style="width:' + t.positionPct + '%;background:' + color + '"></div></div></td>'
+ '<td class="mono tip" data-tip="' + esc(t.technology) + ' has reached ' + (t.adoptionPct * 100).toFixed(0) + '% of its total addressable market based on Norton-Bass diffusion modeling.">' + (t.adoptionPct * 100).toFixed(0) + '%</td>'
+ '<td class="mono tip" data-tip="' + (t.peakYear ? 'Peak hype expected around ' + t.peakYear + '. After this, expect a reality correction before stable adoption.' : 'Peak year not yet determined.') + '">' + esc(t.peakYear || '—') + '</td>'
+ '<td class="mono tip" data-tip="' + (t.yearsToPlateauFromNow != null ? 'Estimated ' + t.yearsToPlateauFromNow + ' years until ' + esc(t.technology) + ' reaches mainstream, stable deployment.' : 'Already at plateau or timeline unknown.') + '">' + (t.yearsToPlateauFromNow != null ? t.yearsToPlateauFromNow + 'y' : '—') + '</td>'
+ '</tr>';
}).join(''));
el('hype-table').querySelectorAll('tr.clickable').forEach(function(row) {
row.addEventListener('click', function() { openHypeDetail(this.getAttribute('data-tech')); });
});
}
// TRANSCEIVERS — with click-through detail
function searchTransceivers() {
var q = el('tx-search').value;
var ff = el('tx-ff-filter').value;
var params = [];
if (q) params.push('q=' + encodeURIComponent(q));
if (ff) params.push('form_factor=' + encodeURIComponent(ff));
params.push('limit=100');
var url = '/api/transceivers?' + params.join('&');
api(url).then(function(data) {
buildDOM(el('tx-table'), (data.transceivers || []).map(function(t) {
return '<tr class="clickable" data-txid="' + esc(t.id) + '">'
+ '<td style="font-weight:500;color:var(--text-bright)">' + esc(t.standard_name || t.slug) + '</td>'
+ '<td><span class="b b-blue">' + esc(t.form_factor) + '</span></td>'
+ '<td class="mono">' + esc(t.speed) + '</td>'
+ '<td>' + esc(t.reach_label) + '</td>'
+ '<td>' + esc(t.fiber_type) + '</td>'
+ '<td>' + esc(t.connector) + '</td>'
+ '<td>' + esc(t.wdm_type || '—') + '</td>'
+ '<td>' + (t.category ? '<span class="b b-neutral">' + esc(t.category) + '</span>' : '') + '</td>'
+ '</tr>';
}).join(''));
el('tx-table').querySelectorAll('tr.clickable').forEach(function(row) {
row.addEventListener('click', function() { openTxDetail(this.getAttribute('data-txid')); });
});
});
}
async function openTxDetail(id) {
openPanel('<div class="loading pulse">Loading...</div>');
try {
var data = await api('/api/transceivers/' + id);
var t = data.transceiver || data;
var h = '<div class="panel-title">' + esc(t.standard_name || t.slug) + '</div>';
h += '<div class="panel-sub">' + esc(t.description || '') + '</div>';
h += '<div class="panel-section">Specifications</div>';
var specs = [
['Form Factor', t.form_factor],
['Speed', t.speed],
['Speed (Gbps)', t.speed_gbps],
['Reach', t.reach_label],
['Reach (km)', t.reach_km],
['Fiber Type', t.fiber_type],
['Connector', t.connector],
['WDM Type', t.wdm_type],
['Wavelength', t.wavelength_nm ? t.wavelength_nm + ' nm' : null],
['Lanes', t.lanes],
['Category', t.category],
['Coherent', t.is_coherent ? 'Yes' : 'No'],
['Bidirectional', t.is_bidirectional ? 'Yes' : 'No'],
['Temperature', t.temperature_range],
['Power (W)', t.power_max_w],
['Standard', t.standard_name || t.standard_id],
];
for (var s = 0; s < specs.length; s++) {
if (specs[s][1] != null && specs[s][1] !== '' && specs[s][1] !== false) {
h += '<div class="panel-row"><span class="panel-row-label">' + esc(specs[s][0]) + '</span><span class="panel-row-val">' + esc(specs[s][1]) + '</span></div>';
}
}
buildDOM(el('panel-content'), h);
} catch(e) { el('panel-content').textContent = 'Error: ' + e.message; }
}
el('tx-search-btn').addEventListener('click', searchTransceivers);
el('tx-search').addEventListener('keydown', function(e) { if (e.key === 'Enter') searchTransceivers(); });
el('tx-ff-filter').addEventListener('change', searchTransceivers);
// NEWS
async function loadNews() {
var data = await api('/api/search?q=optical+transceiver+data+center+networking&collection=news_embeddings&limit=25');
buildDOM(el('news-list'), (data.results || []).map(function(n) {
var urlSafe = (n.url && /^https?:\/\//.test(n.url)) ? n.url : '#';
return '<div class="ri">'
+ '<div class="ri-title">' + esc(n.title) + '</div>'
+ '<div class="ri-body">' + esc(n.summary) + '</div>'
+ '<div class="ri-meta">'
+ '<span class="b b-blue">' + esc(n.source) + '</span>'
+ (n.published_at ? '<span>' + new Date(n.published_at).toLocaleDateString() + '</span>' : '')
+ (urlSafe !== '#' ? '<a href="' + esc(urlSafe) + '" target="_blank" rel="noopener noreferrer" style="color:var(--accent);text-decoration:none;font-size:0.7rem">Read &rarr;</a>' : '')
+ '</div></div>';
}).join('') || '<div class="loading">No news</div>');
}
// BLOG
function generateBlog(topic, speed) {
el('blog-list').textContent = 'Generating...';
var body = { topic: topic };
if (speed) body.speed = speed;
fetch(API + '/api/blog/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.success) showToast('Draft generated', data.draft.title + ' — ' + data.draft.word_count + ' words');
else showToast('Failed', data.error || 'Unknown error', true);
loadBlogDrafts();
}).catch(function(err) { showToast('Network error', err.message, true); });
}
el('gen-hype').addEventListener('click', function() { generateBlog('hype_cycle', '800G'); });
el('gen-comparison').addEventListener('click', function() { generateBlog('comparison', '400G'); });
el('gen-tutorial').addEventListener('click', function() { generateBlog('tutorial'); });
async function loadBlogDrafts() {
var data = await api('/api/blog');
buildDOM(el('blog-list'), (data.drafts || []).map(function(d) {
var sc = d.status === 'published' ? 'b-green' : d.status === 'review' ? 'b-yellow' : 'b-blue';
return '<div class="ri" onclick="openBlogDetail(\'' + esc(d.id) + '\')">'
+ '<div style="display:flex;justify-content:space-between;align-items:center">'
+ '<div class="ri-title">' + esc(d.title) + '</div>'
+ '<span class="b ' + sc + '">' + esc(d.status) + '</span>'
+ '</div>'
+ '<div class="ri-meta">'
+ '<span class="b b-purple">' + esc(d.topic) + '</span>'
+ '<span class="b b-neutral">' + esc(d.target_audience) + '</span>'
+ '<span class="mono">' + esc(d.word_count) + ' words</span>'
+ '<span>' + new Date(d.created_at).toLocaleDateString() + '</span>'
+ '</div></div>';
}).join('') || '<div class="loading">No drafts yet</div>');
}
async function openBlogDetail(id) {
openPanel('<div class="loading pulse">Loading...</div>');
try {
var data = await api('/api/blog/' + id);
var d = data.draft;
var h = '<div class="panel-title">' + esc(d.title) + '</div>';
h += '<div class="panel-sub"><span class="b b-purple">' + esc(d.topic) + '</span> <span class="b b-neutral">' + esc(d.target_audience) + '</span> <span class="mono dim">' + esc(d.word_count) + ' words</span></div>';
h += '<div class="panel-section">Content</div>';
h += '<div style="font-size:0.8rem;color:var(--text);white-space:pre-wrap;line-height:1.6;max-height:60vh;overflow-y:auto;background:var(--surface2);padding:0.75rem;border-radius:4px">' + esc(d.draft_content) + '</div>';
h += '<div class="panel-section">SEO Keywords</div>';
h += '<div style="display:flex;gap:0.25rem;flex-wrap:wrap">' + (d.seo_keywords || []).map(function(k) { return '<span class="b b-neutral">' + esc(k) + '</span>'; }).join('') + '</div>';
buildDOM(el('panel-content'), h);
} catch(e) { el('panel-content').textContent = 'Error: ' + e.message; }
}
// INIT
loadOverview();
</script>
</body>
</html>