fix(procurement): exclude speed=0 signals + consistent speed formatting

Reconciles Stock/Health working-tree WIP uncommitted since 2026-07-04:
- supply-squeeze: exclude speed_gbps=0 / unclassified speed_tier=0 from
  price-momentum and AI-cluster-demand signals
- dashboard: render transceiver speed via fmtSpd() (1600 -> 1.6T) in
  reorder-top, switch-compat, supply-squeeze, dead-stock-revival tables
This commit is contained in:
root 2026-07-06 07:44:34 +00:00
parent 479a0dbe56
commit 41e98ca5df
2 changed files with 22 additions and 19 deletions

View File

@ -938,6 +938,7 @@ procurementRouter.get("/supply-squeeze", async (_req: Request, res: Response) =>
AND t.part_number NOT ILIKE '%AOC%' AND t.part_number NOT ILIKE '%AOC%'
AND t.part_number NOT ILIKE '%-DAC-%' AND t.part_number NOT ILIKE '%-DAC-%'
AND (t.standard_name IS NULL OR (t.standard_name NOT ILIKE '%Switch%' AND t.standard_name NOT ILIKE '%InfiniBand%')) AND (t.standard_name IS NULL OR (t.standard_name NOT ILIKE '%Switch%' AND t.standard_name NOT ILIKE '%InfiniBand%'))
AND t.speed_gbps > 0
GROUP BY t.id, t.speed_gbps, t.form_factor GROUP BY t.id, t.speed_gbps, t.form_factor
) )
SELECT SELECT
@ -955,21 +956,23 @@ procurementRouter.get("/supply-squeeze", async (_req: Request, res: Response) =>
GROUP BY speed_gbps, form_factor GROUP BY speed_gbps, form_factor
HAVING COUNT(*) >= 3 HAVING COUNT(*) >= 3
`), `),
// AI cluster demand by speed tier // AI cluster demand by speed tier (speed_tier = 0 excluded — unclassified announcements)
pool.query(` pool.query(`
SELECT SELECT speed_tier, total_tx, cluster_count FROM (
CASE SELECT
WHEN COALESCE(network_speed, title, summary, '') ILIKE '%800G%' THEN 800 CASE
WHEN COALESCE(network_speed, title, summary, '') ILIKE '%400G%' THEN 400 WHEN COALESCE(network_speed, title, summary, '') ILIKE '%800G%' THEN 800
WHEN COALESCE(network_speed, title, summary, '') ILIKE '%100G%' THEN 100 WHEN COALESCE(network_speed, title, summary, '') ILIKE '%400G%' THEN 400
ELSE 0 WHEN COALESCE(network_speed, title, summary, '') ILIKE '%100G%' THEN 100
END AS speed_tier, ELSE 0
COALESCE(SUM(estimated_transceivers),0)::int AS total_tx, END AS speed_tier,
COUNT(*)::int AS cluster_count COALESCE(SUM(estimated_transceivers),0)::int AS total_tx,
FROM ai_cluster_announcements COUNT(*)::int AS cluster_count
WHERE announced_date >= NOW() - INTERVAL '90 days' FROM ai_cluster_announcements
GROUP BY 1 WHERE announced_date >= NOW() - INTERVAL '90 days'
HAVING COALESCE(SUM(estimated_transceivers),0) > 0 GROUP BY 1
HAVING COALESCE(SUM(estimated_transceivers),0) > 0
) t WHERE speed_tier > 0
`), `),
// Hype phase per technology // Hype phase per technology
pool.query(` pool.query(`
@ -1047,7 +1050,7 @@ procurementRouter.get("/supply-squeeze", async (_req: Request, res: Response) =>
activeSignals, severity, reasons, activeSignals, severity, reasons,
}; };
}) })
.filter((r) => r.activeSignals >= 1) .filter((r) => r.activeSignals >= 1 && parseFloat(String(r.speed_gbps)) > 0)
.sort((a, b) => b.activeSignals - a.activeSignals || b.price_momentum_pct - a.price_momentum_pct); .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 }); res.json({ success: true, signals, criticalCount: signals.filter(s => s.severity === "critical").length });

View File

@ -7990,7 +7990,7 @@ async function loadReorderTop() {
+ '<td style="padding:0.45rem 0.5rem;font-weight:700;color:var(--text-bright);font-size:0.78rem">'+esc(r.vendor_name||'')+'</td>' + '<td style="padding:0.45rem 0.5rem;font-weight:700;color:var(--text-bright);font-size:0.78rem">'+esc(r.vendor_name||'')+'</td>'
+ '<td style="padding:0.45rem 0.5rem;font-family:monospace;font-size:0.72rem;color:var(--text-dim)">'+esc(r.part_number||'')+'</td>' + '<td style="padding:0.45rem 0.5rem;font-family:monospace;font-size:0.72rem;color:var(--text-dim)">'+esc(r.part_number||'')+'</td>'
+ '<td style="padding:0.45rem 0.5rem;text-align:center"><span style="background:rgba(99,102,241,0.15);color:#818cf8;border-radius:4px;padding:2px 6px;font-size:0.7rem">'+esc(r.form_factor||'')+'</span></td>' + '<td style="padding:0.45rem 0.5rem;text-align:center"><span style="background:rgba(99,102,241,0.15);color:#818cf8;border-radius:4px;padding:2px 6px;font-size:0.7rem">'+esc(r.form_factor||'')+'</span></td>'
+ '<td style="padding:0.45rem 0.5rem;text-align:right;color:var(--blue);font-weight:700;font-size:0.78rem">'+esc(String(r.speed_gbps||''))+'G</td>' + '<td style="padding:0.45rem 0.5rem;text-align:right;color:var(--blue);font-weight:700;font-size:0.78rem">'+fmtSpd(r.speed_gbps)+'</td>'
+ '<td style="padding:0.45rem 0.5rem;text-align:center;font-weight:700;color:'+strColor+';font-family:monospace">'+str+'%</td>' + '<td style="padding:0.45rem 0.5rem;text-align:center;font-weight:700;color:'+strColor+';font-family:monospace">'+str+'%</td>'
+ '<td style="padding:0.45rem 0.5rem;font-size:0.7rem">'+pt+' Preis · '+st+' Stock</td>' + '<td style="padding:0.45rem 0.5rem;font-size:0.7rem">'+pt+' Preis · '+st+' Stock</td>'
+ '<td style="padding:0.45rem 0.5rem;font-size:0.7rem;color:var(--text-dim);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+esc(reasons)+'">'+esc(reasons.substring(0,80))+'</td>' + '<td style="padding:0.45rem 0.5rem;font-size:0.7rem;color:var(--text-dim);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+esc(reasons)+'">'+esc(reasons.substring(0,80))+'</td>'
@ -8106,7 +8106,7 @@ async function loadSwitchCompat() {
+ '<td style="padding:0.4rem 0.5rem;font-weight:600;font-size:0.75rem;color:var(--text-bright)">'+esc(t.vendor_name)+'</td>' + '<td style="padding:0.4rem 0.5rem;font-weight:600;font-size:0.75rem;color:var(--text-bright)">'+esc(t.vendor_name)+'</td>'
+ '<td style="padding:0.4rem 0.5rem;font-family:monospace;font-size:0.7rem;color:var(--text-dim)">'+esc(t.part_number||'')+'</td>' + '<td style="padding:0.4rem 0.5rem;font-family:monospace;font-size:0.7rem;color:var(--text-dim)">'+esc(t.part_number||'')+'</td>'
+ '<td style="padding:0.4rem 0.5rem;text-align:center"><span style="background:rgba(99,102,241,0.15);color:#818cf8;border-radius:4px;padding:1px 5px;font-size:0.68rem">'+esc(t.form_factor)+'</span></td>' + '<td style="padding:0.4rem 0.5rem;text-align:center"><span style="background:rgba(99,102,241,0.15);color:#818cf8;border-radius:4px;padding:1px 5px;font-size:0.68rem">'+esc(t.form_factor)+'</span></td>'
+ '<td style="padding:0.4rem 0.5rem;text-align:right;color:var(--blue);font-weight:700;font-size:0.72rem">'+esc(String(t.speed_gbps))+'G</td>' + '<td style="padding:0.4rem 0.5rem;text-align:right;color:var(--blue);font-weight:700;font-size:0.72rem">'+fmtSpd(t.speed_gbps)+'</td>'
+ '<td style="padding:0.4rem 0.5rem;text-align:right;color:var(--green);font-size:0.72rem">'+esc(priceStr)+'</td>' + '<td style="padding:0.4rem 0.5rem;text-align:right;color:var(--green);font-size:0.72rem">'+esc(priceStr)+'</td>'
+ '<td style="padding:0.4rem 0.5rem;text-align:center;font-size:0.68rem;color:var(--text-dim)">'+esc(t.verification_method||'')+'</td>' + '<td style="padding:0.4rem 0.5rem;text-align:center;font-size:0.68rem;color:var(--text-dim)">'+esc(t.verification_method||'')+'</td>'
+ '</tr>'; + '</tr>';
@ -8194,7 +8194,7 @@ async function loadSupplySqueeze() {
var mom = s.price_momentum_pct !== 0 ? (s.price_momentum_pct > 0 ? '+' : '') + s.price_momentum_pct + '%' : '—'; 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'; var momColor = s.price_momentum_pct > 10 ? '#ef4444' : s.price_momentum_pct > 0 ? '#f59e0b' : '#22c55e';
return '<tr style="border-bottom:1px solid var(--border)">' return '<tr style="border-bottom:1px solid var(--border)">'
+ '<td style="padding:0.45rem 0.5rem;font-weight:700;font-size:0.8rem;color:'+col+'">'+icon+' '+esc(String(s.speed_gbps))+'G</td>' + '<td style="padding:0.45rem 0.5rem;font-weight:700;font-size:0.8rem;color:'+col+'">'+icon+' '+fmtSpd(s.speed_gbps)+'</td>'
+ '<td style="padding:0.45rem 0.5rem"><span style="background:rgba(99,102,241,0.15);color:#818cf8;border-radius:4px;padding:2px 6px;font-size:0.7rem">'+esc(s.form_factor||'—')+'</span></td>' + '<td style="padding:0.45rem 0.5rem"><span style="background:rgba(99,102,241,0.15);color:#818cf8;border-radius:4px;padding:2px 6px;font-size:0.7rem">'+esc(s.form_factor||'—')+'</span></td>'
+ '<td style="padding:0.45rem 0.5rem;text-align:right;font-weight:700;color:'+momColor+';font-family:monospace">'+esc(mom)+'</td>' + '<td style="padding:0.45rem 0.5rem;text-align:right;font-weight:700;color:'+momColor+';font-family:monospace">'+esc(mom)+'</td>'
+ '<td style="padding:0.45rem 0.5rem;font-size:0.7rem;color:var(--text-dim)">'+esc((s.hype_phase||'—').replace(/_/g,' '))+'</td>' + '<td style="padding:0.45rem 0.5rem;font-size:0.7rem;color:var(--text-dim)">'+esc((s.hype_phase||'—').replace(/_/g,' '))+'</td>'
@ -8246,7 +8246,7 @@ async function loadDeadStockRevival() {
+ '<td style="padding:0.45rem 0.5rem;font-weight:700;font-size:0.75rem;color:var(--text-bright)">'+esc(r.vendor_name||'')+'</td>' + '<td style="padding:0.45rem 0.5rem;font-weight:700;font-size:0.75rem;color:var(--text-bright)">'+esc(r.vendor_name||'')+'</td>'
+ '<td style="padding:0.45rem 0.5rem;font-family:monospace;font-size:0.68rem;color:var(--text-dim)">'+esc(r.part_number||'')+'</td>' + '<td style="padding:0.45rem 0.5rem;font-family:monospace;font-size:0.68rem;color:var(--text-dim)">'+esc(r.part_number||'')+'</td>'
+ '<td style="padding:0.45rem 0.5rem;text-align:center"><span style="background:rgba(99,102,241,0.15);color:#818cf8;border-radius:4px;padding:2px 5px;font-size:0.68rem">'+esc(r.form_factor)+'</span></td>' + '<td style="padding:0.45rem 0.5rem;text-align:center"><span style="background:rgba(99,102,241,0.15);color:#818cf8;border-radius:4px;padding:2px 5px;font-size:0.68rem">'+esc(r.form_factor)+'</span></td>'
+ '<td style="padding:0.45rem 0.5rem;text-align:right;color:var(--blue);font-weight:700;font-size:0.72rem">'+esc(String(r.speed_gbps))+'G</td>' + '<td style="padding:0.45rem 0.5rem;text-align:right;color:var(--blue);font-weight:700;font-size:0.72rem">'+fmtSpd(r.speed_gbps)+'</td>'
+ '<td style="padding:0.45rem 0.5rem;font-size:0.72rem">'+icon+' '+esc((r.hype_phase||'').replace(/_/g,' '))+'</td>' + '<td style="padding:0.45rem 0.5rem;font-size:0.72rem">'+icon+' '+esc((r.hype_phase||'').replace(/_/g,' '))+'</td>'
+ '<td style="padding:0.45rem 0.5rem;text-align:right;font-weight:700;color:'+scoreColor+';font-family:monospace">'+Math.round(r.hype_score)+'</td>' + '<td style="padding:0.45rem 0.5rem;text-align:right;font-weight:700;color:'+scoreColor+';font-family:monospace">'+Math.round(r.hype_score)+'</td>'
+ '<td style="padding:0.45rem 0.5rem;text-align:right;font-size:0.72rem;color:'+(parseFloat(trend)>0?'#22c55e':'#64748b')+'">'+esc(trend)+'</td>' + '<td style="padding:0.45rem 0.5rem;text-align:right;font-size:0.72rem;color:'+(parseFloat(trend)>0?'#22c55e':'#64748b')+'">'+esc(trend)+'</td>'