diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index ef2a2ad..00ca9a6 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,6 +1,7 @@ # TIP Changelog Format: `{"d":"YYYY-MM-DD","t":"TYPE","m":"Description"}` +{"d":"2026-05-14","t":"FEAT","m":"Procurement: 5 neue Intelligence-Sektionen. (E) 🟒 Buy-Now Intel β€” Top buy_now Reorder Signals aus 211k preberechneten Signalen, filterbar nach Form Factor, SignalstΓ€rke-Balken, Preis/Stock-Trend, GrΓΌnde als Tooltip. API: GET /api/procurement/reorder-top. (A) πŸ’° Arbitrage β€” FX-Preis vs. Competitor-Preis fΓΌr 59k Equivalenz-Paare mit Preisdaten auf beiden Seiten, normalisiert auf USD (EURΓ—1.08, GBPΓ—1.27), sortiert nach Ersparnis-%. API: GET /api/procurement/arbitrage. (B) πŸ–₯ Switch Compat β€” Suche nach Switch-Modell (Cisco, Juniper, Arista etc.), zeigt alle kompatiblen Transceiver mit Preis + Verifikationsmethode. 58k Compatibility-Rows, 429 Switches. API: GET /api/procurement/switch-compat?search=. (C) ⚠️ Supply Squeeze β€” Multi-Signal-Detektor: 4 parallele Quellen (Preis-Momentum 30d vs 60d, Hype-Phase, AI-Cluster-Transceiver-Nachfrage, Stock-Level-Verteilung). Severity: critical/warning/watch. API: GET /api/procurement/supply-squeeze. (D) πŸͺ¦ Dead Stock Revival β€” 7.297 Dead-Stock-SKUs gegen Hype-Cycle-Phasen: zeigt welche LagerhΓΌter in Technologieklassen liegen die gerade aufsteigen (ascending hype phases, score >30). API: GET /api/procurement/dead-stock-revival."} {"d":"2026-05-14","t":"FEAT","m":"Crawler Intelligence: Data Quality panel. New GET /api/scrapers/data-quality endpoint β€” 4 parallel queries over 200,617 transceiver_verification_evidence rows: (1) coverage breakdown (price 11,366/18,146 = 62%, image 12,333/68%, details 17,085/94%, competitor_match 399/2%, quarantined 1,193); (2) all 10 evidence types with count + avg confidence + product count + last seen; (3) robot/scraper contributions table (17 robots ranked by output); (4) daily activity last 14 days. Dashboard Crawler Intelligence tab: new πŸ”¬ Data Quality section with coverage progress bars (color-coded β‰₯80% green / β‰₯50% amber / red), evidence type table, SVG sparkline bar chart for 14-day activity, robot contributions table with live/stale dot indicators."} {"d":"2026-05-14","t":"FEAT","m":"Dynamic Hype Cycle + Market Signal Engine: Hype Cycle tab is now fully data-driven. New GET /api/hype-cycle/market-signals endpoint blends 6 real data sources into a composite Market Signal Score (0–100) per technology: (1) hype_score from Norton-Bass model (30% weight), (2) hyperscaler CapEx YoY avg (Microsoft +68.8%, Alphabet +107.4%, Meta +46.8%), (3) price observation activity ratio 30d vs prior 30d, (4) AI cluster estimated transceiver demand (90d window), (5) eBay secondary market sell-through velocity, (6) internal fast-mover demand trend. Score thresholds: β‰₯70 green, β‰₯50 yellow, β‰₯30 orange, <30 gray. Recommendation engine: buildRecommendation(phase, signalScore, capexYoyAvg, speedGbps) maps hype phase Γ— capex boom Γ— speed class β†’ Buy/Hold/Watch label with color + detail tooltip. Dashboard: Hype Cycle table shows Market Signal ● LIVE column (score + progress bar) + Recommendation column (emoji label, tooltip with reasoning). Market Context cards row above table shows Top Signal, CapEx Boom %, Fast Movers signal, eBay Velocity. New Hyperscaler CapEx panel (SEC filing data) + eBay Secondary Market panel at bottom of hype tab. Procurement: new πŸ›’ eBay Market sub-section with per-form-factor sell-through grid. All 6 queries run in parallel via Promise.all()."} {"d":"2026-05-14","t":"FEAT","m":"Procurement tab: 2 new sections with real data. (1) πŸ“¦ Internal Demand β€” Flexoptix internal SKU velocity from flexoptix_internal_demand table (8,585 SKUs: 70 fast-movers 53k units/12M, 239 regular, 979 slow, 7,297 dead stock). Summary cards with trend %%. Filter by velocity class. API: GET /api/procurement/internal-demand?velocity_class=&limit=&sort=. (2) πŸ€– AI Clusters β€” live AI datacenter announcements from ai_cluster_announcements table (396 in last 30 days). Shows estimated transceiver demand per build, MW scale, company, location, source link. Filter for entries with transceiver estimates. Stats: total announcements, MW, distinct companies, total estimated transceivers. API: GET /api/procurement/ai-clusters?days=&limit=. Replaced misleading DEMO DATA banners on Signals + ABC sections with informational note pointing to Internal Demand data."} diff --git a/packages/api/src/routes/procurement.ts b/packages/api/src/routes/procurement.ts index fa2c716..442970f 100644 --- a/packages/api/src/routes/procurement.ts +++ b/packages/api/src/routes/procurement.ts @@ -500,3 +500,375 @@ procurementRouter.get("/marketplace-velocity", async (_req: Request, res: Respon res.status(500).json({ error: "Internal server error" }); } }); + +// ─── E: GET /api/procurement/reorder-top ───────────────────────────────────── +// Top buy_now reorder signals with full reasons β€” 211k precomputed signals +procurementRouter.get("/reorder-top", async (req: Request, res: Response) => { + const limit = Math.min(parseInt(req.query.limit as string) || 50, 200); + const formFactor = (req.query.form_factor as string) || ""; + const minStrength = parseFloat(req.query.min_strength as string) || 0; + + try { + const result = await pool.query(` + SELECT DISTINCT ON (t.id) + t.id, t.part_number, t.speed_gbps, t.form_factor, t.reach_label, + v.name AS vendor_name, + rs.signal, rs.signal_strength, + rs.price_trend, rs.stock_trend, rs.hype_phase, + rs.reasons, + rs.computed_at + FROM reorder_signals rs + JOIN transceivers t ON t.id = rs.transceiver_id + JOIN vendors v ON v.id = t.vendor_id + WHERE rs.signal = 'buy_now' + AND rs.is_demo_data = false + AND rs.signal_strength >= $1 + AND ($2 = '' OR t.form_factor ILIKE $2) + ORDER BY t.id, rs.signal_strength DESC, rs.computed_at DESC + `, [minStrength, formFactor]); + + // After DISTINCT ON, re-sort by signal_strength + const rows = result.rows.sort( + (a: { signal_strength: string }, b: { signal_strength: string }) => + parseFloat(b.signal_strength) - parseFloat(a.signal_strength) + ); + + const summary = await pool.query(` + SELECT + COUNT(*) FILTER (WHERE signal = 'buy_now' AND is_demo_data = false)::int AS buy_now, + COUNT(*) FILTER (WHERE signal = 'wait' AND is_demo_data = false)::int AS wait, + COUNT(*) FILTER (WHERE signal = 'hold' AND is_demo_data = false)::int AS hold, + COUNT(*) FILTER (WHERE signal = 'monitor' AND is_demo_data = false)::int AS monitor, + ROUND(AVG(signal_strength) FILTER (WHERE signal = 'buy_now' AND is_demo_data = false)::numeric,3) AS avg_buy_strength + FROM reorder_signals + `); + + res.json({ success: true, data: rows.slice(0, limit), summary: summary.rows[0] }); + } catch (err) { + res.status(500).json({ error: String(err) }); + } +}); + +// ─── B: GET /api/procurement/switch-compat ─────────────────────────────────── +// Switch ↔ transceiver compatibility matrix +procurementRouter.get("/switch-compat", async (req: Request, res: Response) => { + const search = (req.query.search as string) || ""; + const limitNum = Math.min(parseInt(req.query.limit as string) || 30, 100); + + try { + if (search.length >= 2) { + // Search for switches matching query, return their compatible transceivers + const switches = await pool.query(` + SELECT DISTINCT ON (sw.id) + sw.id, sw.vendor AS sw_vendor, sw.model AS sw_model, sw.series AS sw_series, + COUNT(c.transceiver_id) OVER (PARTITION BY sw.id)::int AS compat_count + FROM switches sw + JOIN compatibility c ON c.switch_id = sw.id + WHERE sw.model ILIKE $1 OR sw.vendor ILIKE $1 OR sw.series ILIKE $1 + ORDER BY sw.id, compat_count DESC + LIMIT $2 + `, [`%${search}%`, limitNum]); + + // For each matched switch, get top compatible transceivers with prices + const switchIds = switches.rows.map((s: { id: string }) => s.id); + if (switchIds.length === 0) { + return res.json({ success: true, switches: [], transceivers: [] }); + } + + const transceivers = await pool.query(` + SELECT + c.switch_id, + t.id AS tx_id, t.part_number, t.speed_gbps, t.form_factor, t.reach_label, + v.name AS vendor_name, + c.verification_method, c.status, + (SELECT ROUND(MIN(po.price)::numeric,2) FROM price_observations po + WHERE po.transceiver_id = t.id AND po.price > 0 + ORDER BY po.time DESC LIMIT 1) AS min_price, + (SELECT po.currency FROM price_observations po + WHERE po.transceiver_id = t.id AND po.price > 0 + ORDER BY po.time DESC LIMIT 1) AS currency + FROM compatibility c + JOIN transceivers t ON t.id = c.transceiver_id + JOIN vendors v ON v.id = t.vendor_id + WHERE c.switch_id = ANY($1) + AND c.status = 'compatible' + ORDER BY t.speed_gbps DESC, t.form_factor + LIMIT 200 + `, [switchIds]); + + return res.json({ + success: true, + switches: switches.rows, + transceivers: transceivers.rows, + }); + } + + // No search β€” return top switches by compat count + const top = await pool.query(` + SELECT sw.vendor, sw.model, sw.series, + COUNT(c.transceiver_id)::int AS compat_count + FROM switches sw + JOIN compatibility c ON c.switch_id = sw.id + WHERE c.status = 'compatible' + GROUP BY sw.id, sw.vendor, sw.model, sw.series + ORDER BY compat_count DESC + LIMIT $1 + `, [limitNum]); + + const stats = await pool.query(` + SELECT + COUNT(DISTINCT sw.id)::int AS total_switches, + COUNT(DISTINCT c.transceiver_id)::int AS total_transceivers, + COUNT(*)::int AS total_compat_rows + FROM switches sw JOIN compatibility c ON c.switch_id = sw.id + WHERE c.status = 'compatible' + `); + + return res.json({ success: true, topSwitches: top.rows, stats: stats.rows[0] }); + } catch (err) { + res.status(500).json({ error: String(err) }); + } +}); + +// ─── A: GET /api/procurement/arbitrage ─────────────────────────────────────── +// OEM vs Flexoptix price gaps via transceiver_equivalences +procurementRouter.get("/arbitrage", async (_req: Request, res: Response) => { + // FX rates for normalization β€” approximate + const FX: Record = { USD: 1.0, EUR: 1.08, GBP: 1.27 }; + + try { + const result = await pool.query(` + SELECT + te.confidence, + fx.part_number AS fx_part, + vfx.name AS fx_vendor, + fx.speed_gbps, fx.form_factor, fx.reach_label, + comp.part_number AS comp_part, + vcomp.name AS comp_vendor, + (SELECT price FROM price_observations WHERE transceiver_id = te.flexoptix_id AND price > 2 ORDER BY time DESC LIMIT 1) AS fx_price, + (SELECT currency FROM price_observations WHERE transceiver_id = te.flexoptix_id AND price > 2 ORDER BY time DESC LIMIT 1) AS fx_curr, + (SELECT price FROM price_observations WHERE transceiver_id = te.competitor_id AND price > 2 ORDER BY time DESC LIMIT 1) AS comp_price, + (SELECT currency FROM price_observations WHERE transceiver_id = te.competitor_id AND price > 2 ORDER BY time DESC LIMIT 1) AS comp_curr + FROM transceiver_equivalences te + JOIN transceivers fx ON fx.id = te.flexoptix_id + JOIN transceivers comp ON comp.id = te.competitor_id + JOIN vendors vfx ON vfx.id = fx.vendor_id + JOIN vendors vcomp ON vcomp.id = comp.vendor_id + WHERE te.status IN ('approved','auto_approved') + AND EXISTS(SELECT 1 FROM price_observations WHERE transceiver_id = te.flexoptix_id AND price > 2) + AND EXISTS(SELECT 1 FROM price_observations WHERE transceiver_id = te.competitor_id AND price > 2) + ORDER BY te.confidence DESC + LIMIT 2000 + `); + + const pairs = result.rows + .map((r: { + fx_price: string; fx_curr: string; + comp_price: string; comp_curr: string; + confidence: string; + fx_part: string; fx_vendor: string; + comp_part: string; comp_vendor: string; + speed_gbps: string; form_factor: string; reach_label: string; + }) => { + const fxUSD = parseFloat(r.fx_price) * (FX[r.fx_curr] || 1.0); + const compUSD = parseFloat(r.comp_price) * (FX[r.comp_curr] || 1.0); + if (!fxUSD || !compUSD) return null; + const savings = compUSD - fxUSD; + const savingsPct = Math.round((savings / compUSD) * 100); + return { ...r, fxUSD: Math.round(fxUSD), compUSD: Math.round(compUSD), savings: Math.round(savings), savingsPct }; + }) + .filter((r): r is NonNullable => r !== null && r.savings > 0) + .sort((a, b) => b.savingsPct - a.savingsPct) + .slice(0, 100); + + // Stats + const totalPairs = result.rows.length; + const fxCheaper = pairs.length; + const avgSavings = pairs.length ? Math.round(pairs.reduce((s, r) => s + r.savingsPct, 0) / pairs.length) : 0; + + res.json({ success: true, pairs, stats: { totalPairs, fxCheaper, avgSavingsPct: avgSavings } }); + } catch (err) { + res.status(500).json({ error: String(err) }); + } +}); + +// ─── D: GET /api/procurement/dead-stock-revival ────────────────────────────── +// Dead-stock SKUs whose equivalents are in rising hype phases +procurementRouter.get("/dead-stock-revival", async (_req: Request, res: Response) => { + try { + const [deadStock, hypeMap] = await Promise.all([ + pool.query(` + SELECT + fid.transceiver_id, + fid.part_number_raw AS part_number, + fid.velocity_class, + fid.demand_12m, + fid.demand_trend_pct, + t.speed_gbps, t.form_factor, t.reach_label, + v.name AS vendor_name + FROM flexoptix_internal_demand fid + JOIN transceivers t ON t.id = fid.transceiver_id + JOIN vendors v ON v.id = t.vendor_id + WHERE fid.velocity_class = 'dead_stock' + AND fid.is_internal = true + LIMIT 7500 + `), + pool.query(` + SELECT DISTINCT ON (technology) + technology, hype_phase, hype_score, computed_at + FROM hype_cycle_analysis + ORDER BY technology, computed_at DESC + `), + ]); + + // Build speed β†’ hype phase map + type HypeRow = { technology: string; hype_phase: string; hype_score: string }; + const ASCENDING = new Set(["innovation_trigger","peak_inflated_expectations","slope_enlightenment","plateau_productivity"]); + const speedToHype = new Map(); + for (const h of hypeMap.rows as HypeRow[]) { + const speedMatch = h.technology.match(/^(\d+(?:\.\d+)?)G/); + if (speedMatch) speedToHype.set(parseFloat(speedMatch[1]), h); + } + + type DeadRow = { + transceiver_id: string; part_number: string; + speed_gbps: string; form_factor: string; reach_label: string; + vendor_name: string; demand_12m: string; demand_trend_pct: string; + velocity_class: string; + }; + + const revivals = (deadStock.rows as DeadRow[]) + .map((r) => { + const speed = parseFloat(r.speed_gbps); + const hype = speedToHype.get(speed); + if (!hype) return null; + const ascending = ASCENDING.has(hype.hype_phase); + const score = parseFloat(hype.hype_score); + return { ...r, hype_phase: hype.hype_phase, hype_score: score, ascending }; + }) + .filter((r): r is NonNullable => r !== null && r.ascending && r.hype_score > 30) + .sort((a, b) => b.hype_score - a.hype_score) + .slice(0, 100); + + const totalDead = deadStock.rows.length; + res.json({ success: true, revivals, totalDeadStock: totalDead, revivalCount: revivals.length }); + } catch (err) { + res.status(500).json({ error: String(err) }); + } +}); + +// ─── C: GET /api/procurement/supply-squeeze ────────────────────────────────── +// Multi-signal supply constraint detector +procurementRouter.get("/supply-squeeze", async (_req: Request, res: Response) => { + try { + const [priceSignals, aiDemand, hypeData, stockData] = await Promise.all([ + // Price momentum: 30d vs 60d avg by speed/form_factor + pool.query(` + SELECT + t.speed_gbps, t.form_factor, + ROUND(AVG(po.price) FILTER (WHERE po.time >= NOW() - INTERVAL '30 days')::numeric,2) AS avg_30d, + ROUND(AVG(po.price) FILTER (WHERE po.time >= NOW() - INTERVAL '60 days' AND po.time < NOW() - INTERVAL '30 days')::numeric,2) AS avg_prior_30d, + COUNT(*) FILTER (WHERE po.time >= NOW() - INTERVAL '30 days') AS obs_30d + FROM price_observations po + JOIN transceivers t ON t.id = po.transceiver_id + WHERE po.price > 5 AND po.currency = 'USD' + GROUP BY t.speed_gbps, t.form_factor + HAVING COUNT(*) FILTER (WHERE po.time >= NOW() - INTERVAL '30 days') >= 3 + `), + // AI cluster demand by speed tier + pool.query(` + SELECT + CASE + WHEN description ILIKE '%800G%' THEN 800 + WHEN description ILIKE '%400G%' THEN 400 + WHEN description ILIKE '%100G%' THEN 100 + ELSE 0 + END AS speed_tier, + COALESCE(SUM(estimated_transceivers),0)::int AS total_tx, + COUNT(*)::int AS cluster_count + FROM ai_cluster_announcements + WHERE announced_date >= NOW() - INTERVAL '90 days' + GROUP BY speed_tier + HAVING COALESCE(SUM(estimated_transceivers),0) > 0 + `), + // Hype phase per technology + pool.query(` + SELECT DISTINCT ON (technology) + technology, hype_phase, hype_score + FROM hype_cycle_analysis ORDER BY technology, computed_at DESC + `), + // Stock level distribution (in_stock vs out_of_stock) + pool.query(` + SELECT + t.speed_gbps, t.form_factor, + COUNT(*) FILTER (WHERE so.stock_level = 'out_of_stock')::int AS out_of_stock, + COUNT(*) FILTER (WHERE so.stock_level = 'in_stock')::int AS in_stock, + COUNT(*)::int AS total_obs + FROM stock_observations so + JOIN transceivers t ON t.id = so.transceiver_id + WHERE so.observed_at >= NOW() - INTERVAL '14 days' + GROUP BY t.speed_gbps, t.form_factor + HAVING COUNT(*) >= 3 + `).catch(() => ({ rows: [] })), + ]); + + type PriceRow = { speed_gbps: string; form_factor: string; avg_30d: string; avg_prior_30d: string; obs_30d: string }; + type HypeRow = { technology: string; hype_phase: string; hype_score: string }; + type AiRow = { speed_tier: string; total_tx: string; cluster_count: string }; + type StockRow = { speed_gbps: string; form_factor: string; out_of_stock: string; in_stock: string; total_obs: string }; + + const speedToHype = new Map(); + for (const h of hypeData.rows as HypeRow[]) { + const m = h.technology.match(/^(\d+(?:\.\d+)?)G/); + if (m) speedToHype.set(parseFloat(m[1]), h); + } + + const aiBySpeed = new Map(); + for (const a of aiDemand.rows as AiRow[]) { + aiBySpeed.set(parseFloat(a.speed_tier), a); + } + + const stockByKey = new Map(); + for (const s of stockData.rows as StockRow[]) { + stockByKey.set(`${s.speed_gbps}:${s.form_factor}`, s); + } + + const RISKY_PHASES = new Set(["peak_inflated_expectations","slope_enlightenment","plateau_productivity"]); + + const signals = (priceSignals.rows as PriceRow[]) + .map((r) => { + const speed = parseFloat(r.speed_gbps); + const priceUp = r.avg_30d && r.avg_prior_30d + ? ((parseFloat(r.avg_30d) - parseFloat(r.avg_prior_30d)) / parseFloat(r.avg_prior_30d)) * 100 + : 0; + const hype = speedToHype.get(speed); + const ai = aiBySpeed.get(speed); + const stock = stockByKey.get(`${r.speed_gbps}:${r.form_factor}`); + + let activeSignals = 0; + const reasons: string[] = []; + + if (priceUp > 5) { activeSignals++; reasons.push(`Price +${Math.round(priceUp)}% (30d)`); } + if (hype && RISKY_PHASES.has(hype.hype_phase)) { activeSignals++; reasons.push(`Hype: ${hype.hype_phase.replace(/_/g,' ')}`); } + if (ai && parseInt(ai.total_tx) > 50000) { activeSignals++; reasons.push(`AI demand: ${parseInt(ai.total_tx).toLocaleString()} tx in 90d`); } + if (stock && parseInt(stock.out_of_stock) > parseInt(stock.in_stock)) { activeSignals++; reasons.push(`Stock pressure: ${stock.out_of_stock}/${stock.total_obs} vendors OOS`); } + + const severity = activeSignals >= 3 ? "critical" : activeSignals === 2 ? "warning" : activeSignals === 1 ? "watch" : "ok"; + return { + speed_gbps: r.speed_gbps, form_factor: r.form_factor, + avg_30d: r.avg_30d, avg_prior_30d: r.avg_prior_30d, + price_momentum_pct: Math.round(priceUp), + hype_phase: hype?.hype_phase || null, + hype_score: hype ? parseFloat(hype.hype_score) : null, + ai_demand_tx: ai ? parseInt(ai.total_tx) : 0, + activeSignals, severity, reasons, + }; + }) + .filter((r) => r.activeSignals >= 1) + .sort((a, b) => b.activeSignals - a.activeSignals || b.price_momentum_pct - a.price_momentum_pct); + + res.json({ success: true, signals, criticalCount: signals.filter(s => s.severity === "critical").length }); + } catch (err) { + res.status(500).json({ error: String(err) }); + } +}); diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html index 768703a..006779b 100644 --- a/packages/dashboard/index.html +++ b/packages/dashboard/index.html @@ -1594,6 +1594,11 @@
+ + + + + @@ -1604,6 +1609,54 @@
+ + + + + + + + + + + + + + +
β„Ή Reorder Signals basieren auf ABC-Klassifizierung + Preis-Observations-Frequenz. Echte Verkaufsmengendaten β†’ Tab.
@@ -6863,7 +6916,8 @@ var procAiClustersData = []; var procAiClustersMinTx = 0; function showProcSection(name) { - ['signals','abc','demand','marketplace','ai-clusters','market','lifecycle'].forEach(function(s) { + ['signals','reorder-top','arbitrage','switch-compat','supply-squeeze','dead-stock', + 'abc','demand','marketplace','ai-clusters','market','lifecycle'].forEach(function(s) { var sec = el('proc-section-' + s); var btn = el('proc-btn-' + s); if (sec) sec.style.display = s === name ? '' : 'none'; @@ -6873,6 +6927,286 @@ function showProcSection(name) { if (name === 'demand' && procDemandData.length === 0) loadInternalDemand(); if (name === 'ai-clusters' && procAiClustersData.length === 0) loadAiClusters(); if (name === 'marketplace' && !el('proc-marketplace-grid').querySelector('.card')) loadProcMarketplace(); + if (name === 'reorder-top' && !el('proc-reorder-top-list').querySelector('div.card,table')) loadReorderTop(); + if (name === 'arbitrage' && !el('proc-arbitrage-list').querySelector('table')) loadArbitrage(); + if (name === 'switch-compat' && !el('proc-switch-stats').innerHTML) loadSwitchCompatStats(); + if (name === 'supply-squeeze' && !el('proc-squeeze-list').querySelector('div.card,table')) loadSupplySqueeze(); + if (name === 'dead-stock' && !el('proc-deadstock-list').querySelector('table')) loadDeadStockRevival(); +} + +/* ── E: Buy-Now Reorder Intelligence ───────────────────────────────────── */ +async function loadReorderTop() { + var token = (window.loadToken ? window.loadToken() : '') || ''; + var ff = (el('reorder-ff-filter') || {}).value || ''; + var listEl = el('proc-reorder-top-list'); + var summEl = el('proc-reorder-top-summary'); + if (listEl) listEl.innerHTML = '
Loading…
'; + try { + var r = await fetch('/api/procurement/reorder-top?limit=60&form_factor=' + encodeURIComponent(ff), { headers: { 'Authorization': 'Bearer ' + token } }); + var d = await r.json(); + if (!d.success) throw new Error(d.error || 'err'); + // Summary cards + var sm = d.summary || {}; + if (summEl) summEl.innerHTML = [ + { label: '🟒 Buy Now', val: (sm.buy_now||0).toLocaleString(), color: '#22c55e' }, + { label: '⏳ Wait', val: (sm.wait||0).toLocaleString(), color: '#f59e0b' }, + { label: '⏸ Hold', val: (sm.hold||0).toLocaleString(), color: '#64748b' }, + { label: 'πŸ‘ Monitor', val: (sm.monitor||0).toLocaleString(), color: '#94a3b8' }, + { label: 'Ø Buy Strength', val: sm.avg_buy_strength ? Math.round(parseFloat(sm.avg_buy_strength)*100)+'%' : 'β€”', color: '#22c55e' }, + ].map(function(c) { + return '
' + + '
'+esc(c.label)+'
' + + '
'+esc(c.val)+'
' + + '
'; + }).join(''); + // Table + if (!d.data || !d.data.length) { listEl.innerHTML = '
Keine Buy-Now-Signale gefunden.
'; return; } + var rows = d.data.map(function(r) { + var str = Math.round(parseFloat(r.signal_strength)*100); + var strColor = str >= 70 ? '#22c55e' : str >= 50 ? '#f59e0b' : '#94a3b8'; + var reasons = Array.isArray(r.reasons) ? r.reasons.join(' Β· ') : (r.reasons || 'β€”'); + var pt = r.price_trend === 'rising' ? 'πŸ“ˆ' : r.price_trend === 'falling' ? 'πŸ“‰' : 'β†’'; + var st = r.stock_trend === 'declining' ? 'πŸ“‰' : r.stock_trend === 'increasing' ? 'πŸ“ˆ' : 'β†’'; + return '' + + ''+esc(r.vendor_name||'')+'' + + ''+esc(r.part_number||'')+'' + + ''+esc(r.form_factor||'')+'' + + ''+esc(String(r.speed_gbps||''))+'G' + + ''+str+'%' + + ''+pt+' Preis Β· '+st+' Stock' + + ''+esc(reasons.substring(0,80))+'' + + ''; + }).join(''); + listEl.innerHTML = '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + rows + '
VendorPartFFSpeedStΓ€rkeTrendsGrund
'; + } catch(e) { + if (listEl) listEl.innerHTML = '
'+esc(e.message)+'
'; + } +} +function reloadReorderTop() { loadReorderTop(); } + +/* ── A: Arbitrage ──────────────────────────────────────────────────────── */ +async function loadArbitrage() { + var token = (window.loadToken ? window.loadToken() : '') || ''; + var listEl = el('proc-arbitrage-list'); + var statsEl = el('proc-arbitrage-stats'); + if (listEl) listEl.innerHTML = '
Berechne Preisvergleiche aus 63k Equivalenz-Paaren…
'; + try { + var r = await fetch('/api/procurement/arbitrage', { headers: { 'Authorization': 'Bearer ' + token } }); + var d = await r.json(); + if (!d.success) throw new Error(d.error || 'err'); + var st = d.stats || {}; + if (statsEl) statsEl.innerHTML = [ + { label: 'Paare mit Preisdaten', val: (st.totalPairs||0).toLocaleString(), color: '#3b82f6' }, + { label: 'FX günstiger', val: (st.fxCheaper||0).toLocaleString(), color: '#22c55e' }, + { label: 'Ø Ersparnis', val: (st.avgSavingsPct||0)+'%', color: '#22c55e' }, + ].map(function(c) { + return '
' + + '
'+esc(c.label)+'
' + + '
'+esc(c.val)+'
'; + }).join(''); + if (!d.pairs || !d.pairs.length) { listEl.innerHTML = '
Keine Arbitrage-Paare gefunden.
'; return; } + var rows = d.pairs.map(function(p) { + var savColor = p.savingsPct >= 50 ? '#22c55e' : p.savingsPct >= 20 ? '#f59e0b' : '#94a3b8'; + return '' + + ''+esc(p.fx_vendor)+'' + + ''+esc(p.fx_part||'')+'' + + ''+esc(p.comp_vendor)+'' + + ''+esc(p.comp_part||'')+'' + + ''+esc(p.form_factor)+'' + + '$'+esc(String(p.fxUSD))+' ('+esc(p.fx_curr)+')' + + '$'+esc(String(p.compUSD))+' ('+esc(p.comp_curr)+')' + + ''+p.savingsPct+'%' + + ''; + }).join(''); + listEl.innerHTML = '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + rows + '
FX VendorFX PartCompetitorComp PartFFFX (USD)Comp (USD)GΓΌnstiger
'; + } catch(e) { + if (listEl) listEl.innerHTML = '
'+esc(e.message)+'
'; + } +} + +/* ── B: Switch Compatibility ───────────────────────────────────────────── */ +async function loadSwitchCompatStats() { + var token = (window.loadToken ? window.loadToken() : '') || ''; + var statsEl = el('proc-switch-stats'); + try { + var r = await fetch('/api/procurement/switch-compat', { headers: { 'Authorization': 'Bearer ' + token } }); + var d = await r.json(); + if (!d.success) throw new Error(d.error); + var st = d.stats || {}; + if (statsEl) statsEl.innerHTML = '
' + + [ + { label: 'Switches in DB', val: (st.total_switches||0).toLocaleString(), color: '#6366f1' }, + { label: 'Kompatible Transceiver', val: (st.total_transceivers||0).toLocaleString(), color: '#3b82f6' }, + { label: 'Compat-EintrΓ€ge', val: (st.total_compat_rows||0).toLocaleString(), color: '#22c55e' }, + ].map(function(c) { + return '
' + + '
'+esc(c.label)+'
' + + '
'+esc(c.val)+'
'; + }).join('') + '
' + + '
Top Switches nach Compat-Anzahl: ' + + (d.topSwitches||[]).slice(0,6).map(function(s) { + return ''+esc(s.sw_vendor||s.vendor||'')+ ' '+esc(s.sw_model||s.model||'')+''; + }).join('') + '
'; + } catch(e) {} +} +async function loadSwitchCompat() { + var token = (window.loadToken ? window.loadToken() : '') || ''; + var query = (el('switch-search-input') || {}).value || ''; + var resultsEl = el('proc-switch-results'); + if (!query.trim()) { if (resultsEl) resultsEl.innerHTML = '
Switch-Modell eingeben.
'; return; } + if (resultsEl) resultsEl.innerHTML = '
Suche…
'; + try { + var r = await fetch('/api/procurement/switch-compat?search='+encodeURIComponent(query)+'&limit=20', { headers: { 'Authorization': 'Bearer ' + token } }); + var d = await r.json(); + if (!d.success) throw new Error(d.error); + var switches = d.switches || []; + var txList = d.transceivers || []; + if (!switches.length) { resultsEl.innerHTML = '
Kein Switch gefunden fΓΌr "'+esc(query)+'".
'; return; } + var html = switches.map(function(sw) { + var txForSw = txList.filter(function(t) { return t.switch_id === sw.id; }); + var txRows = txForSw.map(function(t) { + var priceStr = t.min_price ? t.currency+' '+t.min_price : 'β€”'; + return '' + + ''+esc(t.vendor_name)+'' + + ''+esc(t.part_number||'')+'' + + ''+esc(t.form_factor)+'' + + ''+esc(String(t.speed_gbps))+'G' + + ''+esc(priceStr)+'' + + ''+esc(t.verification_method||'')+'' + + ''; + }).join(''); + return '
' + + '
' + + ''+esc((sw.sw_vendor||sw.vendor||'')+ ' '+( sw.sw_model||sw.model||''))+'' + + (sw.sw_series||sw.series ? ''+esc(sw.sw_series||sw.series)+'' : '') + + ''+esc(String(sw.compat_count||txForSw.length))+' kompatibel' + + '
' + + (txForSw.length ? '' + + txRows + '
VendorPartFFSpeedPreisMethode
' + : '
Keine Transceiver-Preise verfΓΌgbar.
') + + '
'; + }).join(''); + resultsEl.innerHTML = html; + } catch(e) { + resultsEl.innerHTML = '
'+esc(e.message)+'
'; + } +} + +/* ── C: Supply Squeeze Detector ────────────────────────────────────────── */ +async function loadSupplySqueeze() { + var token = (window.loadToken ? window.loadToken() : '') || ''; + var summEl = el('proc-squeeze-summary'); + var listEl = el('proc-squeeze-list'); + if (listEl) listEl.innerHTML = '
Analysiere Preis- & Nachfragesignale…
'; + try { + var r = await fetch('/api/procurement/supply-squeeze', { headers: { 'Authorization': 'Bearer ' + token } }); + var d = await r.json(); + if (!d.success) throw new Error(d.error); + var sigs = d.signals || []; + var crit = sigs.filter(function(s) { return s.severity === 'critical'; }); + var warn = sigs.filter(function(s) { return s.severity === 'warning'; }); + if (summEl) summEl.innerHTML = '
' + + (crit.length ? '
πŸ”΄ '+crit.length+' Kritisch
' : '') + + (warn.length ? '
⚠️ '+warn.length+' Warnung
' : '') + + (!crit.length && !warn.length ? '
βœ… Kein akuter Engpass erkannt
' : '') + + '
'; + if (!sigs.length) { listEl.innerHTML = '
Keine Squeeze-Signale β€” Markt stabil.
'; return; } + var sevColor = { critical: '#ef4444', warning: '#f59e0b', watch: '#f59e0b', ok: '#64748b' }; + var sevIcon = { critical: 'πŸ”΄', warning: '⚠️', watch: 'πŸ‘', ok: 'βœ…' }; + var rows = sigs.slice(0,30).map(function(s) { + var col = sevColor[s.severity] || '#64748b'; + var icon = sevIcon[s.severity] || ''; + var mom = s.price_momentum_pct !== 0 ? (s.price_momentum_pct > 0 ? '+' : '') + s.price_momentum_pct + '%' : 'β€”'; + var momColor = s.price_momentum_pct > 10 ? '#ef4444' : s.price_momentum_pct > 0 ? '#f59e0b' : '#22c55e'; + return '' + + ''+icon+' '+esc(String(s.speed_gbps))+'G' + + ''+esc(s.form_factor||'β€”')+'' + + ''+esc(mom)+'' + + ''+esc((s.hype_phase||'β€”').replace(/_/g,' '))+'' + + ''+(s.ai_demand_tx ? s.ai_demand_tx.toLocaleString()+' tx' : 'β€”')+'' + + ''+esc(String(s.activeSignals))+'/4' + + ''+esc((s.reasons||[]).join(' Β· ').substring(0,100))+'' + + ''; + }).join(''); + listEl.innerHTML = '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + rows + '
TechnologieForm FactorPreis 30dHype PhaseAI DemandSignaleGrΓΌnde
'; + } catch(e) { + if (listEl) listEl.innerHTML = '
'+esc(e.message)+'
'; + } +} + +/* ── D: Dead Stock Revival ─────────────────────────────────────────────── */ +async function loadDeadStockRevival() { + var token = (window.loadToken ? window.loadToken() : '') || ''; + var summEl = el('proc-deadstock-summary'); + var listEl = el('proc-deadstock-list'); + if (listEl) listEl.innerHTML = '
Analyse Dead-Stock gegen Hype-Cycle…
'; + try { + var r = await fetch('/api/procurement/dead-stock-revival', { headers: { 'Authorization': 'Bearer ' + token } }); + var d = await r.json(); + if (!d.success) throw new Error(d.error); + if (summEl) summEl.innerHTML = [ + { label: 'Total Dead Stock', val: (d.totalDeadStock||0).toLocaleString(), color: '#64748b' }, + { label: 'Revival Kandidaten', val: (d.revivalCount||0).toLocaleString(), color: '#f59e0b' }, + { label: 'Revival Rate', val: d.totalDeadStock ? Math.round(d.revivalCount/d.totalDeadStock*100)+'%' : 'β€”', color: '#22c55e' }, + ].map(function(c) { + return '
' + + '
'+esc(c.label)+'
' + + '
'+esc(c.val)+'
'; + }).join(''); + if (!d.revivals || !d.revivals.length) { listEl.innerHTML = '
Keine Dead-Stock-Revival-Kandidaten gefunden.
'; return; } + var PHASE_ICONS = { innovation_trigger:'πŸ”¬', peak_inflated_expectations:'πŸš€', slope_enlightenment:'πŸ“ˆ', plateau_productivity:'βœ…' }; + var rows = d.revivals.map(function(r) { + var icon = PHASE_ICONS[r.hype_phase] || '●'; + var scoreColor = r.hype_score >= 70 ? '#22c55e' : r.hype_score >= 50 ? '#f59e0b' : '#94a3b8'; + var trend = r.demand_trend_pct != null ? (parseFloat(r.demand_trend_pct) > 0 ? '+' : '') + Math.round(parseFloat(r.demand_trend_pct)) + '%' : 'β€”'; + return '' + + ''+esc(r.vendor_name||'')+'' + + ''+esc(r.part_number||'')+'' + + ''+esc(r.form_factor)+'' + + ''+esc(String(r.speed_gbps))+'G' + + ''+icon+' '+esc((r.hype_phase||'').replace(/_/g,' '))+'' + + ''+Math.round(r.hype_score)+'' + + ''+esc(trend)+'' + + ''; + }).join(''); + listEl.innerHTML = '
Dead-Stock-SKUs deren Speed-Klasse eine steigende Hype-Phase hat (Hype Score >30). Revival = Markt dreht sich wieder fΓΌr diese Technologie.
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + rows + '
VendorPartFFSpeedHype PhaseScoreTrend
'; + } catch(e) { + if (listEl) listEl.innerHTML = '
'+esc(e.message)+'
'; + } } async function loadProcurement() {