From 24a9eba9ce0aba5d88bd39be28b0b3c8878b8c6a Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Tue, 31 Mar 2026 08:57:03 +0200 Subject: [PATCH] feat(v0.2.0): datasheets + adoption roadmap + all routes registered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/datasheets/transceiver/:id — Full datasheet with power budget, pricing, compatibility, HTML export - GET /api/datasheets/switch/:id — Switch datasheet with compatible transceivers - GET /api/adoption — Full technology roadmap with maturity indicators - GET /api/adoption/:technology — Detailed adoption analysis, migration paths, risks, timelines - All v0.2.0 routes registered in index.ts --- packages/api/src/index.ts | 4 + packages/api/src/routes/adoption.ts | 155 +++++++++++++ packages/api/src/routes/datasheets.ts | 316 ++++++++++++++++++++++++++ 3 files changed, 475 insertions(+) create mode 100644 packages/api/src/routes/adoption.ts create mode 100644 packages/api/src/routes/datasheets.ts diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 750830a..7360ad6 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -17,6 +17,8 @@ import { finderRouter } from "./routes/finder"; import { competitorRouter } from "./routes/competitor-alerts"; import { forecastRouter } from "./routes/forecast"; import { transportRouter } from "./routes/transport"; +import { datasheetRouter } from "./routes/datasheets"; +import { adoptionRouter } from "./routes/adoption"; const app = express(); @@ -50,6 +52,8 @@ app.use("/api/finder", finderRouter); app.use("/api/competitor-alerts", competitorRouter); app.use("/api/forecast", forecastRouter); app.use("/api/transport", transportRouter); +app.use("/api/datasheets", datasheetRouter); +app.use("/api/adoption", adoptionRouter); // Dashboard (static HTML) app.use("/dashboard", express.static(join(__dirname, "..", "..", "dashboard"))); diff --git a/packages/api/src/routes/adoption.ts b/packages/api/src/routes/adoption.ts new file mode 100644 index 0000000..3044595 --- /dev/null +++ b/packages/api/src/routes/adoption.ts @@ -0,0 +1,155 @@ +/** + * WS7: Path of Adoption & Implementation Roadmap + */ +import { Router } from "express"; +import { computeAllHypeCycles, computeHypeCycle, findTechnology, TECH_GENERATIONS } from "../hype-cycle/norton-bass"; + +export const adoptionRouter = Router(); + +const MIGRATION_PATHS: Record = { + "1G SFP": [{ from: "1G SFP", to: "10G SFP+", steps: ["Verify switch SFP+ port availability", "Order SFP+ transceivers", "Replace SFP modules during maintenance window", "Update monitoring thresholds"], timeline_months: 2, risk: "Low — drop-in replacement in most cases" }], + "10G SFP+": [{ from: "10G SFP+", to: "25G SFP28", steps: ["Verify SFP28 port compatibility (same cage)", "Check NIC support (25G capable)", "Order SFP28 transceivers", "Swap during maintenance", "Update interface speed config"], timeline_months: 3, risk: "Low — SFP28 backward compatible with SFP+ cage" }], + "25G SFP28": [{ from: "25G SFP28", to: "100G QSFP28", steps: ["Plan QSFP28 leaf-spine topology", "Order new QSFP28 switches if needed", "Deploy 100G spine first", "Migrate leaf uplinks to QSFP28", "Use 4×25G breakout cables for transition"], timeline_months: 6, risk: "Medium — requires topology changes" }], + "40G QSFP+": [{ from: "40G QSFP+", to: "100G QSFP28", steps: ["QSFP28 is backward compatible with QSFP+ cage", "Order QSFP28 transceivers", "Swap QSFP+ for QSFP28 per port", "Update port speed configuration", "Verify optic DOM readings"], timeline_months: 3, risk: "Low — same cage, same fiber" }], + "100G QSFP28": [ + { from: "100G QSFP28", to: "400G QSFP-DD", steps: ["Plan 400G spine deployment", "Evaluate QSFP-DD switches (Arista 7060X5, Cisco N9K-C9364D)", "Deploy 400G spine alongside 100G", "Migrate leaf uplinks using 4×100G breakout", "Replace 100G leaf switches over 12 months"], timeline_months: 12, risk: "Medium — new switches needed, but breakout eases transition" }, + { from: "100G QSFP28", to: "400G OSFP", steps: ["Same as QSFP-DD path but with OSFP switches", "Consider thermal requirements (OSFP runs cooler)", "Check rack compatibility"], timeline_months: 12, risk: "Medium — OSFP cages not backward compatible with QSFP" } + ], + "400G QSFP-DD": [{ from: "400G QSFP-DD", to: "800G QSFP-DD800", steps: ["Evaluate QSFP-DD800 switch availability", "QSFP-DD800 is backward compatible with QSFP-DD", "Deploy 800G spine for AI/ML clusters first", "Use 2×400G breakout for initial migration", "Full 800G deployment as prices normalize"], timeline_months: 18, risk: "Medium-High — early adoption, limited vendor choice" }], +}; + +/** + * GET /api/adoption + * Full technology roadmap with adoption phases + */ +adoptionRouter.get("/", async (_req, res) => { + try { + const year = new Date().getFullYear(); + const allCycles = computeAllHypeCycles(year); + + const roadmap = allCycles.map(h => { + const tech = TECH_GENERATIONS.find(t => t.name === h.technology); + const migrations = MIGRATION_PATHS[h.technology] || []; + + return { + technology: h.technology, + speed_gbps: tech?.speedGbps, + form_factor: tech?.formFactor, + intro_year: tech?.introYear, + peak_year: tech?.peakYear, + phase: h.phase, + position_pct: h.positionPct, + adoption_pct: h.adoptionPct, + maturity: getMaturityIndicators(h.phase, h.adoptionPct), + migration_paths: migrations, + recommendation: getRecommendation(h.phase, h.technology), + }; + }).sort((a, b) => (b.speed_gbps || 0) - (a.speed_gbps || 0)); + + res.json({ roadmap, year }); + } catch (err) { + console.error("Adoption roadmap error:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +/** + * GET /api/adoption/:technology + * Detailed adoption analysis for one technology + */ +adoptionRouter.get("/:technology", async (req, res) => { + try { + const tech = findTechnology(req.params.technology); + if (!tech) return res.status(404).json({ error: "Technology not found", available: TECH_GENERATIONS.map(t => t.name) }); + + const year = new Date().getFullYear(); + const hype = computeHypeCycle(tech, year); + const migrations = MIGRATION_PATHS[tech.name] || []; + + res.json({ + technology: tech.name, + speed_gbps: tech.speedGbps, + form_factor: tech.formFactor, + intro_year: tech.introYear, + peak_year: tech.peakYear, + hype_cycle: { + phase: hype.phase, + position_pct: hype.positionPct, + adoption_pct: hype.adoptionPct, + forecast: hype.forecast, + }, + maturity: getMaturityIndicators(hype.phase, hype.adoptionPct), + migration_paths: migrations, + recommendation: getRecommendation(hype.phase, tech.name), + implementation_timeline: getImplementationTimeline(tech.name, hype.phase), + risks: getRiskAssessment(hype.phase, tech.name), + }); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } +}); + +function getMaturityIndicators(phase: string, adoptionPct: number) { + const indicators: Record = {}; + if (adoptionPct > 80) { + indicators.driver_support = "Universal — all major OS versions"; + indicators.interop = "Excellent — plug-and-play across vendors"; + indicators.supply = "Abundant — multiple factories, short lead times"; + indicators.ecosystem = "Mature — extensive documentation, training available"; + } else if (adoptionPct > 50) { + indicators.driver_support = "Good — latest OS versions, some legacy gaps"; + indicators.interop = "Good — tested across major vendors, edge cases exist"; + indicators.supply = "Stable — 3-6 week lead times typical"; + indicators.ecosystem = "Growing — vendor support responsive, community active"; + } else if (adoptionPct > 20) { + indicators.driver_support = "Limited — requires specific firmware versions"; + indicators.interop = "Developing — test before deploying, vendor-specific quirks"; + indicators.supply = "Constrained — 8-16 week lead times, allocation possible"; + indicators.ecosystem = "Early — limited documentation, vendor engineering support needed"; + } else { + indicators.driver_support = "Bleeding edge — beta firmware, limited NOS support"; + indicators.interop = "Minimal — lab testing required, expect issues"; + indicators.supply = "Scarce — sampling/pre-order only, long lead times"; + indicators.ecosystem = "Nascent — standards still evolving, few reference designs"; + } + return indicators; +} + +function getRecommendation(phase: string, tech: string): string { + switch (phase) { + case "PLATEAU_OF_PRODUCTIVITY": return `${tech} is fully mature. Deploy confidently. Focus on cost optimization and vendor consolidation.`; + case "SLOPE_OF_ENLIGHTENMENT": return `${tech} is the sweet spot for deployment. Proven reliability, falling prices, growing ecosystem. Recommended for new deployments.`; + case "TROUGH_OF_DISILLUSIONMENT": return `${tech} is in the trough — prices dropping but some early adopters had interop issues. Good time to buy at discount if you can handle minor quirks.`; + case "PEAK_OF_INFLATED_EXPECTATIONS": return `${tech} is overhyped. Only deploy if you have a specific use case requiring it. Expect premium pricing and potential supply constraints.`; + case "INNOVATION_TRIGGER": return `${tech} is emerging technology. Evaluate in lab only. Do NOT deploy in production. Wait for standards ratification and ecosystem development.`; + default: return `${tech} is in legacy phase. Plan migration to next generation.`; + } +} + +function getImplementationTimeline(tech: string, phase: string) { + return { + evaluation: phase === "INNOVATION_TRIGGER" ? "3-6 months" : "2-4 weeks", + lab_testing: phase === "INNOVATION_TRIGGER" ? "3-6 months" : "2-4 weeks", + pilot_deployment: "1-3 months", + production_rollout: "3-12 months depending on scale", + full_migration: "6-18 months", + total_estimated: phase === "PLATEAU_OF_PRODUCTIVITY" ? "3-6 months" : + phase === "SLOPE_OF_ENLIGHTENMENT" ? "6-12 months" : + phase === "TROUGH_OF_DISILLUSIONMENT" ? "9-15 months" : "12-24 months", + }; +} + +function getRiskAssessment(phase: string, tech: string) { + return { + technology_risk: phase === "INNOVATION_TRIGGER" ? "High — standards incomplete, firmware bugs likely" : + phase === "PEAK_OF_INFLATED_EXPECTATIONS" ? "Medium — standards ratified but early silicon" : + "Low — proven technology, stable implementations", + supply_risk: phase === "INNOVATION_TRIGGER" ? "High — limited fab capacity, allocation" : + phase === "PEAK_OF_INFLATED_EXPECTATIONS" ? "Medium — demand may exceed supply at peak" : + "Low — multiple suppliers, established supply chains", + vendor_lock_in: "Low — Flexoptix FlexBox coding eliminates vendor lock-in for transceivers", + interop_risk: phase === "INNOVATION_TRIGGER" ? "High — expect vendor-specific issues" : + phase === "PEAK_OF_INFLATED_EXPECTATIONS" ? "Medium — test thoroughly before deployment" : + "Low — broadly tested interoperability", + }; +} diff --git a/packages/api/src/routes/datasheets.ts b/packages/api/src/routes/datasheets.ts new file mode 100644 index 0000000..bd4171e --- /dev/null +++ b/packages/api/src/routes/datasheets.ts @@ -0,0 +1,316 @@ +/** + * WS2: Automated Datasheet Generation + * + * Generates professional PDF datasheets for transceivers and switches. + * Uses HTML templates rendered via the dashboard's static serving. + * Returns structured HTML that can be printed to PDF client-side or via Puppeteer. + */ +import { Router } from "express"; +import { pool } from "../db/client"; + +export const datasheetRouter = Router(); + +/** + * GET /api/datasheets/transceiver/:id + * Returns structured datasheet data for a transceiver (JSON or HTML) + */ +datasheetRouter.get("/transceiver/:id", async (req, res) => { + try { + const { format = "json" } = req.query; + const id = req.params.id; + + // Get transceiver with full details + const t = await pool.query( + `SELECT t.*, v.name AS vendor_name, v.website AS vendor_website, v.logo_r2_key, + s.name AS standard_full_name, s.ieee_reference, s.year_ratified + FROM transceivers t + LEFT JOIN vendors v ON t.vendor_id = v.id + LEFT JOIN standards s ON t.standard_id = s.id + WHERE t.id = $1::text::uuid OR t.slug = $1`, + [id] + ); + + if (!t.rows[0]) return res.status(404).json({ error: "Transceiver not found" }); + const product = t.rows[0]; + + // Get compatible switches (top 30) + const compat = await pool.query( + `SELECT sw.model, sw.series, sv.name AS vendor, c.firmware_min, c.verified_by + FROM compatibility c + JOIN switches sw ON c.switch_id = sw.id + JOIN vendors sv ON sw.vendor_id = sv.id + WHERE c.transceiver_id = $1 AND c.status = 'compatible' + ORDER BY sv.name, sw.model LIMIT 30`, + [product.id] + ); + + // Get latest prices from multiple vendors + const prices = await pool.query( + `SELECT DISTINCT ON (sv.name) + sv.name AS vendor, po.price, po.currency, po.stock_level, po.url, po.time + FROM price_observations po + JOIN vendors sv ON po.source_vendor_id = sv.id + WHERE po.transceiver_id = $1 + ORDER BY sv.name, po.time DESC`, + [product.id] + ); + + // Power budget calculation + const txPowerMin = product.tx_power_min_dbm ? parseFloat(product.tx_power_min_dbm) : null; + const txPowerMax = product.tx_power_max_dbm ? parseFloat(product.tx_power_max_dbm) : null; + const rxSensitivity = product.rx_sensitivity_dbm ? parseFloat(product.rx_sensitivity_dbm) : null; + const opticalBudget = product.optical_budget_db ? parseFloat(product.optical_budget_db) : null; + let powerBudget = null; + if (txPowerMin !== null && rxSensitivity !== null) { + const budget = txPowerMin - rxSensitivity; + const connectorLoss = 0.5; // 2 connectors × 0.25 dB + const spliceLoss = 0.1; + const fiberLoss = product.fiber_type === 'SMF' + ? (product.reach_meters / 1000) * 0.35 // 0.35 dB/km at 1310nm + : (product.reach_meters / 1000) * 3.5; // 3.5 dB/km at 850nm MMF + const margin = budget - fiberLoss - connectorLoss - spliceLoss; + powerBudget = { + tx_power_min_dbm: txPowerMin, + tx_power_max_dbm: txPowerMax, + rx_sensitivity_dbm: rxSensitivity, + link_budget_db: Math.round(budget * 10) / 10, + fiber_loss_db: Math.round(fiberLoss * 10) / 10, + connector_loss_db: connectorLoss, + splice_loss_db: spliceLoss, + margin_db: Math.round(margin * 10) / 10, + sufficient: margin >= 3, + }; + } + + const datasheet = { + product: { + slug: product.slug, + part_number: product.part_number, + vendor: product.vendor_name, + standard: product.standard_full_name || product.standard_name, + ieee_reference: product.ieee_reference, + form_factor: product.form_factor, + speed: product.speed, + speed_gbps: parseFloat(product.speed_gbps), + lanes: product.lanes, + lane_rate: product.lane_rate, + modulation: product.modulation, + reach_label: product.reach_label, + reach_meters: product.reach_meters, + fiber_type: product.fiber_type, + wavelengths: product.wavelengths, + connector: product.connector, + power_consumption_w: product.power_consumption_w ? parseFloat(product.power_consumption_w) : null, + temp_range: product.temp_range, + dom_support: product.dom_support, + category: product.category, + market_status: product.market_status, + image_url: product.image_url, + }, + optical: powerBudget, + wdm: product.wdm_type ? { + type: product.wdm_type, + channels: product.channel_count, + spacing_ghz: product.channel_spacing_ghz ? parseFloat(product.channel_spacing_ghz) : null, + tunable: product.tunable, + itu_grid: product.itu_grid, + } : null, + coherent: product.coherent ? { + baud_rate_gbaud: product.baud_rate_gbaud ? parseFloat(product.baud_rate_gbaud) : null, + fec_type: product.fec_type, + dsp_vendor: product.dsp_vendor, + } : null, + compatible_switches: compat.rows, + pricing: prices.rows.map(p => ({ + vendor: p.vendor, + price: parseFloat(p.price), + currency: p.currency, + stock: p.stock_level, + url: p.url, + as_of: p.time, + })), + flexoptix: { + buy_url: `https://www.flexoptix.net/en/catalogsearch/result/?q=${encodeURIComponent(product.form_factor + ' ' + product.speed_gbps + 'G ' + product.reach_label)}`, + flexbox_note: "Flexoptix transceivers support FlexBox coding — one module works in any vendor's switch.", + }, + generated_at: new Date().toISOString(), + }; + + if (format === "html") { + res.setHeader("Content-Type", "text/html"); + res.send(renderDatasheetHtml(datasheet)); + } else { + res.json(datasheet); + } + } catch (err) { + console.error("Datasheet error:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +/** + * GET /api/datasheets/switch/:id + */ +datasheetRouter.get("/switch/:id", async (req, res) => { + try { + const id = req.params.id; + + const sw = await pool.query( + `SELECT sw.*, v.name AS vendor_name, v.website AS vendor_website + FROM switches sw JOIN vendors v ON sw.vendor_id = v.id + WHERE sw.id = $1::text::uuid OR sw.model = $1`, + [id] + ); + if (!sw.rows[0]) return res.status(404).json({ error: "Switch not found" }); + const device = sw.rows[0]; + + const compat = await pool.query( + `SELECT t.form_factor, t.speed, t.speed_gbps, t.reach_label, t.fiber_type, + tv.name AS transceiver_vendor, c.firmware_min + FROM compatibility c + JOIN transceivers t ON c.transceiver_id = t.id + JOIN vendors tv ON t.vendor_id = tv.id + WHERE c.switch_id = $1 AND c.status = 'compatible' + ORDER BY t.speed_gbps DESC, tv.name LIMIT 50`, + [device.id] + ); + + const docs = await pool.query( + `SELECT doc_type, title, source_url FROM product_documents WHERE switch_id = $1 ORDER BY doc_type`, + [device.id] + ); + + res.json({ + switch: { + model: device.model, + series: device.series, + vendor: device.vendor_name, + category: device.category, + ports_config: device.ports_config, + total_ports: device.total_ports, + max_speed_gbps: device.max_speed_gbps, + switching_capacity_tbps: device.switching_capacity_tbps, + asic: device.asic_vendor ? `${device.asic_vendor} ${device.asic_model || ''}`.trim() : null, + sonic_compatible: device.sonic_compatible, + image_url: device.image_url, + }, + compatible_transceivers: compat.rows, + documents: docs.rows, + generated_at: new Date().toISOString(), + }); + } catch (err) { + console.error("Switch datasheet error:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +/** + * GET /api/datasheets/list — recently generated datasheets + */ +datasheetRouter.get("/list", async (_req, res) => { + try { + const result = await pool.query( + `SELECT * FROM generated_datasheets ORDER BY generated_at DESC LIMIT 50` + ); + res.json({ datasheets: result.rows }); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } +}); + +function renderDatasheetHtml(ds: any): string { + const p = ds.product; + const prices = ds.pricing.map((pr: any) => `${pr.vendor}${pr.currency} ${pr.price.toFixed(2)}${pr.stock || 'N/A'}`).join(''); + const switches = ds.compatible_switches.map((s: any) => `${s.vendor}${s.model}${s.firmware_min || 'Any'}`).join(''); + + return ` + + + +${p.form_factor} ${p.speed} ${p.reach_label} — Datasheet | TIP + + + +
+
+

${p.form_factor} ${p.speed} ${p.reach_label}

+

${p.standard || ''} ${p.ieee_reference ? '(' + p.ieee_reference + ')' : ''}

+
+
${p.market_status || 'Active'}
+
+ + ${p.image_url ? `
${p.form_factor}
` : ''} + +
+

Specifications

+
+
Form Factor${p.form_factor}
+
Speed${p.speed} (${p.speed_gbps} Gbps)
+
Reach${p.reach_label} (${p.reach_meters >= 1000 ? (p.reach_meters/1000) + ' km' : p.reach_meters + ' m'})
+
Fiber${p.fiber_type || 'N/A'}
+
Connector${p.connector || 'N/A'}
+
Wavelength${p.wavelengths || 'N/A'}
+ ${p.lanes ? `
Lanes${p.lanes}× ${p.lane_rate || ''}
` : ''} + ${p.modulation ? `
Modulation${p.modulation}
` : ''} +
Power${p.power_consumption_w ? p.power_consumption_w + ' W' : 'N/A'}
+
Temperature${p.temp_range === 'COM' ? '0°C to 70°C (Commercial)' : '-40°C to 85°C (Industrial)'}
+
+
+ + ${ds.optical ? ` +
+

Power Budget

+
+ TX Power: ${ds.optical.tx_power_min_dbm} to ${ds.optical.tx_power_max_dbm} dBm | + RX Sensitivity: ${ds.optical.rx_sensitivity_dbm} dBm | + Link Budget: ${ds.optical.link_budget_db} dB | + Fiber Loss: ${ds.optical.fiber_loss_db} dB | + Margin: ${ds.optical.margin_db} dB ${ds.optical.sufficient ? '✓' : '⚠ INSUFFICIENT'} +
+
` : ''} + + ${prices ? ` +
+

Current Pricing

+ ${prices}
VendorPriceStock
+
` : ''} + + ${switches ? ` +
+

Compatible Switches (${ds.compatible_switches.length})

+ ${switches}
VendorModelMin Firmware
+
` : ''} + +
+ Buy at Flexoptix → +

${ds.flexoptix.flexbox_note}

+
+ + + +`; +}