Rene Fichtmueller 8c6df1028d merge: reconcile Erik-deployed fixes with Gitea feature branch (origin/main)
Merges 28 Gitea commits (Transceiver Academy, dashboard features A-P, equivalence
matchers OPN/spec, Abverkauf velocity engine, Flexoptix detail sync, SQL 111-118,
MCP equivalences tool, training data) with 22 Erik-local commits (price chart,
fmtSpd, supply-squeeze per-SKU fix, research-robot actions, NADDOD warehouse,
FS competitor stock, reorder bloat fix, flexoptix empty-token fix, vendor
reliability, blog WireGuard failover).

Only 2 files changed on both sides; ort auto-merged both cleanly. Manually
resolved a resulting duplicate GET /api/hype-cycle/market-signals: kept the
Erik handler (technologies+marketSignalScore+drivers+globalContext, matches the
merged dashboard), removed the Gitea Multi-source variant (preserved in history).

All 3 packages build with 0 TS errors. Safety: branch erik-pre-reconcile-20260607,
bundle _RECON_full-backup-20260607.bundle (also on Fearghas), bulk-price WIP in
stash@{0}, untracked SQL backed up to _RECON_untracked_sql_20260607/.
2026-06-07 05:01:32 +00:00

354 lines
14 KiB
TypeScript

/**
* 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,
});
});