317 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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.slug = $1 OR t.id::text = $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.model = $1 OR sw.id::text = $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>`;
}