From 3811b3b9532e2d37ede7a01705a7dace47959528 Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Wed, 1 Apr 2026 20:47:02 +0200 Subject: [PATCH] feat: temp range display, verification badges, competitor prices, tag tooltips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Temperature Range: COM→'0-70°C (COM)', IND→'-40-85°C (IND)' - GET /api/transceivers/:id: returns competitor_prices[] from price_observations - Detail view: verification summary bar (★ 100% VERIFIED / partial) - Detail view: Current Prices section with vendor, price, verified badge, date, link - Detail view: tag tooltips on vendor/category/market_status chips - List view: new Verified column with 100% stamp or price check - Optical Budget: TX Power Min/Max labels clarified --- packages/api/src/routes/transceivers.ts | 33 +++++++++- packages/dashboard/index.html | 88 +++++++++++++++++++++---- 2 files changed, 105 insertions(+), 16 deletions(-) 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 @@
- +
NameVendorForm FactorSpeedReachPriceTierAvail.Category
NameVendorForm FactorSpeedReachPriceTierAvail.CategoryVerified
@@ -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) {