diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html
index f3bb6e7..3b362f4 100644
--- a/packages/dashboard/index.html
+++ b/packages/dashboard/index.html
@@ -328,6 +328,29 @@
@keyframes hype-pulse-anim { 0%,100% { opacity: 0.25; r: 16; } 50% { opacity: 0.08; r: 24; } }
.hype-connector { stroke: rgba(255,255,255,0.12); stroke-width: 1; stroke-dasharray: 2,3; }
+ /* Hype Cycle Hover Tooltip */
+ .hype-tooltip {
+ position: fixed; z-index: 1000; pointer-events: none;
+ background: rgba(10,11,16,0.95); border: 1px solid rgba(255,129,0,0.3);
+ border-radius: 8px; padding: 10px 14px; min-width: 180px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.5); backdrop-filter: blur(8px);
+ font-size: 12px; color: var(--text); opacity: 0; transition: opacity 0.15s;
+ }
+ .hype-tooltip.visible { opacity: 1; }
+ .hype-tooltip .tt-tech { font-weight: 700; font-size: 13px; margin-bottom: 4px; }
+ .hype-tooltip .tt-phase { font-size: 11px; opacity: 0.7; margin-bottom: 6px; }
+ .hype-tooltip .tt-row { display: flex; justify-content: space-between; gap: 12px; margin: 2px 0; }
+ .hype-tooltip .tt-label { color: var(--text-dim); }
+ .hype-tooltip .tt-val { font-family: 'JetBrains Mono', monospace; font-weight: 600; }
+
+ /* Phase Legend */
+ .hype-legend {
+ display: flex; flex-wrap: wrap; gap: 12px; justify-content: center;
+ margin-top: 12px; padding: 8px 0;
+ }
+ .legend-item { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-dim); }
+ .legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
+
.hype-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 1.5rem; position: relative; z-index: 1;
@@ -1422,13 +1445,31 @@ function renderHypeSvg(techs) {
// Outer ring
svg += '';
- // Main dot
- svg += '';
+ // Main dot (with hover data attributes)
+ var adoptPct = t.adoptionPct != null ? Math.round(t.adoptionPct) : 0;
+ var peakYr = t.peakYear || '—';
+ svg += '';
// Inner highlight
svg += '';
}
svg += '';
+
+ // Phase legend
+ svg += '
';
+ var phases = [
+ {k:'Innovation Trigger',c:PC['Innovation Trigger']||'#4deaff'},
+ {k:'Peak of Inflated Expectations',c:PC['Peak of Inflated Expectations']||'#fbbf24'},
+ {k:'Trough of Disillusionment',c:PC['Trough of Disillusionment']||'#f87171'},
+ {k:'Slope of Enlightenment',c:PC['Slope of Enlightenment']||'#a78bfa'},
+ {k:'Plateau of Productivity',c:PC['Plateau of Productivity']||'#34d399'},
+ {k:'Legacy / Decline',c:PC['Legacy / Decline']||'#8888a4'}
+ ];
+ for (var pi = 0; pi < phases.length; pi++) {
+ var cnt = techs.filter(function(t){return t.phase===phases[pi].k}).length;
+ if (cnt > 0) svg += ''+phases[pi].k+' ('+cnt+')';
+ }
+ svg += '
';
return svg;
}
diff --git a/packages/mcp-server/src/tools/switch-docs.ts b/packages/mcp-server/src/tools/switch-docs.ts
index 16464af..c216291 100644
--- a/packages/mcp-server/src/tools/switch-docs.ts
+++ b/packages/mcp-server/src/tools/switch-docs.ts
@@ -19,7 +19,7 @@ export async function registerSwitchDocTools(server: McpServer): Promise {
const switchResult = await pool.query(
`SELECT sw.id, sw.model, sw.series, sw.image_url, sw.datasheet_url,
sw.product_page_url, sw.manual_urls,
- v.name as vendor_name, v.docs_portal_url, v.support_portal_url
+ v.name as vendor_name, v.website as vendor_website
FROM switches sw
LEFT JOIN vendors v ON sw.vendor_id = v.id
WHERE sw.model ILIKE $1
@@ -60,11 +60,8 @@ export async function registerSwitchDocTools(server: McpServer): Promise {
if (sw.datasheet_url) {
text += `**Datasheet:** ${sw.datasheet_url}\n`;
}
- if (sw.docs_portal_url) {
- text += `**Vendor Docs Portal:** ${sw.docs_portal_url}\n`;
- }
- if (sw.support_portal_url) {
- text += `**Support Portal:** ${sw.support_portal_url}\n`;
+ if (sw.vendor_website) {
+ text += `**Vendor Website:** ${sw.vendor_website}\n`;
}
if (docsResult.rows.length > 0) {