feat(v0.2.0): datasheets + adoption roadmap + all routes registered
- 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
This commit is contained in:
parent
aa977abc97
commit
3a6cbc475d
@ -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")));
|
||||
|
||||
155
packages/api/src/routes/adoption.ts
Normal file
155
packages/api/src/routes/adoption.ts
Normal file
@ -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<string, { from: string; to: string; steps: string[]; timeline_months: number; risk: string }[]> = {
|
||||
"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<string, string> = {};
|
||||
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",
|
||||
};
|
||||
}
|
||||
316
packages/api/src/routes/datasheets.ts
Normal file
316
packages/api/src/routes/datasheets.ts
Normal file
@ -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) => `<tr><td>${pr.vendor}</td><td>${pr.currency} ${pr.price.toFixed(2)}</td><td>${pr.stock || 'N/A'}</td></tr>`).join('');
|
||||
const switches = ds.compatible_switches.map((s: any) => `<tr><td>${s.vendor}</td><td>${s.model}</td><td>${s.firmware_min || 'Any'}</td></tr>`).join('');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${p.form_factor} ${p.speed} ${p.reach_label} — Datasheet | TIP</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'DM Sans', sans-serif; color: #333; background: #fff; max-width: 210mm; margin: 0 auto; padding: 20mm; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; border-bottom: 3px solid #FF6600; padding-bottom: 16px; margin-bottom: 24px; }
|
||||
.header h1 { font-size: 24px; font-weight: 700; }
|
||||
.header .badge { background: #FF6600; color: white; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||||
.section { margin-bottom: 20px; }
|
||||
.section h2 { font-size: 16px; font-weight: 600; color: #FF6600; margin-bottom: 8px; border-bottom: 1px solid #eee; padding-bottom: 4px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
th, td { padding: 6px 10px; text-align: left; border-bottom: 1px solid #f0f0f0; }
|
||||
th { background: #f8f8f8; font-weight: 600; }
|
||||
.specs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.spec-item { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #f5f5f5; font-size: 13px; }
|
||||
.spec-label { color: #888; }
|
||||
.spec-value { font-weight: 600; }
|
||||
.power-budget { background: #f8f9fa; padding: 12px; border-radius: 8px; font-size: 13px; }
|
||||
.power-budget .ok { color: #2d6a4f; } .power-budget .warn { color: #c1121f; }
|
||||
.footer { margin-top: 30px; padding-top: 12px; border-top: 2px solid #FF6600; font-size: 11px; color: #888; display: flex; justify-content: space-between; }
|
||||
.cta { background: #FF6600; color: white; padding: 10px 20px; border-radius: 8px; text-decoration: none; font-weight: 600; display: inline-block; margin-top: 12px; }
|
||||
@media print { body { padding: 10mm; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>${p.form_factor} ${p.speed} ${p.reach_label}</h1>
|
||||
<p style="color:#888;font-size:14px">${p.standard || ''} ${p.ieee_reference ? '(' + p.ieee_reference + ')' : ''}</p>
|
||||
</div>
|
||||
<div class="badge">${p.market_status || 'Active'}</div>
|
||||
</div>
|
||||
|
||||
${p.image_url ? `<div style="text-align:center;margin-bottom:20px"><img src="${p.image_url}" alt="${p.form_factor}" style="max-height:120px;border-radius:8px"></div>` : ''}
|
||||
|
||||
<div class="section">
|
||||
<h2>Specifications</h2>
|
||||
<div class="specs-grid">
|
||||
<div class="spec-item"><span class="spec-label">Form Factor</span><span class="spec-value">${p.form_factor}</span></div>
|
||||
<div class="spec-item"><span class="spec-label">Speed</span><span class="spec-value">${p.speed} (${p.speed_gbps} Gbps)</span></div>
|
||||
<div class="spec-item"><span class="spec-label">Reach</span><span class="spec-value">${p.reach_label} (${p.reach_meters >= 1000 ? (p.reach_meters/1000) + ' km' : p.reach_meters + ' m'})</span></div>
|
||||
<div class="spec-item"><span class="spec-label">Fiber</span><span class="spec-value">${p.fiber_type || 'N/A'}</span></div>
|
||||
<div class="spec-item"><span class="spec-label">Connector</span><span class="spec-value">${p.connector || 'N/A'}</span></div>
|
||||
<div class="spec-item"><span class="spec-label">Wavelength</span><span class="spec-value">${p.wavelengths || 'N/A'}</span></div>
|
||||
${p.lanes ? `<div class="spec-item"><span class="spec-label">Lanes</span><span class="spec-value">${p.lanes}× ${p.lane_rate || ''}</span></div>` : ''}
|
||||
${p.modulation ? `<div class="spec-item"><span class="spec-label">Modulation</span><span class="spec-value">${p.modulation}</span></div>` : ''}
|
||||
<div class="spec-item"><span class="spec-label">Power</span><span class="spec-value">${p.power_consumption_w ? p.power_consumption_w + ' W' : 'N/A'}</span></div>
|
||||
<div class="spec-item"><span class="spec-label">Temperature</span><span class="spec-value">${p.temp_range === 'COM' ? '0°C to 70°C (Commercial)' : '-40°C to 85°C (Industrial)'}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${ds.optical ? `
|
||||
<div class="section">
|
||||
<h2>Power Budget</h2>
|
||||
<div class="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 |
|
||||
<strong class="${ds.optical.sufficient ? 'ok' : 'warn'}">Margin: ${ds.optical.margin_db} dB ${ds.optical.sufficient ? '✓' : '⚠ INSUFFICIENT'}</strong>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
${prices ? `
|
||||
<div class="section">
|
||||
<h2>Current Pricing</h2>
|
||||
<table><tr><th>Vendor</th><th>Price</th><th>Stock</th></tr>${prices}</table>
|
||||
</div>` : ''}
|
||||
|
||||
${switches ? `
|
||||
<div class="section">
|
||||
<h2>Compatible Switches (${ds.compatible_switches.length})</h2>
|
||||
<table><tr><th>Vendor</th><th>Model</th><th>Min Firmware</th></tr>${switches}</table>
|
||||
</div>` : ''}
|
||||
|
||||
<div class="section">
|
||||
<a class="cta" href="${ds.flexoptix.buy_url}">Buy at Flexoptix →</a>
|
||||
<p style="font-size:12px;color:#888;margin-top:8px">${ds.flexoptix.flexbox_note}</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>Generated by Transceiver Intelligence Platform (TIP) v0.2.0</span>
|
||||
<span>${ds.generated_at}</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user