diff --git a/packages/api/src/routes/transceivers.ts b/packages/api/src/routes/transceivers.ts
index 3cdd5c3..cf9a5ce 100644
--- a/packages/api/src/routes/transceivers.ts
+++ b/packages/api/src/routes/transceivers.ts
@@ -31,7 +31,7 @@ transceiverRouter.get("/", async (req: Request, res: Response) => {
}
});
-// GET /api/transceivers/:id — Get single transceiver
+// GET /api/transceivers/:id — Get single transceiver with latest prices per vendor
transceiverRouter.get("/:id", async (req: Request, res: Response) => {
try {
const transceiver = await getTransceiverById(String(req.params.id));
@@ -39,7 +39,36 @@ transceiverRouter.get("/:id", async (req: Request, res: Response) => {
res.status(404).json({ success: false, error: "Transceiver not found" });
return;
}
- res.json({ success: true, data: transceiver });
+
+ // Latest price per source vendor — last 30 days only
+ const pricesResult = await pool.query(
+ `SELECT DISTINCT ON (po.source_vendor_id)
+ po.price, po.currency, po.url, po.time, po.stock_level, po.is_verified,
+ v.name AS vendor_name, v.type AS vendor_type, v.website AS vendor_website
+ FROM price_observations po
+ JOIN vendors v ON po.source_vendor_id = v.id
+ WHERE po.transceiver_id = $1
+ AND po.time > NOW() - INTERVAL '30 days'
+ ORDER BY po.source_vendor_id, po.time DESC`,
+ [transceiver.id]
+ );
+
+ // Flag: price is only "verified" if also observed within 7 days
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
+ const prices = pricesResult.rows.map((row) => ({
+ vendor_name: row.vendor_name,
+ vendor_type: row.vendor_type,
+ vendor_website: row.vendor_website,
+ price: parseFloat(row.price),
+ currency: row.currency,
+ url: row.url,
+ stock_level: row.stock_level,
+ observed_at: row.time,
+ // Verified = scraped from real URL + observed within 7 days
+ is_verified: row.url && new Date(row.time) > sevenDaysAgo,
+ }));
+
+ res.json({ success: true, data: { ...transceiver, competitor_prices: prices } });
} catch (err) {
console.error("Get transceiver error:", err);
res.status(500).json({ success: false, error: "Internal server error" });
diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html
index aa57dc3..33cf28b 100644
--- a/packages/dashboard/index.html
+++ b/packages/dashboard/index.html
@@ -823,7 +823,7 @@
- | Name | Vendor | Form Factor | Speed | Reach | Price | Tier | Avail. | Category |
+ | Name | Vendor | Form Factor | Speed | Reach | Price | Tier | Avail. | Category | Verified |
@@ -959,6 +959,20 @@ function buildDOM(parent, html) {
parent.appendChild(t.content.cloneNode(true));
}
+// Temperature range code → human-readable display
+function tempRangeDisplay(code) {
+ if (!code) return null;
+ var map = { 'COM': '0 – 70 °C (COM)', 'IND': '-40 – 85 °C (IND)', 'EXT': '-40 – 70 °C (EXT)' };
+ return map[code] || code;
+}
+
+// Format observed date as "DD.MM.YYYY"
+function fmtDate(iso) {
+ if (!iso) return '';
+ var d = new Date(iso);
+ return d.getDate().toString().padStart(2,'0') + '.' + (d.getMonth()+1).toString().padStart(2,'0') + '.' + d.getFullYear();
+}
+
// Build a human-readable descriptive product name from available fields
function txDescName(t) {
// Use description field if populated and meaningful (not just the SKU)
@@ -1779,6 +1793,10 @@ function searchTransceivers() {
+ '
' + (t.price_tier ? '' + esc(t.price_tier) + '' : '—') + ' | '
+ '
' + (t.market_status ? '' + esc(t.market_status) + '' : '—') + ' | '
+ '
' + (t.category ? '' + esc(t.category) + '' : '') + ' | '
+ + '
' + (t.fully_verified
+ ? '★ 100%'
+ : t.price_verified ? '✓ Price' : '')
+ + ' | '
+ '';
}).join(''));
@@ -1818,9 +1836,15 @@ async function openTxDetail(id) {
// Title + Vendor badge
h += '
' + esc(t.standard_name || t.slug) + '
';
h += '
';
- if (t.vendor_name) h += '' + esc(t.vendor_name) + ' ';
- if (t.category) h += '' + esc(t.category) + ' ';
- if (t.market_status) h += '' + esc(t.market_status) + '';
+ if (t.vendor_name) h += '' + esc(t.vendor_name) + ' ';
+ if (t.category) h += '' + esc(t.category) + ' ';
+ if (t.market_status) {
+ var msTooltip = t.market_status === 'Emerging' ? 'Hype Cycle: Technologie gewinnt erste Akzeptanz – wachsendes Ökosystem, noch Premium-Preise'
+ : t.market_status === 'Mainstream' ? 'Hype Cycle: Technologie ist etabliert – breite Verfügbarkeit, kompetitive Preise'
+ : t.market_status === 'Legacy' ? 'Hype Cycle: Technologie veraltet – wird durch neuere Generationen abgelöst'
+ : 'Marktphase: ' + t.market_status;
+ h += '' + esc(t.market_status) + '';
+ }
h += '
';
if (t.description) h += '
' + esc(t.description) + '
';
@@ -1832,6 +1856,24 @@ async function openTxDetail(id) {
h += '
Fiber
' + esc(t.fiber_type || '—') + '
';
h += '
';
+ // Verification summary bar
+ var verItems = [];
+ if (t.price_verified) verItems.push('✓ Price');
+ if (t.image_verified) verItems.push('✓ Image');
+ if (t.details_verified) verItems.push('✓ Details');
+ if (t.fully_verified) {
+ h += ''
+ + '★ 100% VERIFIED'
+ + '–'
+ + verItems.join('·')
+ + (t.fully_verified_at ? 'seit ' + fmtDate(t.fully_verified_at) + '' : '')
+ + '
';
+ } else if (verItems.length > 0) {
+ h += ''
+ + verItems.join('·')
+ + '
';
+ }
+
// Helper: render a spec section as a clean table (like Flexoptix spec tables)
function renderSpecTable(title, rows) {
var visible = rows.filter(function(r) { return r[1] != null && r[1] !== '' && r[1] !== false; });
@@ -1856,7 +1898,7 @@ async function openTxDetail(id) {
['Tunable', t.tunable ? 'Yes' : null],
['ITU Grid', t.itu_grid],
['Coherent', t.coherent ? 'Yes' : null],
- ['Temperature Range', t.temp_range],
+ ['Temperature Range', tempRangeDisplay(t.temp_range)],
]);
// SPECIFICATION — Performance
@@ -1875,9 +1917,9 @@ async function openTxDetail(id) {
// SPECIFICATION — Optical Budget
h += renderSpecTable('Optical Budget', [
['Power Budget', t.optical_budget_db ? t.optical_budget_db + ' dB' : null],
- ['Tx Power Min', t.tx_power_min_dbm ? t.tx_power_min_dbm + ' dBm' : null],
- ['Tx Power Max', t.tx_power_max_dbm ? t.tx_power_max_dbm + ' dBm' : null],
- ['Rx Sensitivity', t.rx_sensitivity_dbm ? t.rx_sensitivity_dbm + ' dBm' : null],
+ ['TX Power (Min)', t.tx_power_min_dbm != null ? t.tx_power_min_dbm + ' dBm' : null],
+ ['TX Power (Max)', t.tx_power_max_dbm != null ? t.tx_power_max_dbm + ' dBm' : null],
+ ['RX Sensitivity', t.rx_sensitivity_dbm != null ? t.rx_sensitivity_dbm + ' dBm' : null],
]);
// SPECIFICATION — Breakout
@@ -1899,12 +1941,30 @@ async function openTxDetail(id) {
['Year Mainstream', t.year_mainstream],
]);
- // SPECIFICATION — Pricing
- h += renderSpecTable('Pricing', [
- ['MSRP', t.msrp_usd ? '$' + parseFloat(t.msrp_usd).toLocaleString() : null],
- ['Street Price', t.street_price_usd ? '$' + parseFloat(t.street_price_usd).toLocaleString() : null],
- ['Price Tier', t.price_tier],
- ]);
+ // SPECIFICATION — Pricing (verified prices from DB)
+ var prices = t.competitor_prices || [];
+ if (prices.length > 0) {
+ h += 'Current Prices
';
+ h += '';
+ prices.forEach(function(p) {
+ var verBadge = p.is_verified
+ ? '
✓ Verified'
+ : '
unverified';
+ var priceStr = p.currency + ' ' + parseFloat(p.price).toLocaleString('de-DE', {minimumFractionDigits:2,maximumFractionDigits:2});
+ var dateStr = '
' + fmtDate(p.observed_at) + '';
+ var urlLink = p.url ? '
↗' : '';
+ h += '
' + esc(p.vendor_name) + ''
+ + '' + priceStr + verBadge + dateStr + urlLink + '
';
+ });
+ h += '
';
+ } else {
+ // Fallback: show MSRP/street from transceivers table if no price_observations
+ h += renderSpecTable('Pricing', [
+ ['MSRP', t.msrp_usd ? '$' + parseFloat(t.msrp_usd).toLocaleString() : null],
+ ['Street Price', t.street_price_usd ? '$' + parseFloat(t.street_price_usd).toLocaleString() : null],
+ ['Price Tier', t.price_tier],
+ ]);
+ }
// Notes (scraped extra specs)
if (t.notes) {