feat: hype cycle hover tooltips + phase legend, fix switch-docs missing column

Dashboard: Added hover tooltips on hype cycle dots showing phase, adoption %,
peak year, score. Added color-coded phase legend with technology counts.
MCP: Fixed docs_portal_url column reference in switch-docs tool.
This commit is contained in:
Rene Fichtmueller 2026-03-30 08:25:41 +02:00
parent 814325b349
commit f614c425ea
2 changed files with 46 additions and 8 deletions

View File

@ -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 += '<circle cx="' + dotX + '" cy="' + dotY + '" r="10" fill="none" stroke="' + color + '" stroke-width="0.8" opacity="0.25" />';
// Main dot
svg += '<circle cx="' + dotX + '" cy="' + dotY + '" r="5.5" fill="' + color + '" class="hype-dot" data-tech="' + esc(t.technology) + '" filter="url(#glow)" stroke="rgba(255,255,255,0.25)" stroke-width="0.8" />';
// Main dot (with hover data attributes)
var adoptPct = t.adoptionPct != null ? Math.round(t.adoptionPct) : 0;
var peakYr = t.peakYear || '—';
svg += '<circle cx="' + dotX + '" cy="' + dotY + '" r="5.5" fill="' + color + '" class="hype-dot" data-tech="' + esc(t.technology) + '" data-phase="' + esc(t.phase) + '" data-adoption="' + adoptPct + '" data-peak="' + peakYr + '" data-score="' + (t.compositeScore||0) + '" filter="url(#glow)" stroke="rgba(255,255,255,0.25)" stroke-width="0.8" style="cursor:pointer" />';
// Inner highlight
svg += '<circle cx="' + (dotX-1.2) + '" cy="' + (dotY-1.2) + '" r="1.5" fill="#fff" opacity="0.5" pointer-events="none" />';
}
svg += '</svg>';
// Phase legend
svg += '<div class="hype-legend">';
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 += '<span class="legend-item"><span class="legend-dot" style="background:'+phases[pi].c+'"></span>'+phases[pi].k+' ('+cnt+')</span>';
}
svg += '</div>';
return svg;
}

View File

@ -19,7 +19,7 @@ export async function registerSwitchDocTools(server: McpServer): Promise<void> {
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<void> {
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) {