/** * Hype Cycle API routes * * GET /api/hype-cycle — All technologies with current phase * GET /api/hype-cycle/enriched — All technologies with data-driven metrics * GET /api/hype-cycle/lifecycle — Revenue lifecycle predictions for all speeds * GET /api/hype-cycle/regional/:tech — Regional adoption model for a technology * GET /api/hype-cycle/:tech — Specific technology with 5-year forecast */ import { Router, Request, Response } from "express"; import { computeAllHypeCycles, computeHypeCycle, findTechnology, TECH_GENERATIONS, SPECIAL_TECHS, } from "../hype-cycle/norton-bass"; import { getDataDrivenOverrides, getSpeedClassMetrics, computeRevenueLifecycle, computeRegionalAdoption, } from "../hype-cycle/data-enrichment"; export const hypeCycleRouter = Router(); const q = (p: string, req: Request): string | undefined => req.query[p] ? String(req.query[p]) : undefined; // GET /api/hype-cycle — All technologies (model-only, fast) hypeCycleRouter.get("/", (_req: Request, res: Response) => { const yearParam = q("year", _req); const year = yearParam ? parseInt(yearParam) : new Date().getFullYear(); const results = computeAllHypeCycles(year); const sorted = [...results].sort((a, b) => a.positionPct - b.positionPct); res.json({ success: true, year, model: "Norton-Bass Multigenerational Diffusion", technologies: sorted.map((r) => ({ technology: r.technology, phase: r.phaseLabel, positionPct: r.positionPct, adoptionPct: r.adoptionPct, compositeScore: r.compositeScore, peakYear: r.forecast.peakShipmentYear, yearsToPlateauFromNow: r.forecast.yearsToPlateauFromNow, })), }); }); // GET /api/hype-cycle/enriched — Data-driven enrichment from scraped data hypeCycleRouter.get("/enriched", async (_req: Request, res: Response) => { try { const yearParam = q("year", _req); const year = yearParam ? parseInt(yearParam) : new Date().getFullYear(); const overridesMap = await getDataDrivenOverrides(); const allTechs = [...TECH_GENERATIONS, ...SPECIAL_TECHS]; const results = allTechs.map((tech) => { const overrides = overridesMap.get(tech.speedGbps); return computeHypeCycle(tech, year, overrides); }); const sorted = [...results].sort((a, b) => a.positionPct - b.positionPct); // Also include raw metrics for transparency const speedMetrics = await getSpeedClassMetrics(); res.json({ success: true, year, model: "Norton-Bass + Data-Driven Enrichment", dataSource: { totalTransceivers: speedMetrics.reduce((s, m) => s + m.skuCount, 0), totalPricePoints: speedMetrics.reduce((s, m) => s + m.priceCount, 0), speedClasses: speedMetrics.length, }, technologies: sorted.map((r) => ({ technology: r.technology, phase: r.phaseLabel, positionPct: r.positionPct, adoptionPct: r.adoptionPct, compositeScore: r.compositeScore, peakYear: r.forecast.peakShipmentYear, yearsToPlateauFromNow: r.forecast.yearsToPlateauFromNow, metrics: r.metrics, fiveYearForecast: r.forecast.fiveYearProjection, })), rawSpeedMetrics: speedMetrics.map((m) => ({ speedGbps: m.speedGbps, vendorCount: m.vendorCount, skuCount: m.skuCount, avgPrice: m.avgPrice ? Math.round(m.avgPrice * 100) / 100 : null, minPrice: m.minPrice ? Math.round(m.minPrice * 100) / 100 : null, maxPrice: m.maxPrice ? Math.round(m.maxPrice * 100) / 100 : null, formFactors: m.formFactors, reachVariants: m.reachVariants, })), }); } catch (err) { console.error("Enriched hype cycle error:", err); res.status(500).json({ success: false, error: "Failed to compute enriched hype cycle" }); } }); // GET /api/hype-cycle/lifecycle — Revenue lifecycle predictions hypeCycleRouter.get("/lifecycle", async (_req: Request, res: Response) => { try { const currentYear = new Date().getFullYear(); const speedMetrics = await getSpeedClassMetrics(); const priceMap = new Map(speedMetrics.map((m) => [m.speedGbps, m.avgPrice])); const allTechs = [...TECH_GENERATIONS, ...SPECIAL_TECHS]; const lifecycles = allTechs.map((tech) => computeRevenueLifecycle( tech.speedGbps, tech.name, tech.introYear, tech.peakYear, currentYear, priceMap.get(tech.speedGbps), ) ); // Sort by revenue index (highest current revenue first) const sorted = [...lifecycles].sort((a, b) => b.revenueIndex - a.revenueIndex); res.json({ success: true, currentYear, lifecycles: sorted, }); } catch (err) { console.error("Lifecycle error:", err); res.status(500).json({ success: false, error: "Failed to compute lifecycles" }); } }); // GET /api/hype-cycle/regional/:tech — Regional adoption by technology hypeCycleRouter.get("/regional/:tech", (req: Request, res: Response) => { const techQuery = String(req.params.tech); const currentYear = new Date().getFullYear(); const tech = findTechnology(techQuery); if (!tech) { res.status(404).json({ success: false, error: `Technology "${techQuery}" not found. Available: 1G, 10G, 25G, 40G, 100G, 400G, 800G, 1.6T, CPO, LPO, 400ZR`, }); return; } const regions = computeRegionalAdoption(tech.peakYear, currentYear, tech.name); res.json({ success: true, technology: tech.name, speedGbps: tech.speedGbps, globalPeakYear: tech.peakYear, regions, }); }); // ── MARKET SIGNAL COMPUTATION ─────────────────────────────────────────────── /** Technology → form-factor/speed mapping for cross-table signal aggregation */ const TECH_SIGNAL_MAP = [ { label: "10G-SFP+", speedGbps: 10, formFactors: ["SFP+"], speedLabel: "10G" }, { label: "100G-QSFP28", speedGbps: 100, formFactors: ["QSFP28"], speedLabel: "100G" }, { label: "400G-QSFP-DD", speedGbps: 400, formFactors: ["QSFP-DD"], speedLabel: "400G" }, { label: "800G-OSFP", speedGbps: 800, formFactors: ["OSFP"], speedLabel: "800G" }, { label: "1.6T-OSFP", speedGbps: 1600, formFactors: ["OSFP"], speedLabel: "1.6T" }, { label: "400G-ZR", speedGbps: 400, formFactors: ["SFP-DD", "CFP2"], speedLabel: "400G" }, ] as const; type PhaseKey = | "plateau_productivity" | "slope_enlightenment" | "trough_disillusionment" | "peak_inflated_expectations" | "innovation_trigger"; function buildRecommendation( phase: PhaseKey, signalScore: number, capexYoyAvg: number, speedGbps: number, ): { label: string; color: string; detail: string } { const fast = speedGbps >= 400; const capexBoom = capexYoyAvg > 50; switch (phase) { case "plateau_productivity": if (fast && capexBoom) return { label: "🚀 Buy — AI Wave", color: "#16a34a", detail: "Commodity pricing + AI infrastructure demand surge. Stock up now." }; if (signalScore >= 85) return { label: "✅ Hold — Stable", color: "#2563eb", detail: "Mature commodity market, stable long-term demand." }; return { label: "📦 Hold", color: "#64748b", detail: "Commodity market. Price-driven. Order on demand." }; case "slope_enlightenment": if (capexBoom) return { label: "🟢 Buy Now", color: "#16a34a", detail: "Growing mainstream adoption + hyperscaler capex boom. Window closing." }; return { label: "🟡 Buy", color: "#ca8a04", detail: "Adoption curve steepening. Monitor pricing before large orders." }; case "trough_disillusionment": if (signalScore > 50) return { label: "🔍 Buy Opportunity", color: "#7c3aed", detail: "Hype trough but demand signals emerging. Strategic buying window." }; return { label: "⏳ Watch", color: "#94a3b8", detail: "Wait for demand confirmation before stocking." }; case "peak_inflated_expectations": if (fast && capexBoom) return { label: "⚡ Caution / Buy", color: "#f97316", detail: "Hype peak but real hyperscaler demand. Buy selectively, not speculatively." }; return { label: "⚠ Caution", color: "#ef4444", detail: "Peak hype. Verify real end-customer demand before building inventory." }; case "innovation_trigger": return { label: "👁 Watch", color: "#94a3b8", detail: "Early stage — too early for volume commitments. Monitor for traction." }; default: return { label: "📊 Monitor", color: "#64748b", detail: "Monitor signals." }; } } // GET /api/hype-cycle/analysis — Bass-fitted results from DB (hype_cycle_analysis table) hypeCycleRouter.get("/analysis", async (_req: Request, res: Response) => { try { const { pool } = await import("../db/client"); const { rows } = await pool.query(` SELECT DISTINCT ON (technology) technology, computed_at, bass_p, bass_q, bass_m, t_peak_year, current_t, ROUND(current_share * 100, 1) AS share_pct, ROUND(projected_share_1y * 100, 1) AS share_1y_pct, ROUND(projected_share_3y * 100, 1) AS share_3y_pct, hype_phase, hype_score, phase_since_year, years_to_next_phase, asp_current_usd, asp_decline_pct_3y, r_squared, data_points, notes FROM hype_cycle_analysis ORDER BY technology, computed_at DESC `); if (rows.length === 0) { res.json({ success: true, source: "db", data: [], message: "No analysis yet — run compute:hype-cycle job" }); return; } res.json({ success: true, source: "db", computed_at: rows[0]?.computed_at, data: rows }); } catch (err) { console.error("Hype cycle analysis DB error:", err); res.status(500).json({ success: false, error: "Failed to fetch analysis" }); } }); // GET /api/hype-cycle/:tech — Specific technology detail (must be last!) // GET /api/hype-cycle/market-signals — data-driven per-technology market signal score + drivers + recommendation hypeCycleRouter.get("/market-signals", async (_req: Request, res: Response) => { try { const yearParam = q("year", _req); const year = yearParam ? parseInt(yearParam) : new Date().getFullYear(); const overridesMap = await getDataDrivenOverrides(); const speedMetrics = await getSpeedClassMetrics(); const metricsBySpeed = new Map(speedMetrics.map((m) => [m.speedGbps, m])); const maxSku = Math.max(1, ...speedMetrics.map((m) => m.skuCount)); const allTechs = [...TECH_GENERATIONS, ...SPECIAL_TECHS]; const technologies = allTechs.map((tech) => { const r = computeHypeCycle(tech, year, overridesMap.get(tech.speedGbps)); const sm = metricsBySpeed.get(tech.speedGbps); const adoption = Math.max(0, Math.round(r.adoptionPct)); const composite = Math.max(0, Math.round(r.compositeScore)); const depth = sm ? Math.round(100 * Math.min(1, sm.skuCount / maxSku)) : 0; const marketSignalScore = Math.max(0, Math.min(100, Math.round(0.4 * adoption + 0.3 * composite + 0.3 * depth))); const drivers: string[] = []; drivers.push(adoption + "% modeled adoption"); if (sm && sm.skuCount) drivers.push(sm.skuCount + " SKUs / " + sm.vendorCount + " vendors"); if (sm && sm.avgPrice) drivers.push("avg $" + Math.round(sm.avgPrice)); drivers.push("phase: " + r.phaseLabel); const phase = String(r.phaseLabel || ""); let recommendation = { label: "Monitor", detail: "Insufficient market momentum — keep watching.", color: "#94a3b8" }; if (/Plateau/i.test(phase)) recommendation = { label: "Commodity — negotiate", detail: "Mature, broad supply (" + (sm ? sm.vendorCount : 0) + " vendors). Push for price.", color: "#0ea5e9" }; else if (/Slope/i.test(phase)) recommendation = { label: "Mainstream — buy", detail: "Mainstreaming with falling prices and rising supply.", color: "#16a34a" }; else if (/Trough/i.test(phase)) recommendation = { label: "Maturing — opportunity", detail: "Past the hype; early mainstream pricing forming.", color: "#ca8a04" }; else if (/Peak/i.test(phase)) recommendation = { label: "Hype peak — caution", detail: "Elevated expectations; supply and price still volatile.", color: "#f97316" }; else if (/Innovation|Trigger/i.test(phase)) recommendation = { label: "Emerging — pilot", detail: "Early window; limited supply, premium pricing.", color: "#ca8a04" }; return { technology: tech.name, phase: r.phaseLabel, marketSignalScore, adoptionPct: adoption, compositeScore: composite, skuCount: sm ? sm.skuCount : 0, vendorCount: sm ? sm.vendorCount : 0, avgPrice: sm && sm.avgPrice ? Math.round(sm.avgPrice * 100) / 100 : null, drivers, recommendation, }; }); technologies.sort((a, b) => b.marketSignalScore - a.marketSignalScore); const totalSkus = speedMetrics.reduce((acc, m) => acc + m.skuCount, 0); const totalVendors = speedMetrics.length ? Math.max(...speedMetrics.map((m) => m.vendorCount)) : 0; const avgSignal = technologies.length ? Math.round(technologies.reduce((acc, t) => acc + t.marketSignalScore, 0) / technologies.length) : 0; const globalContext = { totalSkus, totalVendors, avgMarketSignal: avgSignal, strongest: technologies.length ? technologies[0].technology : null, strongestScore: technologies.length ? technologies[0].marketSignalScore : null, generatedAt: new Date().toISOString(), }; res.json({ success: true, year, technologies, globalContext }); } catch (err) { console.error("market-signals error:", err); res.status(500).json({ success: false, error: "Failed to compute market signals" }); } }); hypeCycleRouter.get("/:tech", (req: Request, res: Response) => { const techQuery = String(req.params.tech); const yearParam = q("year", req); const year = yearParam ? parseInt(yearParam) : new Date().getFullYear(); const tech = findTechnology(techQuery); if (!tech) { res.status(404).json({ success: false, error: `Technology "${techQuery}" not found. Available: 1G, 10G, 25G, 40G, 100G, 400G, 800G, 1.6T, CPO, LPO, 400ZR`, }); return; } const result = computeHypeCycle(tech, year); // Add regional data const regions = computeRegionalAdoption(tech.peakYear, year, tech.name); // Add revenue lifecycle const lifecycle = computeRevenueLifecycle( tech.speedGbps, tech.name, tech.introYear, tech.peakYear, year, ); res.json({ success: true, ...result, regionalAdoption: regions, revenueLifecycle: lifecycle, }); });