feat: TIP Phase 0+1 — monorepo, DB schema, API, scraper engine
Phase 0 - Foundation: - Restructure into npm workspace monorepo (packages/core, api, scraper) - PostgreSQL 17 + TimescaleDB schema (15 tables incl. hypertables) - Docker Compose for local dev (PostgreSQL on 5433 + Qdrant) - Express 5 API on port 3200 with 6 routes - Seed script to migrate 159 transceivers + 42 standards from npm package - Erik server setup script + PM2 ecosystem config Phase 1 - Scraper Engine: - Crawlee + Playwright framework with pg-boss scheduler - FS.com scraper (PlaywrightCrawler, anti-bot workaround) - Optcore.net scraper (WP REST API enumeration + PlaywrightCrawler) - Uses /wp-json/wp/v2/product to get 2000+ product URLs - Playwright renders individual product pages for price extraction - Cisco TMG Matrix scraper (compatibility data) - News RSS aggregator (optics.org, SPIE, Network World, Nature Photonics) - Keyword relevance scoring for transceiver/fiber topics - xml2js with malformed XML sanitization - SHA-256 content hashing for change detection (skip unchanged records) - pg-boss v10 with explicit queue creation before scheduling
This commit is contained in:
parent
ddd0a592aa
commit
e9fb50a248
1107
CONCEPT-transceiver-intelligence-platform.md
Normal file
1107
CONCEPT-transceiver-intelligence-platform.md
Normal file
File diff suppressed because it is too large
Load Diff
672
RESEARCH-optical-transceiver-history.md
Normal file
672
RESEARCH-optical-transceiver-history.md
Normal file
@ -0,0 +1,672 @@
|
|||||||
|
# Optical Transceiver Evolution: Complete History & Database Reference (2001-2026)
|
||||||
|
|
||||||
|
> Deep research compiled from OFC proceedings, LightCounting, Cignal AI, IEEE, OIF, and industry publications.
|
||||||
|
> Last updated: 2026-03-27
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Form Factor Evolution Timeline](#1-form-factor-evolution-timeline)
|
||||||
|
2. [Speed Tier Evolution](#2-speed-tier-evolution)
|
||||||
|
3. [Key Standards & Adoption Timelines](#3-key-standards--adoption-timelines)
|
||||||
|
4. [CWDM vs DWDM Evolution](#4-cwdm-vs-dwdm-evolution)
|
||||||
|
5. [Major Transceiver Manufacturers](#5-major-transceiver-manufacturers)
|
||||||
|
6. [Next-Generation Technologies (2025-2030)](#6-next-generation-technologies-2025-2030)
|
||||||
|
7. [Market Data Points](#7-market-data-points)
|
||||||
|
8. [Database Schema Recommendations](#8-database-schema-recommendations)
|
||||||
|
9. [Hype Cycle Analysis](#9-hype-cycle-analysis)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Form Factor Evolution Timeline
|
||||||
|
|
||||||
|
### Complete Form Factor Database
|
||||||
|
|
||||||
|
| Form Factor | Year Introduced | Peak Adoption | Legacy/Decline | Max Speed | Connector | Lanes | Status |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **GBIC** | 1995 | 2000-2004 | 2006+ | 2.5 Gbps | SC Duplex | 1 | Obsolete |
|
||||||
|
| **SFP** | 2001 | 2004-present | Still active (1G) | 4.25 Gbps | LC Duplex | 1 | Active (legacy speeds) |
|
||||||
|
| **XENPAK** | 2001 | 2002-2006 | 2007+ | 10 Gbps | SC Duplex | 1 | Obsolete |
|
||||||
|
| **X2** | 2003 | 2004-2008 | 2009+ | 10 Gbps | SC Duplex | 1 | Obsolete |
|
||||||
|
| **XFP** | 2002 (MSA), 2003 (adopted) | 2005-2012 | 2013+ | 10 Gbps (DWDM capable) | LC Duplex | 1 | Legacy |
|
||||||
|
| **SFP+** | 2006 | 2008-present | Still active | 16 Gbps | LC Duplex | 1 | Active |
|
||||||
|
| **QSFP** | 2006 | 2008-2012 | 2013+ | 4x1G = 4 Gbps | MPO-12 | 4 | Legacy |
|
||||||
|
| **CFP** | 2009 | 2010-2016 | 2017+ | 100 Gbps | LC Duplex/MPO | 10x10G | Legacy |
|
||||||
|
| **QSFP+** | 2012 | 2013-2020 | Declining | 40 Gbps | MPO-12 / LC | 4x10G | Active (declining) |
|
||||||
|
| **CFP2** | 2012 | 2014-2020 | 2021+ | 200 Gbps | LC Duplex | varies | Legacy (except coherent) |
|
||||||
|
| **CFP4** | 2014 | 2015-2019 | 2020+ | 100 Gbps | LC Duplex | 4x25G | Legacy |
|
||||||
|
| **QSFP28** | 2014 | 2016-2023 | Declining | 100 Gbps | LC / MPO-12 | 4x25G | Active (declining) |
|
||||||
|
| **SFP28** | 2014 | 2016-present | Still active | 25 Gbps | LC Duplex | 1 | Active |
|
||||||
|
| **OSFP** | 2016 (announced) | 2020-present | - | 800 Gbps (8x100G) | MPO-16 / LC | 8 | Active (growing) |
|
||||||
|
| **CSFP** | 2018 | 2019-present | - | 2x1 Gbps | LC (BiDi) | 2 (BiDi) | Niche |
|
||||||
|
| **QSFP56** | 2019 | 2020-2024 | Declining | 200 Gbps | MPO-12 / LC | 4x50G | Active (declining) |
|
||||||
|
| **QSFP-DD** | 2019 | 2021-present | - | 800 Gbps (8x100G) | MPO-16 / LC | 8 | Active (growing) |
|
||||||
|
| **SFP56** | 2020 (spec), 2024 (products) | 2024-present | - | 50 Gbps | LC Duplex | 1 | Active (emerging) |
|
||||||
|
| **QSFP112** | 2021 | 2022-present | - | 400 Gbps | MPO-12 / LC | 4x100G | Active |
|
||||||
|
| **SFP-DD** | 2017 (spec) | 2020-present | - | 2x25G = 50 Gbps | LC Duplex | 2 | Niche |
|
||||||
|
| **OSFP-XD** | 2022 | 2025-present | - | 1.6T (16x100G), 3.2T future | MPO-16 | 16 | Emerging |
|
||||||
|
| **QSFP-DD1600** | 2024 (spec in progress) | 2026+ (projected) | - | 1.6T (8x200G) | MPO-16 | 8 | Emerging |
|
||||||
|
| **OSFP1600** | 2022 (spec) | 2025-2026 | - | 1.6T (8x200G) | MPO-16 | 8 | Emerging |
|
||||||
|
|
||||||
|
### Form Factor Hype Cycle Phases
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1: INTRODUCTION - Standard published, first samples
|
||||||
|
Phase 2: EARLY ADOPTION - Hyperscale/cloud first movers
|
||||||
|
Phase 3: MAINSTREAM - Broad enterprise deployment, pricing declines
|
||||||
|
Phase 4: MATURITY - Commoditized, price floor reached
|
||||||
|
Phase 5: DECLINE - Next generation overtakes, volume drops
|
||||||
|
Phase 6: LEGACY - Minimal new deployments, maintenance only
|
||||||
|
Phase 7: OBSOLETE - No longer manufactured
|
||||||
|
```
|
||||||
|
|
||||||
|
| Form Factor | Current Phase (2026) |
|
||||||
|
|---|---|
|
||||||
|
| GBIC | 7-OBSOLETE |
|
||||||
|
| XENPAK | 7-OBSOLETE |
|
||||||
|
| X2 | 7-OBSOLETE |
|
||||||
|
| XFP | 6-LEGACY |
|
||||||
|
| SFP (1G) | 4-MATURITY |
|
||||||
|
| SFP+ (10G) | 4-MATURITY |
|
||||||
|
| QSFP+ (40G) | 5-DECLINE |
|
||||||
|
| CFP/CFP2/CFP4 | 6-LEGACY (except CFP2-DCO) |
|
||||||
|
| SFP28 (25G) | 3-MAINSTREAM |
|
||||||
|
| QSFP28 (100G) | 4-MATURITY / 5-DECLINE |
|
||||||
|
| QSFP56 (200G) | 5-DECLINE |
|
||||||
|
| QSFP-DD (400G/800G) | 3-MAINSTREAM |
|
||||||
|
| OSFP (400G/800G) | 3-MAINSTREAM |
|
||||||
|
| QSFP112 (400G) | 2-EARLY ADOPTION |
|
||||||
|
| OSFP-XD (1.6T) | 1-INTRODUCTION |
|
||||||
|
| QSFP-DD1600 (1.6T) | 1-INTRODUCTION |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Speed Tier Evolution
|
||||||
|
|
||||||
|
### Speed Tier Database
|
||||||
|
|
||||||
|
| Speed | Year Standardized | Year Mainstream | Dominant Form Factor | Modulation | Lanes | Key Standard | Current Status |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **1G** | 1998 (802.3z) | 2002 | SFP | NRZ | 1 | IEEE 802.3z | Mature/commodity |
|
||||||
|
| **10G** | 2002 (802.3ae) | 2007 | SFP+ | NRZ | 1 | IEEE 802.3ae | Mature/commodity |
|
||||||
|
| **25G** | 2016 (802.3by) | 2018 | SFP28 | NRZ | 1 | IEEE 802.3by | Mainstream |
|
||||||
|
| **40G** | 2010 (802.3ba) | 2013 | QSFP+ | NRZ | 4x10G | IEEE 802.3ba | Declining |
|
||||||
|
| **50G** | 2016 (802.3cd) | 2020 | SFP56 / QSFP28 | PAM4 (single lane) | 1 | IEEE 802.3cd | Niche |
|
||||||
|
| **100G** | 2010 (802.3ba) / 2014 (QSFP28) | 2017 | QSFP28 | NRZ (4x25G) | 4 | IEEE 802.3ba | Mainstream/declining |
|
||||||
|
| **200G** | 2017 (802.3bs) | 2020 | QSFP56 / QSFP-DD | PAM4 | 4x50G | IEEE 802.3bs | Active |
|
||||||
|
| **400G** | 2017 (802.3bs) | 2022 | QSFP-DD / OSFP | PAM4 | 8x50G or 4x100G | IEEE 802.3bs | Mainstream |
|
||||||
|
| **800G** | 2024 (802.3df) | 2024-2025 | OSFP / QSFP-DD | PAM4 | 8x100G | IEEE 802.3df | Rapid growth |
|
||||||
|
| **1.6T** | 2026 (802.3dj target) | 2026-2027 (projected) | OSFP-XD / OSFP1600 | PAM4 | 8x200G or 16x100G | IEEE 802.3dj | Emerging |
|
||||||
|
|
||||||
|
### Speed Tier Adoption S-Curves (Port Shipment Peak Years)
|
||||||
|
|
||||||
|
```
|
||||||
|
1G: Peak ~2010-2014, still shipping in volume for enterprise access
|
||||||
|
10G: Peak ~2016-2020, declining but high volume
|
||||||
|
25G: Peak ~2020-2024, server-side standard
|
||||||
|
40G: Peak ~2015-2019, largely replaced by 100G
|
||||||
|
100G: Peak ~2020-2024, transitioning to 400G
|
||||||
|
400G: Peak ~2024-2027 (projected), current mainstream for spine/core
|
||||||
|
800G: Peak ~2026-2029 (projected), AI backend standard
|
||||||
|
1.6T: Peak ~2028-2031 (projected), next-gen AI/HPC
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modulation Technology Timeline
|
||||||
|
|
||||||
|
| Technology | Speed Range | Years Active | Key Characteristic |
|
||||||
|
|---|---|---|---|
|
||||||
|
| NRZ (Non-Return-to-Zero) | 1G-25G per lane | 1995-present | 1 bit per symbol, simple |
|
||||||
|
| PAM4 (4-level Pulse Amplitude) | 50G-200G per lane | 2017-present | 2 bits per symbol, requires DSP/FEC |
|
||||||
|
| Coherent (DP-QPSK/DP-16QAM) | 100G-800G per wavelength | 2011-present | Phase + amplitude, long-haul |
|
||||||
|
|
||||||
|
### Per-Lane Rate Evolution
|
||||||
|
|
||||||
|
| Year | Per-Lane Rate | Technology | Key Enabler |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2001-2005 | 1G | NRZ | DFB/VCSEL |
|
||||||
|
| 2006-2013 | 10G | NRZ | DFB/VCSEL, CDR |
|
||||||
|
| 2014-2018 | 25G | NRZ | EML, CDR |
|
||||||
|
| 2019-2022 | 50G | PAM4 | DSP (7nm/5nm) |
|
||||||
|
| 2022-2025 | 100G | PAM4 | DSP (5nm/3nm), SiPh |
|
||||||
|
| 2025-2028 | 200G | PAM4 | DSP (3nm), advanced FEC |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Key Standards & Adoption Timelines
|
||||||
|
|
||||||
|
### IEEE 802.3 Optical Ethernet Standards
|
||||||
|
|
||||||
|
| Standard | Year Ratified | Speed | Key PHY Types | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 802.3z | 1998 | 1 Gbps | 1000BASE-SX, 1000BASE-LX | First Gigabit Ethernet |
|
||||||
|
| 802.3ae | June 2002 | 10 Gbps | 10GBASE-SR, -LR, -ER, -LX4 | First 10GbE, fiber only |
|
||||||
|
| 802.3aq | 2006 | 10 Gbps | 10GBASE-LRM | Long reach multimode |
|
||||||
|
| 802.3ba | June 2010 | 40/100 Gbps | 40GBASE-SR4/LR4, 100GBASE-SR10/LR4/ER4 | First multi-rate standard |
|
||||||
|
| 802.3bm | 2015 | 40/100 Gbps | 40GBASE-SR4 (OM3/OM4), 100GBASE-SR4 | Improved MMF reach |
|
||||||
|
| 802.3by | 2016 | 25 Gbps | 25GBASE-SR, 25GBASE-LR | Single-lane 25G |
|
||||||
|
| 802.3bs | Dec 2017 | 200/400 Gbps | 200GBASE-DR4, 400GBASE-SR16/DR4/FR8/LR8 | First PAM4 in standard |
|
||||||
|
| 802.3cd | Dec 2018 | 50/100/200 Gbps | 50GBASE-SR/LR/FR/CR, 100GBASE-DR/SR2 | Single-lane 50G NRZ |
|
||||||
|
| 802.3cm | 2020 | 400 Gbps | 400GBASE-SR4.2 | Short-reach MMF (BiDi SWDM) |
|
||||||
|
| 802.3ct | 2021 | 100 Gbps | 100GBASE-ZR | Coherent 100G pluggable |
|
||||||
|
| 802.3cu | 2021 | 100/400 Gbps | 100GBASE-FR1/LR1, 400GBASE-FR4 | Single-lambda 100G |
|
||||||
|
| 802.3ck | Sep 2022 | 100/200/400 Gbps | Electrical interfaces (100G/lane) | Defines 100G SerDes |
|
||||||
|
| 802.3db | Sep 2022 | 100/200/400 Gbps | 100GBASE-VR1, 400GBASE-VR4 | Very short reach |
|
||||||
|
| 802.3df | Feb 2024 | 400/800 Gbps | 800GBASE-DR8, 400GBASE-DR4-2 | 800G standard |
|
||||||
|
| 802.3dj | ~2026 (target) | 200/400/800/1600 Gbps | 200G/lane PHYs | 1.6T Ethernet |
|
||||||
|
|
||||||
|
### OIF Implementation Agreements
|
||||||
|
|
||||||
|
| Agreement | Year Published | Speed | Max Reach | Key Feature |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| VSR-5 OIF-05.0 | ~2010 | 100G | 100m | Very short reach coherent |
|
||||||
|
| 400ZR | Dec 2020 | 400G | 120km (amplified) | Pluggable coherent DWDM in QSFP-DD/OSFP |
|
||||||
|
| 400ZR+ (vendor-specific) | 2021 | 400G | 450-600km | Extended reach, oFEC |
|
||||||
|
| 800ZR (in progress) | 2024-2025 | 800G | 80-120km | Next-gen pluggable coherent |
|
||||||
|
| 1600ZR (in progress) | 2025+ | 1.6T | TBD | Future coherent standard |
|
||||||
|
| CEI-112G | 2021 | 112 Gbps/lane | Chip-to-module | 100G PAM4 electrical interface |
|
||||||
|
| CEI-224G | 2025 (target) | 224 Gbps/lane | Chip-to-module | 200G PAM4 electrical interface |
|
||||||
|
|
||||||
|
### Multi-Source Agreements (MSAs)
|
||||||
|
|
||||||
|
| MSA | Year Published | Speed | Technology | Reach | Key Members |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| SFP MSA | 2000 | 1-4G | Various | Varies | Finisar, JDS, Agilent |
|
||||||
|
| XFP MSA | 2002 | 10G | Various | Varies | Finisar + 10 companies |
|
||||||
|
| SFP+ MSA (SFF-8431) | 2006 | 10G | NRZ | Varies | Industry-wide |
|
||||||
|
| QSFP+ MSA (SFF-8436) | 2009 | 40G | 4x10G NRZ | Varies | Industry-wide |
|
||||||
|
| CFP MSA | 2009 | 100G | 10x10G/4x25G | Varies | Industry-wide |
|
||||||
|
| QSFP28 MSA (SFF-8665) | 2014 | 100G | 4x25G NRZ | Varies | Industry-wide |
|
||||||
|
| 100G PSM4 MSA | Mar 2014 | 100G | 4x25G parallel SM | 500m | Corning, Intel, Luxtera, etc. |
|
||||||
|
| 100G CWDM4 MSA | Sep 2014 | 100G | 4x25G CWDM | 2km | Avago, Finisar, JDSU, etc. |
|
||||||
|
| SFP28 MSA (SFF-8402) | 2014 | 25G | NRZ | Varies | Industry-wide |
|
||||||
|
| 25G Ethernet Consortium | 2014 | 25/50G | NRZ | Varies | Arista, Broadcom, Google, Microsoft |
|
||||||
|
| 100G Lambda MSA | Sep 2017 | 100G/400G | Single-lambda 100G PAM4 | 2-40km | Alibaba, Cisco, Intel, +39 members |
|
||||||
|
| QSFP-DD MSA | 2017 | 200-800G | 8-lane double density | Varies | Broadcom, Cisco, Finisar, etc. |
|
||||||
|
| OSFP MSA | 2016 | 400-800G | 8-lane octal | Varies | Arista, Broadcom, Mellanox, etc. |
|
||||||
|
| OpenZR+ MSA | May 2020 | 100-400G | Coherent DWDM | 1000+km | Acacia, Cisco, Juniper, Lumentum |
|
||||||
|
| OSFP-XD MSA | 2022 | 1.6-3.2T | 16-lane | Varies | Industry-wide |
|
||||||
|
| CMIS (Common Mgmt Interface) | v5.0: 2020, v5.3: 2024 | All | Management spec | - | Industry-wide |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. CWDM vs DWDM Evolution
|
||||||
|
|
||||||
|
### CWDM Technical Specifications
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|---|---|
|
||||||
|
| Standard | ITU-T G.694.2 |
|
||||||
|
| Wavelength Range | 1270-1610 nm |
|
||||||
|
| Channel Spacing | 20 nm |
|
||||||
|
| Total Channels | 18 (full grid) |
|
||||||
|
| Practical Channels | 8-16 (water peak limits 1370-1410nm) |
|
||||||
|
| Laser Type | Uncooled DFB |
|
||||||
|
| Max Reach | ~70 km (unamplified) |
|
||||||
|
| Max Per-Channel Speed | 100 Gbps (current), 25G most common |
|
||||||
|
| Amplification | None (passive) |
|
||||||
|
| Cost | Lower (uncooled lasers, wider tolerance) |
|
||||||
|
|
||||||
|
#### CWDM Wavelength Grid
|
||||||
|
|
||||||
|
| Channel | Wavelength (nm) | Band | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | 1271 | O-band | Commonly used |
|
||||||
|
| 2 | 1291 | O-band | Commonly used |
|
||||||
|
| 3 | 1311 | O-band | Commonly used |
|
||||||
|
| 4 | 1331 | O-band | Commonly used |
|
||||||
|
| 5 | 1351 | E-band | Water peak region |
|
||||||
|
| 6 | 1371 | E-band | Water peak region |
|
||||||
|
| 7 | 1391 | S-band | Water peak region (limited to 40km) |
|
||||||
|
| 8 | 1411 | S-band | Water peak region (limited to 40km) |
|
||||||
|
| 9 | 1431 | S-band | |
|
||||||
|
| 10 | 1451 | S-band | |
|
||||||
|
| 11 | 1471 | C-band edge | Commonly used |
|
||||||
|
| 12 | 1491 | S/C-band | Commonly used |
|
||||||
|
| 13 | 1511 | C-band | Commonly used |
|
||||||
|
| 14 | 1531 | C-band | Commonly used |
|
||||||
|
| 15 | 1551 | C-band | Commonly used |
|
||||||
|
| 16 | 1571 | L-band | Commonly used |
|
||||||
|
| 17 | 1591 | L-band | |
|
||||||
|
| 18 | 1611 | L-band | |
|
||||||
|
|
||||||
|
### DWDM Technical Specifications
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|---|---|
|
||||||
|
| Standard | ITU-T G.694.1 |
|
||||||
|
| C-Band Range | 1528.77-1563.86 nm (191.7-196.1 THz) |
|
||||||
|
| L-Band Range | 1565-1625 nm |
|
||||||
|
| Channel Spacing (100 GHz) | 0.8 nm, ~40 channels in C-band |
|
||||||
|
| Channel Spacing (50 GHz) | 0.4 nm, ~80 channels in C-band |
|
||||||
|
| Channel Spacing (25 GHz) | 0.2 nm, ~160 channels (flex grid) |
|
||||||
|
| Laser Type | Cooled DFB / Tunable |
|
||||||
|
| Max Reach | 3000+ km (amplified with EDFA/Raman) |
|
||||||
|
| Max Per-Channel Speed | 800 Gbps (coherent pluggable) |
|
||||||
|
| Amplification | EDFA, Raman |
|
||||||
|
| Flex Grid | Supports variable channel widths (12.5 GHz granularity) |
|
||||||
|
|
||||||
|
### Coherent Optics Evolution
|
||||||
|
|
||||||
|
| Generation | Year | Per-Wavelength Rate | Modulation | Baud Rate | Form Factor |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Gen 1 | 2011 | 40G | DP-QPSK | 10-12 GBd | Line card (chassis) |
|
||||||
|
| Gen 2 | 2012 | 100G | DP-QPSK | 32 GBd | Line card / CFP |
|
||||||
|
| Gen 3 | 2016 | 200G | DP-16QAM | 32-45 GBd | CFP2-DCO |
|
||||||
|
| Gen 4 | 2018 | 400G | DP-16QAM | 64 GBd | CFP2-DCO |
|
||||||
|
| Gen 5 (400ZR) | 2021 | 400G | DP-16QAM | 60 GBd | QSFP-DD / OSFP |
|
||||||
|
| Gen 6 (ZR+) | 2022 | 400G | DP-16QAM (enhanced) | 64 GBd | QSFP-DD / OSFP |
|
||||||
|
| Gen 7 (800ZR) | 2024 | 800G | DP-64QAM / prob-shaped | 100+ GBd | QSFP-DD / OSFP |
|
||||||
|
| Gen 8 (1600ZR) | 2026+ | 1.6T | TBD | 130+ GBd | OSFP / OSFP-XD |
|
||||||
|
|
||||||
|
### C+L Band Capacity Evolution
|
||||||
|
|
||||||
|
| Year | Typical System Capacity | Technology |
|
||||||
|
|---|---|---|
|
||||||
|
| 2005 | 40x10G = 400 Gbps | C-band, 100GHz grid |
|
||||||
|
| 2010 | 80x40G = 3.2 Tbps | C-band, 50GHz grid |
|
||||||
|
| 2015 | 80x100G = 8 Tbps | C-band, 50GHz grid, coherent |
|
||||||
|
| 2020 | 80x400G = 32 Tbps | C-band, flex grid |
|
||||||
|
| 2024 | 80x800G = 64 Tbps | C-band, flex grid |
|
||||||
|
| 2025+ | 120+x800G = 96+ Tbps | C+L band, flex grid |
|
||||||
|
|
||||||
|
### Key WDM Transceiver Types by Speed
|
||||||
|
|
||||||
|
| Speed | CWDM Variants | DWDM Variants |
|
||||||
|
|---|---|---|
|
||||||
|
| 1G | SFP CWDM (18 wavelengths) | SFP DWDM (C-band) |
|
||||||
|
| 10G | SFP+ CWDM, XFP CWDM | XFP/SFP+ DWDM (tunable) |
|
||||||
|
| 25G | SFP28 CWDM | SFP28 DWDM |
|
||||||
|
| 40G | QSFP+ CWDM4 | CFP DWDM (coherent) |
|
||||||
|
| 100G | QSFP28 CWDM4 | QSFP28 DWDM / CFP2-DCO |
|
||||||
|
| 400G | (not practical) | QSFP-DD/OSFP ZR/ZR+ |
|
||||||
|
| 800G | (not practical) | OSFP/QSFP-DD 800ZR/ZR+ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Major Transceiver Manufacturers
|
||||||
|
|
||||||
|
### Manufacturer Database
|
||||||
|
|
||||||
|
| Company | HQ | Founding | Key Milestones | Specialty | 2024 Revenue (transceivers) | Market Position |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **Coherent Corp.** | Pittsburgh, USA | 1971 (as II-VI) | Acquired Finisar ($3.2B, 2019), Coherent ($6.56B, 2022) | Coherent, Datacom, InP lasers | ~$2.5B+ | #2 globally, #1 telecom |
|
||||||
|
| **Zhongji Innolight** | Suzhou, China | 2008 | #1 globally 2023, 50%+ Nvidia wallet share | Datacom, 800G/1.6T | ~$3.3B (114% YoY growth) | #1 globally |
|
||||||
|
| **Lumentum** | San Jose, USA | 2015 (spun off JDS Uniphase) | Acquired Cloud Light ($750M, 2024), Oclaro ($1.8B, 2018) | Coherent, lasers, 3D sensing | ~$1.5B | #3 globally |
|
||||||
|
| **Broadcom (Optical)** | San Jose, USA | Broadcom acquired original Avago/LSI/Broadcom | Key DSP/PAM4 supplier | DSP chips, SiPh, VCSEL | ~$1B+ | Major component supplier |
|
||||||
|
| **Cisco (Silicon Photonics)** | San Jose, USA | Acquired Luxtera ($660M, 2019), Acacia ($4.6B, 2021) | Integrated SiPh transceivers | SiPh, coherent (via Acacia) | Internal consumption + merchant | #4-5 globally |
|
||||||
|
| **Eoptolink** | Shenzhen, China | 2004 | 175% revenue growth 2024, #3 globally | Datacom, LPO, SiPh | ~$1.2B | #3 globally |
|
||||||
|
| **HG Genuine** | Wuhan, China | 2001 | ByteDance/TikTok supplier since 2021 | Datacom, access optics | ~$600M+ | #8 globally |
|
||||||
|
| **Accelink Technologies** | Wuhan, China | 2001 | Chinese cloud supplier | Telecom, passive components | ~$600M+ | #5 globally |
|
||||||
|
| **Hisense Broadband** | Qingdao, China | 2003 (Hisense subsidiary) | PON/access market leader | Access, PON, 5G | ~$600M+ | #6 globally |
|
||||||
|
| **Source Photonics** | West Hills, USA / China | 2002 | Chinese cloud supplier | Access, enterprise, DC | ~$400M | #9 globally |
|
||||||
|
| **Applied Optoelectronics (AOI)** | Sugar Land, USA | 1997 | CATV and DC optics | VCSEL, DFB, DC transceivers | ~$200M | Niche |
|
||||||
|
| **Intel Silicon Photonics** | Santa Clara, USA | SiPh division ~2010 | 100G PSM4, 1.6T SiPh engines | Silicon photonics platform | Sold to third parties (Jabil etc.) | Technology leader |
|
||||||
|
| **ColorChip** | Yokneam, Israel | 2001 | Acquired by Source Photonics 2018 | PLC-based transceivers | (merged) | Acquired |
|
||||||
|
| **Broadex Technologies** | Chengdu, China | 2016 | Fast-growing Chinese supplier | Datacom, 400G/800G | ~$300M | Emerging |
|
||||||
|
| **Centera Photonics** | Taiwan | 2007 | 800G/1.6T development | Datacom transceivers | ~$150M | Regional |
|
||||||
|
|
||||||
|
### Market Share Trends (Global Optical Transceiver Revenue)
|
||||||
|
|
||||||
|
| Year | #1 | #2 | #3 | Chinese in Top 10 | Key Shift |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2015 | Finisar | Lumentum/JDSU | Avago/Broadcom | 2-3 | US/Japan dominance |
|
||||||
|
| 2018 | Finisar | II-VI | Lumentum | 3-4 | Pre-merger era |
|
||||||
|
| 2020 | Coherent (II-VI+Finisar) | Innolight | Lumentum | 4-5 | Chinese rise begins |
|
||||||
|
| 2022 | Innolight = Coherent (~$1.4B each) | Lumentum | Accelink | 5-6 | Chinese parity |
|
||||||
|
| 2023 | Innolight | Coherent | Lumentum | 7 of top 10 | Chinese dominance |
|
||||||
|
| 2024 | Innolight ($3.3B) | Coherent (~$2.5B) | Eoptolink ($1.2B) | 7 of top 10 | AI-driven surge |
|
||||||
|
|
||||||
|
### Major M&A Timeline
|
||||||
|
|
||||||
|
| Year | Acquirer | Target | Value | Impact |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 2013 | Oclaro | Opnext | $180M | Combined coherent portfolio |
|
||||||
|
| 2015 | Lumentum spins off | from JDS Uniphase | - | Created independent photonics leader |
|
||||||
|
| 2018 | II-VI | Finisar | $3.2B | Created #1 transceiver company |
|
||||||
|
| 2018 | Lumentum | Oclaro | $1.8B | Strengthened InP/coherent capabilities |
|
||||||
|
| 2019 | Cisco | Luxtera | $660M | Silicon photonics integration |
|
||||||
|
| 2019 | Cisco | Acacia Communications | $4.6B | Coherent DSP leadership |
|
||||||
|
| 2021 | Intel | (SiPh division established) | Internal | 100G-1.6T silicon photonics engines |
|
||||||
|
| 2022 | II-VI | Coherent Inc. (laser co.) | $6.56B | Renamed to Coherent Corp. |
|
||||||
|
| 2024 | Lumentum | Cloud Light Technology | $750M | DC infrastructure boost |
|
||||||
|
| 2024 | Nvidia | (investing in optical supply chain) | Various | Vertical integration signal |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Next-Generation Technologies (2025-2030)
|
||||||
|
|
||||||
|
### 1.6T Transceivers
|
||||||
|
|
||||||
|
| Parameter | Gen1 (16x100G) | Gen2 (8x200G) |
|
||||||
|
|---|---|---|
|
||||||
|
| Timeline | 2025 (shipping) | 2026 (maturing) |
|
||||||
|
| Lane Rate | 100G PAM4 | 200G PAM4 |
|
||||||
|
| Lane Count | 16 | 8 |
|
||||||
|
| Form Factor | OSFP-XD | OSFP1600, OSFP, QSFP-DD1600 |
|
||||||
|
| DSP Process | 5nm | 3nm |
|
||||||
|
| Power (retimed) | ~25-30W | ~17-26W |
|
||||||
|
| Power (LPO) | ~8-12W | ~5W |
|
||||||
|
| Key DSPs | Broadcom Sian2, Marvell Aries | Broadcom Sian3, Marvell next-gen |
|
||||||
|
|
||||||
|
### Co-Packaged Optics (CPO) Timeline
|
||||||
|
|
||||||
|
| Year | Milestone |
|
||||||
|
|---|---|
|
||||||
|
| 2021 | Broadcom Tomahawk 4 + Humboldt = first CPO chipset |
|
||||||
|
| 2022 | Broadcom Tomahawk 5 + Bailly = first volume-production CPO |
|
||||||
|
| 2025 Q1 | NVIDIA announces first 1.6T CPO system (Micro Ring Modulators) |
|
||||||
|
| 2025 Q2 | NVIDIA Quantum-X SiPh switch ships |
|
||||||
|
| 2025 | TSMC COUPE platform adopted by NVIDIA, Broadcom |
|
||||||
|
| 2025 | Meta tests Broadcom CPO for 1M+ link-hours |
|
||||||
|
| 2025 Nov | Ayar Labs integrates TeraPHY into GUC ASIC workflow |
|
||||||
|
| 2026 H2 | NVIDIA Spectrum-X Photonics system ships |
|
||||||
|
| 2026-2027 | Broad CPO commercialization begins |
|
||||||
|
| 2028-2030 | Large-scale CPO deployment in hyperscale |
|
||||||
|
|
||||||
|
### CPO vs LPO vs Traditional DSP Comparison
|
||||||
|
|
||||||
|
| Feature | Traditional (DSP) | LPO (Linear Drive) | CPO (Co-Packaged) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Power Consumption | Baseline | -30 to -50% | -50 to -84% |
|
||||||
|
| Latency | ~100ns (DSP) | <15ns reduction | Near-zero electrical path |
|
||||||
|
| Serviceability | Hot-swappable | Hot-swappable | Requires board replacement |
|
||||||
|
| Maturity | Production | Shipping (NVIDIA, Meta) | Pre-production/early access |
|
||||||
|
| Cost | Baseline | Lower (no DSP in module) | Higher initially, lower at scale |
|
||||||
|
| Best For | Long reach, interop | Short reach (<2km), AI clusters | Ultra-dense, scale-up AI |
|
||||||
|
| Market Share (2025) | ~60% of 800G/1.6T | ~30% | ~5% |
|
||||||
|
| Market Share (2030, projected) | ~30% | ~40% | ~30% |
|
||||||
|
|
||||||
|
### Silicon Photonics Adoption
|
||||||
|
|
||||||
|
| Year | SiPh Share of Transceivers | Key Driver |
|
||||||
|
|---|---|---|
|
||||||
|
| 2018 | ~14% | 100G PSM4 (Intel) |
|
||||||
|
| 2020 | ~20% | 400G DR4 ramp |
|
||||||
|
| 2022 | ~25% | 400G mainstream |
|
||||||
|
| 2024 | ~35% | 800G ramp |
|
||||||
|
| 2025 | ~40-45% | 800G mainstream, 1.6T intro |
|
||||||
|
| 2030 (proj.) | ~60% | LPO + CPO adoption |
|
||||||
|
|
||||||
|
### O-Band vs C-Band Data Center Trends
|
||||||
|
|
||||||
|
| Parameter | O-Band (1310nm) | C-Band (1550nm) |
|
||||||
|
|---|---|---|
|
||||||
|
| Primary Use | Data center interconnect (<10km) | Metro/long-haul, DCI (>10km) |
|
||||||
|
| Technology | Direct detect, PAM4 | Coherent or PAM4 WDM |
|
||||||
|
| Standards | DR, FR, LR variants | ZR, ZR+, DWDM |
|
||||||
|
| Advantages | Lower cost, simpler, lower dispersion | Higher capacity, longer reach |
|
||||||
|
| Trend | Dominant for intra-DC | Growing for inter-DC via ZR/ZR+ |
|
||||||
|
| 800G Example | 800G-DR8 (O-band, 500m) | 800ZR (C-band, 120km) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Market Data Points
|
||||||
|
|
||||||
|
### Global Optical Transceiver Market Size
|
||||||
|
|
||||||
|
| Year | Market Size (USD) | YoY Growth | Key Driver |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2019 | ~$6.5B | - | 100G mainstream |
|
||||||
|
| 2020 | ~$7.0B | +8% | COVID + cloud demand |
|
||||||
|
| 2021 | ~$8.0B | +14% | 400G ramp begins |
|
||||||
|
| 2022 | ~$9.0B | +13% | 400G mainstream deployment |
|
||||||
|
| 2023 | ~$10.5B | +17% | AI infrastructure begins |
|
||||||
|
| 2024 | ~$13.6B | +30% | AI explosion, 800G ramp |
|
||||||
|
| 2025 (est.) | ~$15.6-16B | +15-18% | 800G mainstream, 1.6T intro |
|
||||||
|
| 2029 (proj.) | ~$25B | CAGR 13% | 1.6T mainstream |
|
||||||
|
| 2034 (proj.) | ~$46B | CAGR 17% | CPO + next-gen |
|
||||||
|
|
||||||
|
### Port Shipment Data
|
||||||
|
|
||||||
|
| Metric | 2023 | 2024 | 2025 (est.) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Total transceiver units deployed | ~15M | ~22.5M | ~34.5M |
|
||||||
|
| 400G+800G unit shipments | ~6M | 20M+ | 30M+ (est.) |
|
||||||
|
| Quarterly record (400/800G) | <3M | 5M+ (Q2 2024) | 7M+ (projected) |
|
||||||
|
| 400G/800G YoY growth | - | +250% | +60% (800G specifically) |
|
||||||
|
| 800G as % of high-speed | ~20% | ~35% | ~50% (est.) |
|
||||||
|
|
||||||
|
### Average Selling Price (ASP) Trends
|
||||||
|
|
||||||
|
| Speed | Launch ASP | 2024 ASP | ASP Decline Pattern |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1G SFP | ~$500 (2001) | ~$5-15 | >95% decline over 20 years |
|
||||||
|
| 10G SFP+ | ~$500 (2007) | ~$15-40 | >90% decline over 15 years |
|
||||||
|
| 25G SFP28 | ~$100 (2016) | ~$15-30 | ~75% decline over 8 years |
|
||||||
|
| 40G QSFP+ | ~$300 (2012) | ~$30-80 | ~80% decline over 12 years |
|
||||||
|
| 100G QSFP28 | ~$1,000 (2015) | ~$50-120 | ~90% decline, 60% in last 5 years |
|
||||||
|
| 400G QSFP-DD | ~$800-1,200 (2020) | ~$120-250 | ~75% decline, SR8 50% in 1 year |
|
||||||
|
| 800G OSFP | ~$800-1,000 (2023) | ~$360-450 | Early decline, still premium |
|
||||||
|
| 1.6T OSFP-XD | ~$1,300-1,500 (2025) | $1,300-1,500 | Launch pricing, projected ~$1,100 by 2027 |
|
||||||
|
|
||||||
|
### ASP Decline Model (Typical Pattern)
|
||||||
|
|
||||||
|
```
|
||||||
|
Year 0 (Launch): 100% (premium pricing)
|
||||||
|
Year 1: 80-90% (early adoption)
|
||||||
|
Year 2: 60-70% (volume ramp)
|
||||||
|
Year 3: 40-50% (mainstream)
|
||||||
|
Year 4: 30-40% (commoditization begins)
|
||||||
|
Year 5+: 20-30% (commodity, Chinese competition)
|
||||||
|
Year 7+: 10-15% (floor pricing)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Market Segment Split (2024-2025)
|
||||||
|
|
||||||
|
| Segment | 2024 Share | 2025 Share (est.) | Growth Driver |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Data Centers | 45-55% | 55-60% | AI/ML clusters, hyperscale |
|
||||||
|
| Telecommunications | 30-40% | 25-30% | 5G, coherent metro/long-haul |
|
||||||
|
| Enterprise Networking | 14-20% | 12-15% | LAN/WAN upgrades to 100G |
|
||||||
|
| Other (defense, govt, research) | 5-10% | 5-8% | Specialty applications |
|
||||||
|
|
||||||
|
### Datacom vs Telecom Module Revenue
|
||||||
|
|
||||||
|
| Year | Datacom Revenue | Telecom Revenue | Datacom Share |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2020 | ~$4B | ~$3B | 57% |
|
||||||
|
| 2022 | ~$5.5B | ~$3.5B | 61% |
|
||||||
|
| 2024 | ~$9B+ | ~$4B | 69% |
|
||||||
|
| 2025 (est.) | ~$12B+ | ~$4B | 75% |
|
||||||
|
|
||||||
|
### Coherent Pluggable Shipments
|
||||||
|
|
||||||
|
| Year | 400ZR/ZR+ Units | Key Milestone |
|
||||||
|
|---|---|---|
|
||||||
|
| 2021 | <50K | First GA shipments |
|
||||||
|
| 2022 | ~100-150K | Initial ramp |
|
||||||
|
| 2023 | ~300K | Broad deployment |
|
||||||
|
| 2024 | ~500K | Fastest adopted coherent ever |
|
||||||
|
| 2025 (est.) | ~600K+ | 800ZR enters market |
|
||||||
|
|
||||||
|
### AI Optics Market Specifically
|
||||||
|
|
||||||
|
| Year | AI Optics Market | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| 2023 | ~$3B | GPU interconnect demand begins |
|
||||||
|
| 2024 | ~$5B | NVIDIA GB200 drives 800G demand |
|
||||||
|
| 2025 | ~$7-8B | 800G mainstream for AI |
|
||||||
|
| 2026 (est.) | ~$10B+ | 1.6T for AI clusters |
|
||||||
|
|
||||||
|
### Vendor Count Per Standard (approximate)
|
||||||
|
|
||||||
|
| Standard/Type | Vendor Count (2025) | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| 1G SFP | 100+ | Fully commoditized |
|
||||||
|
| 10G SFP+ | 80+ | Commoditized |
|
||||||
|
| 25G SFP28 | 50+ | Maturing |
|
||||||
|
| 100G QSFP28 | 40+ | Maturing |
|
||||||
|
| 400G QSFP-DD | 25+ | Mainstream competition |
|
||||||
|
| 400G ZR/ZR+ | 10-15 | Specialized |
|
||||||
|
| 800G OSFP/QSFP-DD | 15-20 | Growing |
|
||||||
|
| 800G ZR/ZR+ | 5-8 | Emerging |
|
||||||
|
| 1.6T OSFP/OSFP-XD | 8-12 | Early stage |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Database Schema Recommendations
|
||||||
|
|
||||||
|
### Core Tables
|
||||||
|
|
||||||
|
```
|
||||||
|
TABLE: form_factors
|
||||||
|
- id, name, year_introduced, year_mainstream, year_decline, year_obsolete
|
||||||
|
- max_speed_gbps, connector_type, lane_count, width_mm, depth_mm
|
||||||
|
- power_class_w, backward_compatible_with, msa_spec_url
|
||||||
|
- status (emerging/active/declining/legacy/obsolete)
|
||||||
|
|
||||||
|
TABLE: speed_tiers
|
||||||
|
- id, speed_gbps, year_standardized, year_mainstream, year_peak, year_decline
|
||||||
|
- primary_ieee_standard, modulation_type, lanes_config
|
||||||
|
- typical_launch_asp_usd, current_asp_usd
|
||||||
|
|
||||||
|
TABLE: standards
|
||||||
|
- id, name, organization (IEEE/OIF/MSA), year_published, year_ratified
|
||||||
|
- speed_gbps, reach_km, key_phy_types, status
|
||||||
|
|
||||||
|
TABLE: manufacturers
|
||||||
|
- id, name, hq_country, year_founded, specialties
|
||||||
|
- annual_revenue_usd, market_rank, key_products
|
||||||
|
|
||||||
|
TABLE: market_data (time series)
|
||||||
|
- id, year, quarter, metric_name, metric_value, unit
|
||||||
|
- segment (datacom/telecom/enterprise), source
|
||||||
|
|
||||||
|
TABLE: products
|
||||||
|
- id, manufacturer_id, form_factor_id, speed_tier_id
|
||||||
|
- model_name, wavelength_nm, reach_km, fiber_type
|
||||||
|
- modulation, fec_type, power_w, temperature_range
|
||||||
|
- year_launched, current_asp_usd, status
|
||||||
|
|
||||||
|
TABLE: technology_transitions
|
||||||
|
- id, technology_name, category (modulation/integration/packaging)
|
||||||
|
- year_introduced, year_mainstream, year_peak
|
||||||
|
- market_share_pct, hype_cycle_phase
|
||||||
|
|
||||||
|
TABLE: acquisitions
|
||||||
|
- id, acquirer_id, target_name, year, value_usd, strategic_rationale
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Hype Cycle Analysis
|
||||||
|
|
||||||
|
### Technology Hype Cycle Positions (2026)
|
||||||
|
|
||||||
|
```
|
||||||
|
PEAK OF INFLATED EXPECTATIONS:
|
||||||
|
- Co-Packaged Optics (CPO)
|
||||||
|
- 3.2T transceivers
|
||||||
|
- Optical compute interconnect
|
||||||
|
|
||||||
|
SLOPE OF ENLIGHTENMENT:
|
||||||
|
- 1.6T pluggable transceivers
|
||||||
|
- Linear Drive Optics (LPO)
|
||||||
|
- 200G/lane PAM4
|
||||||
|
- Near-Packaged Optics (NPO)
|
||||||
|
|
||||||
|
PLATEAU OF PRODUCTIVITY:
|
||||||
|
- 800G pluggable transceivers
|
||||||
|
- 400ZR/ZR+ coherent pluggables
|
||||||
|
- Silicon photonics (in 400G/800G)
|
||||||
|
- PAM4 modulation
|
||||||
|
|
||||||
|
ENTERING DECLINE:
|
||||||
|
- 400G pluggable (mainstream, starting decline)
|
||||||
|
- 100G QSFP28 (commoditized)
|
||||||
|
- NRZ modulation (for new designs)
|
||||||
|
|
||||||
|
OBSOLESCENCE TRAJECTORY:
|
||||||
|
- 40G QSFP+
|
||||||
|
- 10G XFP
|
||||||
|
- CFP/CFP2/CFP4 (except DCO)
|
||||||
|
- CWDM for high-speed (>100G)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Factor Hype Cycles (Historical Overlay)
|
||||||
|
|
||||||
|
```
|
||||||
|
Innovation Peak Trough Slope Plateau
|
||||||
|
Trigger Hype Disillusion Enlighten Productivity
|
||||||
|
GBIC 1995 1998 2002 - 2000-2004
|
||||||
|
SFP 2001 2003 - 2004 2005-forever
|
||||||
|
XFP 2002 2005 2008 - 2006-2012
|
||||||
|
SFP+ 2006 2008 - 2009 2010-forever
|
||||||
|
QSFP+ 2012 2014 - 2015 2016-2022
|
||||||
|
QSFP28 2014 2016 - 2017 2018-2024
|
||||||
|
QSFP-DD 2019 2021 - 2022 2023-present
|
||||||
|
OSFP 2016 2020 - 2022 2023-present
|
||||||
|
OSFP-XD 2022 2025 - 2026(est) 2027(est)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Speed Tier Lifecycle Model
|
||||||
|
|
||||||
|
Each speed tier follows a predictable ~10-year lifecycle:
|
||||||
|
|
||||||
|
```
|
||||||
|
Years 0-2: INTRODUCTION - Standards ratified, samples shipping, $$$$ pricing
|
||||||
|
Years 2-4: GROWTH - Volume ramps, multiple vendors, pricing drops 40-60%
|
||||||
|
Years 4-6: MAINSTREAM - Peak shipments, broad adoption, pricing drops another 30-50%
|
||||||
|
Years 6-8: MATURITY - Pricing floor, commoditized, Chinese competition dominant
|
||||||
|
Years 8-10: DECLINE - Next-gen overtakes, volumes drop, maintenance-only
|
||||||
|
Years 10+: LEGACY - Minimal shipments, long-tail demand
|
||||||
|
```
|
||||||
|
|
||||||
|
| Speed | Introduction | Growth | Mainstream | Maturity | Decline | Legacy |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1G | 1998-2002 | 2002-2005 | 2005-2010 | 2010-2016 | 2016-2020 | 2020+ |
|
||||||
|
| 10G | 2002-2006 | 2006-2010 | 2010-2016 | 2016-2022 | 2022+ | - |
|
||||||
|
| 25G | 2014-2017 | 2017-2019 | 2019-2023 | 2023-2026 | 2026+ | - |
|
||||||
|
| 40G | 2010-2013 | 2013-2015 | 2015-2019 | 2019-2022 | 2022+ | - |
|
||||||
|
| 100G | 2014-2017 | 2017-2020 | 2020-2024 | 2024-2026 | 2026+ | - |
|
||||||
|
| 400G | 2020-2022 | 2022-2024 | 2024-2027 | 2027-2030 | 2030+ | - |
|
||||||
|
| 800G | 2023-2025 | 2025-2027 | 2027-2030 | 2030-2033 | 2033+ | - |
|
||||||
|
| 1.6T | 2025-2027 | 2027-2029 | 2029-2032 | 2032-2035 | 2035+ | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources & References
|
||||||
|
|
||||||
|
### Market Research
|
||||||
|
- [Cignal AI - 800GbE Optics Shipments](https://cignal.ai/2025/05/800gbe-optics-shipments-to-grow-60-in-2025/)
|
||||||
|
- [Cignal AI - 20M 400G/800G Shipments 2024](https://cignal.ai/2025/01/over-20-million-400g-800g-datacom-optical-module-shipments-expected-for-2024/)
|
||||||
|
- [LightCounting - Silicon Photonics May 2025](https://www.lightcounting.com/newsletter/en/may-2025-silicon-photonics-linear-drive-pluggable-and-cpo-updated-november-2025-334)
|
||||||
|
- [LightCounting - AI Optics Jan 2025](https://www.lightcounting.com/newsletter/en/january-2025-optics-for-ai-clusters-319)
|
||||||
|
- [Mordor Intelligence - Optical Transceiver Market](https://www.mordorintelligence.com/industry-reports/optical-transceiver-market)
|
||||||
|
- [MarketsAndMarkets - Optical Transceiver Market 2030](https://www.marketsandmarkets.com/Market-Reports/optical-transceiver-market-161339599.html)
|
||||||
|
- [Fortune Business Insights - Optical Transceiver Market](https://www.fortunebusinessinsights.com/optical-transceiver-market-108985)
|
||||||
|
|
||||||
|
### Standards & Specifications
|
||||||
|
- [IEEE 802.3 Working Group Archive](https://www.ieee802.org/3/archive.html)
|
||||||
|
- [IEEE 802.3 - Wikipedia](https://en.wikipedia.org/wiki/IEEE_802.3)
|
||||||
|
- [OIF 400ZR Implementation Agreement](https://www.oiforum.com/technical-work/hot-topics/400zr-2/)
|
||||||
|
- [OpenZR+ MSA](https://openzrplus.org/)
|
||||||
|
- [100G Lambda MSA](https://100glambda.com/)
|
||||||
|
- [CWDM4 MSA](https://cwdm4-msa.org/)
|
||||||
|
- [100G PSM4 MSA](http://www.psm4.org/)
|
||||||
|
|
||||||
|
### Form Factors & Technology
|
||||||
|
- [Prooptix - History of Form Factors](https://www.prooptix.com/news/transceiver-form-factors/)
|
||||||
|
- [Meticulous Research - 25 Years of Optical Transceiver Evolution](https://www.meticulousresearch.com/blog/207/evolution-of-optical-transceiver-technologies-in-the-last-25-years)
|
||||||
|
- [Vitex - Transceiver Form Factors Guide](https://www.vitextech.com/blogs/blog/transceiver-form-factors)
|
||||||
|
- [FS.com - High-Speed Transceivers Guide](https://www.fs.com/blog/a-comprehensive-guide-to-highspeed-transceivers-400g-800g-and-the-leap-to-16t-13767.html)
|
||||||
|
|
||||||
|
### Coherent Optics & WDM
|
||||||
|
- [WWT - 400G-ZR & ZR+ Guide](https://www.wwt.com/blog/400gzr-and-zr-the-latest-in-pluggable-coherent-dwdm)
|
||||||
|
- [Acacia/Cisco - 2024 Coherent Optics Review](https://acacia-inc.com/blog/a-look-back-at-2024-whats-ahead-for-coherent-optics-in-2025/)
|
||||||
|
- [Smartoptics - CWDM DWDM Explained](https://smartoptics.com/knowledgebank-post/cwdm-dwdm-explained/)
|
||||||
|
- [FS.com - DWDM/CWDM ITU Channels Guide](https://www.fs.com/blog/dwdmcwdm-wavelength-itu-channels-guide-3149.html)
|
||||||
|
|
||||||
|
### Manufacturer & Industry Analysis
|
||||||
|
- [Iamfabian - 800G/1.6T Transceiver Battle](https://iamfabian.substack.com/p/pluggables-power-and-geopolitics)
|
||||||
|
- [Deep Fundamental - Optical Module Market Deep Dive](https://deepfundamental.substack.com/p/deep-dive-optical-module-market)
|
||||||
|
- [Chinese Suppliers Dominate 2024 Rankings](https://www.opticaltransceivermodules.com/news/chinese-optical-transceiver-suppliers-dominate-global-rankings-225829.html)
|
||||||
|
- [Coherent Corp - Wikipedia](https://en.wikipedia.org/wiki/Coherent_Corp.)
|
||||||
|
|
||||||
|
### Next-Gen Technology
|
||||||
|
- [EDN - Co-Packaged Optics 2026](https://www.edn.com/where-co-packaged-optics-cpo-technology-stands-in-2026/)
|
||||||
|
- [IDTechEx - CPO Market Forecast](https://www.idtechex.com/en/research-report/co-packaged-optics-cpo/1138)
|
||||||
|
- [Eoptolink - Gen2 1.6T at OFC 2025](https://www.eoptolink.com/news/361-eoptolink-launches-its-gen2-1-6t-osfp-and-osfp-rhs-transceiver-family-at-ofc-2025)
|
||||||
|
- [Broadcom OFC 2025 Advances](https://investors.broadcom.com/news-releases/news-release-details/broadcom-advances-optical-connectivity-ai-infrastructure)
|
||||||
|
- [Jabil 1.6T Transceiver Launch](https://investors.jabil.com/news/news-details/2025/Jabil-Launches-1-6T-Pluggable-Transceiver-to-Support-Growing-Demand-for-Intra-Data-Center-and-AI-Connectivity/default.aspx)
|
||||||
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: timescale/timescaledb:latest-pg17
|
||||||
|
container_name: tip-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: transceiver_db
|
||||||
|
POSTGRES_USER: tip
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tip_dev_2026}
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
volumes:
|
||||||
|
- tip_pgdata:/var/lib/postgresql/data
|
||||||
|
- ./sql:/docker-entrypoint-initdb.d
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U tip -d transceiver_db"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
qdrant:
|
||||||
|
image: qdrant/qdrant:latest
|
||||||
|
container_name: tip-qdrant
|
||||||
|
ports:
|
||||||
|
- "6333:6333"
|
||||||
|
- "6334:6334"
|
||||||
|
volumes:
|
||||||
|
- tip_qdrant:/qdrant/storage
|
||||||
|
environment:
|
||||||
|
QDRANT__SERVICE__GRPC_PORT: 6334
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tip_pgdata:
|
||||||
|
tip_qdrant:
|
||||||
16
ecosystem.config.js
Normal file
16
ecosystem.config.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "tip-api",
|
||||||
|
script: "packages/api/dist/index.js",
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: "512M",
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
API_PORT: 3200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
5660
package-lock.json
generated
5660
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@ -1,45 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "transceiver-db",
|
"name": "transceiver-intelligence-platform",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"description": "Open-source optical transceiver database. 89 products, 39 IEEE/MSA standards, 16 form factors, 9 speed tiers. SFP to 800G OSFP.",
|
"private": true,
|
||||||
"main": "dist/index.js",
|
"description": "Transceiver Intelligence Platform — the world's most comprehensive optical transceiver & network switch database",
|
||||||
"types": "dist/index.d.ts",
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "npm run build --workspaces",
|
||||||
"prepublishOnly": "npm run build"
|
"build:core": "npm run build -w packages/core",
|
||||||
|
"build:api": "npm run build -w packages/api",
|
||||||
|
"dev": "npm run dev -w packages/api",
|
||||||
|
"migrate": "tsx scripts/migrate.ts",
|
||||||
|
"seed": "tsx scripts/seed-from-npm.ts",
|
||||||
|
"db:reset": "npm run migrate && npm run seed"
|
||||||
},
|
},
|
||||||
|
"author": "Rene Fichtmueller",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
|
||||||
"transceiver",
|
|
||||||
"optics",
|
|
||||||
"sfp",
|
|
||||||
"qsfp",
|
|
||||||
"networking",
|
|
||||||
"fiber",
|
|
||||||
"ieee",
|
|
||||||
"telecom",
|
|
||||||
"osfp",
|
|
||||||
"qsfp-dd",
|
|
||||||
"optical",
|
|
||||||
"datacenter",
|
|
||||||
"100g",
|
|
||||||
"400g",
|
|
||||||
"800g"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"LICENSE",
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/renefichtmueller/transceiver-db"
|
"url": "https://github.com/renefichtmueller/transceiver-db"
|
||||||
},
|
},
|
||||||
"author": "Rene Fichtmueller",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"tsx": "^4.19",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
packages/api/package.json
Normal file
28
packages/api/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@tip/api",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "TIP REST API server",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"start": "node dist/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"helmet": "^8.0.0",
|
||||||
|
"express-rate-limit": "^7.5.0",
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/pg": "^8.11.11",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"tsx": "^4.19.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/api/src/config.ts
Normal file
20
packages/api/src/config.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { config } from "dotenv";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
config({ path: join(__dirname, "..", "..", "..", ".env") });
|
||||||
|
|
||||||
|
export const cfg = {
|
||||||
|
port: parseInt(process.env.API_PORT || "3200"),
|
||||||
|
nodeEnv: process.env.NODE_ENV || "development",
|
||||||
|
db: {
|
||||||
|
host: process.env.POSTGRES_HOST || "localhost",
|
||||||
|
port: parseInt(process.env.POSTGRES_PORT || "5432"),
|
||||||
|
database: process.env.POSTGRES_DB || "transceiver_db",
|
||||||
|
user: process.env.POSTGRES_USER || "tip",
|
||||||
|
password: process.env.POSTGRES_PASSWORD || "tip_dev_2026",
|
||||||
|
},
|
||||||
|
qdrant: {
|
||||||
|
host: process.env.QDRANT_HOST || "localhost",
|
||||||
|
port: parseInt(process.env.QDRANT_PORT || "6333"),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
17
packages/api/src/db/client.ts
Normal file
17
packages/api/src/db/client.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Pool } from "pg";
|
||||||
|
import { cfg } from "../config";
|
||||||
|
|
||||||
|
export const pool = new Pool({
|
||||||
|
host: cfg.db.host,
|
||||||
|
port: cfg.db.port,
|
||||||
|
database: cfg.db.database,
|
||||||
|
user: cfg.db.user,
|
||||||
|
password: cfg.db.password,
|
||||||
|
max: 20,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
connectionTimeoutMillis: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
pool.on("error", (err) => {
|
||||||
|
console.error("Unexpected PostgreSQL error:", err);
|
||||||
|
});
|
||||||
211
packages/api/src/db/queries.ts
Normal file
211
packages/api/src/db/queries.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import { pool } from "./client";
|
||||||
|
|
||||||
|
export interface SearchParams {
|
||||||
|
q?: string;
|
||||||
|
form_factor?: string;
|
||||||
|
speed?: string;
|
||||||
|
speed_gbps?: number;
|
||||||
|
category?: string;
|
||||||
|
fiber_type?: string;
|
||||||
|
reach_min?: number;
|
||||||
|
reach_max?: number;
|
||||||
|
wdm_type?: string;
|
||||||
|
coherent?: boolean;
|
||||||
|
market_status?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchTransceivers(params: SearchParams) {
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const values: any[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
if (params.q) {
|
||||||
|
conditions.push(`search_vector @@ plainto_tsquery('english', $${idx})`);
|
||||||
|
values.push(params.q);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.form_factor) {
|
||||||
|
conditions.push(`form_factor = $${idx}`);
|
||||||
|
values.push(params.form_factor);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.speed) {
|
||||||
|
conditions.push(`speed = $${idx}`);
|
||||||
|
values.push(params.speed);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.speed_gbps) {
|
||||||
|
conditions.push(`speed_gbps = $${idx}`);
|
||||||
|
values.push(params.speed_gbps);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.category) {
|
||||||
|
conditions.push(`category = $${idx}`);
|
||||||
|
values.push(params.category);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.fiber_type) {
|
||||||
|
conditions.push(`fiber_type = $${idx}`);
|
||||||
|
values.push(params.fiber_type);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.reach_min) {
|
||||||
|
conditions.push(`reach_meters >= $${idx}`);
|
||||||
|
values.push(params.reach_min);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.reach_max) {
|
||||||
|
conditions.push(`reach_meters <= $${idx}`);
|
||||||
|
values.push(params.reach_max);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.wdm_type) {
|
||||||
|
conditions.push(`wdm_type = $${idx}`);
|
||||||
|
values.push(params.wdm_type);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.coherent !== undefined) {
|
||||||
|
conditions.push(`coherent = $${idx}`);
|
||||||
|
values.push(params.coherent);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.market_status) {
|
||||||
|
conditions.push(`market_status = $${idx}`);
|
||||||
|
values.push(params.market_status);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||||
|
const limit = params.limit || 50;
|
||||||
|
const offset = params.offset || 0;
|
||||||
|
|
||||||
|
// Add relevance ranking when full-text search is used
|
||||||
|
const orderBy = params.q
|
||||||
|
? `ORDER BY ts_rank(search_vector, plainto_tsquery('english', $1)) DESC`
|
||||||
|
: `ORDER BY speed_gbps DESC, reach_meters ASC`;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT t.*, v.name as vendor_name
|
||||||
|
FROM transceivers t
|
||||||
|
LEFT JOIN vendors v ON t.vendor_id = v.id
|
||||||
|
${where}
|
||||||
|
${orderBy}
|
||||||
|
LIMIT ${limit} OFFSET ${offset}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const countQuery = `SELECT COUNT(*) FROM transceivers ${where}`;
|
||||||
|
|
||||||
|
const [dataResult, countResult] = await Promise.all([
|
||||||
|
pool.query(query, values),
|
||||||
|
pool.query(countQuery, values),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: dataResult.rows,
|
||||||
|
total: parseInt(countResult.rows[0].count),
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTransceiverById(id: string) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT t.*, v.name as vendor_name, s.name as standard_full_name
|
||||||
|
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.id = $1 OR t.slug = $1`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchSwitches(params: SearchParams) {
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const values: any[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
if (params.q) {
|
||||||
|
conditions.push(`sw.search_vector @@ plainto_tsquery('english', $${idx})`);
|
||||||
|
values.push(params.q);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (params.category) {
|
||||||
|
conditions.push(`sw.category = $${idx}`);
|
||||||
|
values.push(params.category);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||||
|
const limit = params.limit || 50;
|
||||||
|
const offset = params.offset || 0;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT sw.*, v.name as vendor_name
|
||||||
|
FROM switches sw
|
||||||
|
LEFT JOIN vendors v ON sw.vendor_id = v.id
|
||||||
|
${where}
|
||||||
|
ORDER BY sw.max_speed_gbps DESC NULLS LAST
|
||||||
|
LIMIT ${limit} OFFSET ${offset}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await pool.query(query, values);
|
||||||
|
return { data: result.rows, limit, offset };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSwitchById(id: string) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT sw.*, v.name as vendor_name
|
||||||
|
FROM switches sw
|
||||||
|
LEFT JOIN vendors v ON sw.vendor_id = v.id
|
||||||
|
WHERE sw.id = $1`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCompatibleTransceivers(switchId: string) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT t.*, c.status, c.verified_by, c.notes as compat_notes
|
||||||
|
FROM compatibility c
|
||||||
|
JOIN transceivers t ON c.transceiver_id = t.id
|
||||||
|
WHERE c.switch_id = $1 AND c.status = 'compatible'
|
||||||
|
ORDER BY t.speed_gbps DESC`,
|
||||||
|
[switchId]
|
||||||
|
);
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listVendors(type?: string) {
|
||||||
|
const query = type
|
||||||
|
? `SELECT * FROM vendors WHERE type = $1 ORDER BY name`
|
||||||
|
: `SELECT * FROM vendors ORDER BY name`;
|
||||||
|
const result = await pool.query(query, type ? [type] : []);
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listStandards(speed?: string) {
|
||||||
|
const query = speed
|
||||||
|
? `SELECT * FROM standards WHERE speed = $1 ORDER BY year_ratified DESC`
|
||||||
|
: `SELECT * FROM standards ORDER BY year_ratified DESC`;
|
||||||
|
const result = await pool.query(query, speed ? [speed] : []);
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDbStats() {
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM vendors) as vendor_count,
|
||||||
|
(SELECT COUNT(*) FROM standards) as standard_count,
|
||||||
|
(SELECT COUNT(*) FROM transceivers) as transceiver_count,
|
||||||
|
(SELECT COUNT(*) FROM switches) as switch_count,
|
||||||
|
(SELECT COUNT(*) FROM compatibility) as compatibility_count,
|
||||||
|
(SELECT COUNT(*) FROM breakouts) as breakout_count,
|
||||||
|
(SELECT COUNT(*) FROM knowledge_base) as kb_count,
|
||||||
|
(SELECT COUNT(*) FROM documents) as document_count,
|
||||||
|
(SELECT COUNT(*) FROM news_articles) as news_count
|
||||||
|
`);
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
57
packages/api/src/index.ts
Normal file
57
packages/api/src/index.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import express from "express";
|
||||||
|
import cors from "cors";
|
||||||
|
import helmet from "helmet";
|
||||||
|
import rateLimit from "express-rate-limit";
|
||||||
|
import { cfg } from "./config";
|
||||||
|
import { transceiverRouter } from "./routes/transceivers";
|
||||||
|
import { switchRouter } from "./routes/switches";
|
||||||
|
import { vendorRouter } from "./routes/vendors";
|
||||||
|
import { standardRouter } from "./routes/standards";
|
||||||
|
import { healthRouter } from "./routes/health";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(
|
||||||
|
rateLimit({
|
||||||
|
windowMs: 60 * 1000,
|
||||||
|
max: 200,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use("/api/transceivers", transceiverRouter);
|
||||||
|
app.use("/api/switches", switchRouter);
|
||||||
|
app.use("/api/vendors", vendorRouter);
|
||||||
|
app.use("/api/standards", standardRouter);
|
||||||
|
app.use("/api/health", healthRouter);
|
||||||
|
|
||||||
|
// Root
|
||||||
|
app.get("/", (_req, res) => {
|
||||||
|
res.json({
|
||||||
|
name: "Transceiver Intelligence Platform",
|
||||||
|
version: "0.1.0",
|
||||||
|
endpoints: [
|
||||||
|
"GET /api/transceivers?q=&form_factor=&speed=&category=&fiber_type=&wdm_type=&coherent=",
|
||||||
|
"GET /api/transceivers/:id",
|
||||||
|
"GET /api/switches?q=&category=",
|
||||||
|
"GET /api/switches/:id",
|
||||||
|
"GET /api/switches/:id/compatibility",
|
||||||
|
"GET /api/vendors?type=",
|
||||||
|
"GET /api/standards?speed=",
|
||||||
|
"GET /api/health",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start
|
||||||
|
app.listen(cfg.port, () => {
|
||||||
|
console.log(`\n TIP API running on http://localhost:${cfg.port}`);
|
||||||
|
console.log(` Environment: ${cfg.nodeEnv}`);
|
||||||
|
console.log(` Database: ${cfg.db.host}:${cfg.db.port}/${cfg.db.database}\n`);
|
||||||
|
});
|
||||||
32
packages/api/src/routes/health.ts
Normal file
32
packages/api/src/routes/health.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import { getDbStats } from "../db/queries";
|
||||||
|
import { pool } from "../db/client";
|
||||||
|
|
||||||
|
export const healthRouter = Router();
|
||||||
|
|
||||||
|
// GET /api/health — Health check with DB stats
|
||||||
|
healthRouter.get("/", async (_req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const start = Date.now();
|
||||||
|
const stats = await getDbStats();
|
||||||
|
const latencyMs = Date.now() - start;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
status: "healthy",
|
||||||
|
version: "0.1.0",
|
||||||
|
uptime: process.uptime(),
|
||||||
|
database: {
|
||||||
|
connected: true,
|
||||||
|
latency_ms: latencyMs,
|
||||||
|
stats,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(503).json({
|
||||||
|
success: false,
|
||||||
|
status: "unhealthy",
|
||||||
|
database: { connected: false, error: String(err) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
15
packages/api/src/routes/standards.ts
Normal file
15
packages/api/src/routes/standards.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import { listStandards } from "../db/queries";
|
||||||
|
|
||||||
|
export const standardRouter = Router();
|
||||||
|
|
||||||
|
// GET /api/standards — List all standards
|
||||||
|
standardRouter.get("/", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const standards = await listStandards(req.query.speed ? String(req.query.speed) : undefined);
|
||||||
|
res.json({ success: true, data: standards, total: standards.length });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("List standards error:", err);
|
||||||
|
res.status(500).json({ success: false, error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
46
packages/api/src/routes/switches.ts
Normal file
46
packages/api/src/routes/switches.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import { searchSwitches, getSwitchById, getCompatibleTransceivers } from "../db/queries";
|
||||||
|
|
||||||
|
export const switchRouter = Router();
|
||||||
|
|
||||||
|
// GET /api/switches — Search/list switches
|
||||||
|
switchRouter.get("/", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const result = await searchSwitches({
|
||||||
|
q: String(req.query.q || ""),
|
||||||
|
category: req.query.category ? String(req.query.category) : undefined,
|
||||||
|
limit: req.query.limit ? parseInt(String(req.query.limit)) : 50,
|
||||||
|
offset: req.query.offset ? parseInt(String(req.query.offset)) : 0,
|
||||||
|
});
|
||||||
|
res.json({ success: true, ...result });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Search switches error:", err);
|
||||||
|
res.status(500).json({ success: false, error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/switches/:id — Get single switch
|
||||||
|
switchRouter.get("/:id", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const sw = await getSwitchById(String(req.params.id));
|
||||||
|
if (!sw) {
|
||||||
|
res.status(404).json({ success: false, error: "Switch not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ success: true, data: sw });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Get switch error:", err);
|
||||||
|
res.status(500).json({ success: false, error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/switches/:id/compatibility — Compatible transceivers for a switch
|
||||||
|
switchRouter.get("/:id/compatibility", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const transceivers = await getCompatibleTransceivers(String(req.params.id));
|
||||||
|
res.json({ success: true, data: transceivers, total: transceivers.length });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Get compatibility error:", err);
|
||||||
|
res.status(500).json({ success: false, error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
45
packages/api/src/routes/transceivers.ts
Normal file
45
packages/api/src/routes/transceivers.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import { searchTransceivers, getTransceiverById } from "../db/queries";
|
||||||
|
|
||||||
|
export const transceiverRouter = Router();
|
||||||
|
|
||||||
|
// GET /api/transceivers — Search/list transceivers
|
||||||
|
transceiverRouter.get("/", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const q = (p: string) => req.query[p] ? String(req.query[p]) : undefined;
|
||||||
|
const result = await searchTransceivers({
|
||||||
|
q: q("q"),
|
||||||
|
form_factor: q("form_factor"),
|
||||||
|
speed: q("speed"),
|
||||||
|
speed_gbps: q("speed_gbps") ? parseFloat(q("speed_gbps")!) : undefined,
|
||||||
|
category: q("category"),
|
||||||
|
fiber_type: q("fiber_type"),
|
||||||
|
reach_min: q("reach_min") ? parseInt(q("reach_min")!) : undefined,
|
||||||
|
reach_max: q("reach_max") ? parseInt(q("reach_max")!) : undefined,
|
||||||
|
wdm_type: q("wdm_type"),
|
||||||
|
coherent: q("coherent") === "true" ? true : q("coherent") === "false" ? false : undefined,
|
||||||
|
market_status: q("market_status"),
|
||||||
|
limit: q("limit") ? parseInt(q("limit")!) : 50,
|
||||||
|
offset: q("offset") ? parseInt(q("offset")!) : 0,
|
||||||
|
});
|
||||||
|
res.json({ success: true, ...result });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Search transceivers error:", err);
|
||||||
|
res.status(500).json({ success: false, error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/transceivers/:id — Get single transceiver
|
||||||
|
transceiverRouter.get("/:id", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const transceiver = await getTransceiverById(String(req.params.id));
|
||||||
|
if (!transceiver) {
|
||||||
|
res.status(404).json({ success: false, error: "Transceiver not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ success: true, data: transceiver });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Get transceiver error:", err);
|
||||||
|
res.status(500).json({ success: false, error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
15
packages/api/src/routes/vendors.ts
Normal file
15
packages/api/src/routes/vendors.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import { listVendors } from "../db/queries";
|
||||||
|
|
||||||
|
export const vendorRouter = Router();
|
||||||
|
|
||||||
|
// GET /api/vendors — List all vendors
|
||||||
|
vendorRouter.get("/", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const vendors = await listVendors(req.query.type ? String(req.query.type) : undefined);
|
||||||
|
res.json({ success: true, data: vendors, total: vendors.length });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("List vendors error:", err);
|
||||||
|
res.status(500).json({ success: false, error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
19
packages/api/tsconfig.json
Normal file
19
packages/api/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
45
packages/core/package.json
Normal file
45
packages/core/package.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "@tip/core",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Core optical transceiver database. 159 products, 42 IEEE/MSA standards, 16 form factors, 9 speed tiers.",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"transceiver",
|
||||||
|
"optics",
|
||||||
|
"sfp",
|
||||||
|
"qsfp",
|
||||||
|
"networking",
|
||||||
|
"fiber",
|
||||||
|
"ieee",
|
||||||
|
"telecom",
|
||||||
|
"osfp",
|
||||||
|
"qsfp-dd",
|
||||||
|
"optical",
|
||||||
|
"datacenter",
|
||||||
|
"100g",
|
||||||
|
"400g",
|
||||||
|
"800g"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/renefichtmueller/transceiver-db"
|
||||||
|
},
|
||||||
|
"author": "Rene Fichtmueller",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/core/src/breakouts.ts
Normal file
20
packages/core/src/breakouts.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Breakout cable configurations.
|
||||||
|
* Maps high-speed ports to multiple lower-speed connections.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Breakout } from "./types";
|
||||||
|
|
||||||
|
export const breakouts: readonly Breakout[] = [
|
||||||
|
{ id: "40g-4x10g-sr", from: "40GBASE-SR4", to: "4x 10GBASE-SR", formFactor: "QSFP+ to 4x SFP+", description: "Break out one 40G QSFP+ port into four 10G SFP+ ports. Uses MPO-to-LC harness.", cableType: "Passive", maxLength: "5m (passive) / 100m (active)", speedPerLane: "10G" },
|
||||||
|
{ id: "40g-4x10g-aoc", from: "40GBASE-SR4", to: "4x 10GBASE-SR", formFactor: "QSFP+ to 4x SFP+ AOC", description: "Active optical breakout cable from 40G QSFP+ to four 10G SFP+.", cableType: "Active", maxLength: "1-30m", speedPerLane: "10G" },
|
||||||
|
{ id: "100g-4x25g-sr", from: "100GBASE-SR4", to: "4x 25GBASE-SR", formFactor: "QSFP28 to 4x SFP28", description: "Break out one 100G QSFP28 port into four 25G SFP28 ports.", cableType: "Passive", maxLength: "5m (passive) / 100m (active)", speedPerLane: "25G" },
|
||||||
|
{ id: "100g-4x25g-dac", from: "100G QSFP28", to: "4x 25G SFP28", formFactor: "QSFP28 to 4x SFP28 DAC", description: "Passive copper breakout DAC from 100G QSFP28 to four 25G SFP28.", cableType: "Passive", maxLength: "1-5m", speedPerLane: "25G" },
|
||||||
|
{ id: "400g-4x100g-dr", from: "400GBASE-DR4", to: "4x 100GBASE-DR", formFactor: "QSFP-DD to 4x QSFP28 (MPO-12 to 4x LC)", description: "Break out one 400G DR4 port into four 100G DR ports. Parallel SMF to duplex LC.", cableType: "Passive", maxLength: "500m (fiber reach)", speedPerLane: "100G" },
|
||||||
|
{ id: "400g-4x100g-fr", from: "400GBASE-XDR4", to: "4x 100GBASE-FR1", formFactor: "QSFP-DD to 4x QSFP28", description: "Break out one 400G XDR4 port into four 100G FR1 ports. 2km reach per lane.", cableType: "Passive", maxLength: "2km (fiber reach)", speedPerLane: "100G" },
|
||||||
|
{ id: "400g-4x100g-lr", from: "400G-PLR4", to: "4x 100GBASE-LR1", formFactor: "QSFP-DD to 4x QSFP28", description: "Break out one 400G PLR4 port into four 100G LR1 ports. 10km reach per lane.", cableType: "Passive", maxLength: "10km (fiber reach)", speedPerLane: "100G" },
|
||||||
|
{ id: "800g-2x400g-dr", from: "800GBASE-DR8", to: "2x 400GBASE-DR4", formFactor: "OSFP to 2x QSFP-DD", description: "Break out one 800G DR8 port into two 400G DR4 ports.", cableType: "Passive", maxLength: "500m (fiber reach)", speedPerLane: "100G" },
|
||||||
|
{ id: "800g-8x100g-dr", from: "800GBASE-DR8", to: "8x 100GBASE-DR", formFactor: "OSFP to 8x QSFP28", description: "Break out one 800G DR8 port into eight 100G DR ports.", cableType: "Passive", maxLength: "500m (fiber reach)", speedPerLane: "100G" },
|
||||||
|
{ id: "200g-4x50g-sr", from: "200GBASE-SR4", to: "4x 50GBASE-SR", formFactor: "QSFP56 to 4x SFP56", description: "Break out one 200G SR4 port into four 50G SR ports.", cableType: "Passive", maxLength: "100m (OM4)", speedPerLane: "50G" },
|
||||||
|
{ id: "200g-2x100g-dr", from: "200GBASE-DR4", to: "2x 100GBASE-DR", formFactor: "QSFP56 to 2x QSFP28", description: "Break out one 200G DR4 port into two 100G DR ports.", cableType: "Passive", maxLength: "500m (fiber reach)", speedPerLane: "50G" },
|
||||||
|
];
|
||||||
217
packages/core/src/database.ts
Normal file
217
packages/core/src/database.ts
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
/**
|
||||||
|
* Transceiver product database — 159 optical transceiver products.
|
||||||
|
* Covers GBIC, XFP, SFP, SFP+, SFP28, SFP56, QSFP+, QSFP28, QSFP56,
|
||||||
|
* QSFP-DD, OSFP, CFP/CFP2/CFP4, CFP2-DCO, CXP, DAC, and AOC.
|
||||||
|
*
|
||||||
|
* Speeds: 1G, 10G, 25G, 40G, 50G, 100G, 200G, 400G, 800G.
|
||||||
|
*
|
||||||
|
* Data sourced from publicly available IEEE standards, MSA specifications,
|
||||||
|
* vendor datasheets, and industry documentation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Transceiver, VendorCompat } from "./types";
|
||||||
|
|
||||||
|
// Vendor shorthand helpers (internal)
|
||||||
|
const V_CISCO: VendorCompat = { vendor: "Cisco", partPattern: "SFP-*-*" };
|
||||||
|
const V_JUNIPER: VendorCompat = { vendor: "Juniper", partPattern: "EX-SFP-*" };
|
||||||
|
const V_ARISTA: VendorCompat = { vendor: "Arista", partPattern: "SFP-*-*" };
|
||||||
|
const V_HUAWEI: VendorCompat = { vendor: "Huawei", partPattern: "SFP-*-*" };
|
||||||
|
const V_NOKIA: VendorCompat = { vendor: "Nokia", partPattern: "3HE*" };
|
||||||
|
const V_HPE: VendorCompat = { vendor: "HPE/Aruba", partPattern: "J*" };
|
||||||
|
const V_DELL: VendorCompat = { vendor: "Dell", partPattern: "407-*" };
|
||||||
|
const V_EXTREME: VendorCompat = { vendor: "Extreme", partPattern: "10*" };
|
||||||
|
const V_NVIDIA: VendorCompat = { vendor: "NVIDIA", partPattern: "MMS4*|MMA1*" };
|
||||||
|
|
||||||
|
function allMajorVendors(): VendorCompat[] {
|
||||||
|
return [V_CISCO, V_JUNIPER, V_ARISTA, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME];
|
||||||
|
}
|
||||||
|
|
||||||
|
function dcVendors(): VendorCompat[] {
|
||||||
|
return [V_CISCO, V_JUNIPER, V_ARISTA, V_HUAWEI, V_DELL, V_EXTREME];
|
||||||
|
}
|
||||||
|
|
||||||
|
function carrierVendors(): VendorCompat[] {
|
||||||
|
return [V_CISCO, V_JUNIPER, V_HUAWEI, V_NOKIA];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete transceiver database — all 159 products.
|
||||||
|
*/
|
||||||
|
export const transceivers: readonly Transceiver[] = [
|
||||||
|
// ── GBIC — 1G Legacy ──
|
||||||
|
{ id: "gbic-sx", standard: "1000BASE-SX", formFactor: "GBIC", speed: "1G", speedGbps: 1, reachMeters: 550, reachLabel: "550m", fiberType: "MMF", wavelengths: "850nm", connector: "SC", powerConsumptionW: 1.5, tempRange: "COM", category: "Legacy", priceTier: "Budget", useCase: "Legacy 1G multimode short-reach links in older switches using GBIC slots.", vendors: [{ vendor: "Cisco", partPattern: "WS-G5484" }, { vendor: "Juniper", partPattern: "SRX-SFP-1GE-SX" }], tags: ["1G", "legacy", "multimode", "short-reach", "GBIC"] },
|
||||||
|
{ id: "gbic-lx", standard: "1000BASE-LX", formFactor: "GBIC", speed: "1G", speedGbps: 1, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "SC", powerConsumptionW: 1.5, tempRange: "COM", category: "Legacy", priceTier: "Budget", useCase: "Legacy 1G single-mode links up to 10km in older GBIC-slot equipment.", vendors: [{ vendor: "Cisco", partPattern: "WS-G5486" }], tags: ["1G", "legacy", "singlemode", "10km", "GBIC"] },
|
||||||
|
|
||||||
|
// ── SFP — 1G ──
|
||||||
|
{ id: "sfp-sx", standard: "1000BASE-SX", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 550, reachLabel: "550m", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 0.8, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "Standard 1G multimode short-reach for data center and campus backbone links.", vendors: [{ vendor: "Cisco", partPattern: "GLC-SX-MMD" }, { vendor: "Juniper", partPattern: "EX-SFP-1GE-SX" }, { vendor: "Arista", partPattern: "SFP-1G-SX" }, { vendor: "Huawei", partPattern: "SFP-GE-SX" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["1G", "multimode", "short-reach", "campus", "SFP"] },
|
||||||
|
{ id: "sfp-lx", standard: "1000BASE-LX", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 0.8, tempRange: "COM", category: "Metro", priceTier: "Budget", useCase: "1G single-mode for campus/metro links up to 10km. Most common 1G SFP.", vendors: [{ vendor: "Cisco", partPattern: "GLC-LH-SMD" }, { vendor: "Juniper", partPattern: "EX-SFP-1GE-LX" }, { vendor: "Arista", partPattern: "SFP-1G-LX" }, { vendor: "Huawei", partPattern: "SFP-GE-LX-SM1310" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["1G", "singlemode", "10km", "metro", "campus", "SFP"] },
|
||||||
|
{ id: "sfp-zx", standard: "1000BASE-ZX", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 80000, reachLabel: "80km", fiberType: "SMF", wavelengths: "1550nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "LongHaul", priceTier: "Standard", useCase: "1G long-haul single-mode up to 80km for inter-city or metro ring links.", vendors: [{ vendor: "Cisco", partPattern: "GLC-ZX-SMD" }, { vendor: "Juniper", partPattern: "SFP-1GE-LH" }, V_HUAWEI, V_NOKIA], tags: ["1G", "singlemode", "80km", "long-haul", "SFP"] },
|
||||||
|
{ id: "sfp-t", standard: "1000BASE-T", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 100, reachLabel: "100m", fiberType: "Copper", wavelengths: "N/A", connector: "RJ45", powerConsumptionW: 1.0, tempRange: "COM", category: "Access", priceTier: "Budget", useCase: "1G copper SFP for short-reach RJ45 connections to servers or endpoints.", vendors: allMajorVendors(), tags: ["1G", "copper", "RJ45", "access", "SFP"] },
|
||||||
|
{ id: "sfp-bidi-1310-1550", standard: "1000BASE-BX10", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm TX / 1550nm RX", connector: "LC", powerConsumptionW: 0.8, tempRange: "COM", category: "BiDi", priceTier: "Standard", useCase: "Bidirectional 1G over a single fiber strand. Sold in pairs (upstream/downstream). Saves fiber.", vendors: [{ vendor: "Cisco", partPattern: "GLC-BX-U / GLC-BX-D" }, { vendor: "Juniper", partPattern: "SFP-1GE-BX" }, V_ARISTA, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["1G", "BiDi", "single-fiber", "singlemode", "10km", "SFP"] },
|
||||||
|
{ id: "sfp-cwdm-1470", standard: "SFP CWDM", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 80000, reachLabel: "40-80km", fiberType: "SMF", wavelengths: "1470-1610nm (8 channels, 20nm spacing)", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "CWDM", priceTier: "Standard", useCase: "1G CWDM for multiplexing up to 8 channels on a single fiber pair. Cost-effective WDM.", vendors: [{ vendor: "Cisco", partPattern: "CWDM-SFP-*" }, { vendor: "Juniper", partPattern: "SFP-1GE-CWDM*" }, V_HUAWEI, V_NOKIA], tags: ["1G", "CWDM", "WDM", "multiplexing", "SFP"] },
|
||||||
|
|
||||||
|
// ── XFP — 10G Legacy ──
|
||||||
|
{ id: "xfp-sr", standard: "10GBASE-SR", formFactor: "XFP", speed: "10G", speedGbps: 10, reachMeters: 300, reachLabel: "300m", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "Legacy", priceTier: "Budget", useCase: "Legacy 10G short-reach for older platforms with XFP slots. Replaced by SFP+ in modern gear.", vendors: [{ vendor: "Cisco", partPattern: "XFP-10G-MM-SR" }, { vendor: "Juniper", partPattern: "XFP-10G-S" }], tags: ["10G", "legacy", "multimode", "short-reach", "XFP"] },
|
||||||
|
{ id: "xfp-lr", standard: "10GBASE-LR", formFactor: "XFP", speed: "10G", speedGbps: 10, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "Legacy", priceTier: "Budget", useCase: "Legacy 10G single-mode for metro links on older XFP-based routers.", vendors: [{ vendor: "Cisco", partPattern: "XFP-10GLR-OC192SR" }, { vendor: "Juniper", partPattern: "XFP-10G-L-OC192-SR1" }], tags: ["10G", "legacy", "singlemode", "10km", "XFP"] },
|
||||||
|
{ id: "xfp-er", standard: "10GBASE-ER", formFactor: "XFP", speed: "10G", speedGbps: 10, reachMeters: 40000, reachLabel: "40km", fiberType: "SMF", wavelengths: "1550nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "Legacy", priceTier: "Standard", useCase: "Legacy 10G extended reach for metro/regional links on XFP platforms.", vendors: [{ vendor: "Cisco", partPattern: "XFP-10GER-OC192IR" }, { vendor: "Juniper", partPattern: "XFP-10GE-ER" }], tags: ["10G", "legacy", "singlemode", "40km", "XFP", "extended-reach"] },
|
||||||
|
|
||||||
|
// ── SFP+ — 10G ──
|
||||||
|
{ id: "sfpp-sr", standard: "10GBASE-SR", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 300, reachLabel: "300m (OM3) / 400m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "The workhorse of 10G data center connectivity. Short-reach multimode for server-to-switch links.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-SR" }, { vendor: "Juniper", partPattern: "EX-SFP-10GE-SR" }, { vendor: "Arista", partPattern: "SFP-10G-SR" }, { vendor: "Huawei", partPattern: "SFP-10G-USR" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["10G", "multimode", "short-reach", "data-center", "server", "SFP+"] },
|
||||||
|
{ id: "sfpp-lr", standard: "10GBASE-LR", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "Metro", priceTier: "Budget", useCase: "10G single-mode for campus backbone and metro links up to 10km.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-LR" }, { vendor: "Juniper", partPattern: "EX-SFP-10GE-LR" }, { vendor: "Arista", partPattern: "SFP-10G-LR" }, { vendor: "Huawei", partPattern: "SFP-10G-LR" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["10G", "singlemode", "10km", "metro", "campus", "SFP+"] },
|
||||||
|
{ id: "sfpp-er", standard: "10GBASE-ER", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 40000, reachLabel: "40km", fiberType: "SMF", wavelengths: "1550nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "10G extended reach for metro rings and inter-building links up to 40km.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-ER" }, { vendor: "Juniper", partPattern: "EX-SFP-10GE-ER" }, { vendor: "Arista", partPattern: "SFP-10G-ER" }, V_HUAWEI, V_NOKIA], tags: ["10G", "singlemode", "40km", "metro", "extended-reach", "SFP+"] },
|
||||||
|
{ id: "sfpp-zr", standard: "10GBASE-ZR", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 80000, reachLabel: "80km", fiberType: "SMF", wavelengths: "1550nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "LongHaul", priceTier: "Standard", useCase: "10G long-reach for inter-city metro and regional network links up to 80km.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-ZR" }, { vendor: "Juniper", partPattern: "SFP-10GE-ZR" }, V_HUAWEI, V_NOKIA], tags: ["10G", "singlemode", "80km", "long-haul", "SFP+"] },
|
||||||
|
{ id: "sfpp-bidi-10", standard: "10GBASE-BX", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1270nm TX / 1330nm RX", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "BiDi", priceTier: "Standard", useCase: "Bidirectional 10G over single fiber. Pairs required (upstream/downstream). Halves fiber count.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-BXU-I / SFP-10G-BXD-I" }, { vendor: "Juniper", partPattern: "SFP-10GE-BX" }, V_ARISTA, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["10G", "BiDi", "single-fiber", "singlemode", "10km", "SFP+"] },
|
||||||
|
{ id: "sfpp-cwdm", standard: "10G SFP+ CWDM", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 80000, reachLabel: "40-80km", fiberType: "SMF", wavelengths: "1270-1610nm (18 channels, 20nm spacing)", connector: "LC", powerConsumptionW: 1.2, tempRange: "COM", category: "CWDM", priceTier: "Standard", useCase: "10G CWDM SFP+ for WDM multiplexing. Up to 18 channels over a single fiber pair.", vendors: [{ vendor: "Cisco", partPattern: "CWDM-SFP10G-*" }, { vendor: "Juniper", partPattern: "SFP-10GE-CWDM*" }, V_HUAWEI, V_NOKIA], tags: ["10G", "CWDM", "WDM", "multiplexing", "SFP+"] },
|
||||||
|
{ id: "sfpp-dwdm", standard: "10G SFP+ DWDM", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 80000, reachLabel: "40-80km", fiberType: "SMF", wavelengths: "C-band (1528-1565nm, 80+ channels, 100GHz/50GHz spacing)", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "DWDM", priceTier: "Premium", useCase: "10G DWDM for high-density wavelength multiplexing. 80+ channels on C-band. Fixed or tunable.", vendors: [{ vendor: "Cisco", partPattern: "DWDM-SFP10G-*" }, { vendor: "Juniper", partPattern: "SFP-10GE-DWDM*" }, V_HUAWEI, V_NOKIA], tags: ["10G", "DWDM", "WDM", "C-band", "tunable", "SFP+"] },
|
||||||
|
{ id: "sfpp-t", standard: "10GBASE-T", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 30, reachLabel: "30m", fiberType: "Copper", wavelengths: "N/A", connector: "RJ45", powerConsumptionW: 2.5, tempRange: "COM", category: "Access", priceTier: "Budget", useCase: "10G copper SFP+ for short-reach RJ45 connections. Higher power than fiber variants.", vendors: allMajorVendors(), tags: ["10G", "copper", "RJ45", "access", "SFP+"] },
|
||||||
|
{ id: "sfpp-usr", standard: "10GBASE-USR", ieeeReference: "SFF-8431", formFactor: "SFP+", speed: "10G", speedGbps: 10, lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", reachMeters: 100, reachLabel: "10-100m (OM3/OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "Ultra short-reach 10G for within-rack and adjacent-rack connections.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-SR" }, { vendor: "Huawei", partPattern: "SFP-10G-USR" }], tags: ["10G", "multimode", "ultra-short-reach", "data-center", "SFP+"], generation: "Gen1 NRZ", marketStatus: "Mainstream" },
|
||||||
|
{ id: "sfpp-lrm", standard: "10GBASE-LRM", ieeeReference: "IEEE 802.3aq", formFactor: "SFP+", speed: "10G", speedGbps: 10, lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", reachMeters: 220, reachLabel: "220m (FDDI/OM1 legacy MMF)", fiberType: "MMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "10G over legacy multimode fiber (OM1/OM2). Uses 1310nm to achieve 220m on older fiber.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-LRM" }, { vendor: "Juniper", partPattern: "EX-SFP-10GE-LRM" }], tags: ["10G", "multimode", "legacy-fiber", "LRM", "SFP+"], generation: "Gen1 NRZ", marketStatus: "Legacy", yearIntroduced: 2006 },
|
||||||
|
|
||||||
|
// ── SFP28 — 25G ──
|
||||||
|
{ id: "sfp28-sr", standard: "25GBASE-SR", ieeeReference: "IEEE 802.3by", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "Standard 25G data center server access. Replaced 10G SFP+ in hyperscale deployments.", vendors: [{ vendor: "Cisco", partPattern: "SFP-25G-SR-S" }, { vendor: "Juniper", partPattern: "SFP-25G-SR" }, { vendor: "Arista", partPattern: "SFP-25G-SR" }, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["25G", "multimode", "short-reach", "data-center", "server", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Mainstream", yearIntroduced: 2016 },
|
||||||
|
{ id: "sfp28-lr", standard: "25GBASE-LR", ieeeReference: "IEEE 802.3cc", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "25G single-mode for campus backbone and 5G fronthaul links up to 10km.", vendors: [{ vendor: "Cisco", partPattern: "SFP-25G-LR-S" }, { vendor: "Juniper", partPattern: "SFP-25G-LR" }, { vendor: "Arista", partPattern: "SFP-25G-LR" }, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["25G", "singlemode", "10km", "metro", "5G-fronthaul", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Growth", yearIntroduced: 2017 },
|
||||||
|
{ id: "sfp28-er", standard: "25GBASE-ER", ieeeReference: "IEEE 802.3cc", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", reachMeters: 30000, reachLabel: "30km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "25G extended reach for metro and 5G midhaul applications.", vendors: [{ vendor: "Cisco", partPattern: "SFP-25G-ER-S" }, { vendor: "Juniper", partPattern: "SFP-25G-ER" }], tags: ["25G", "singlemode", "30km", "metro", "5G-midhaul", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Growth", yearIntroduced: 2017 },
|
||||||
|
{ id: "sfp28-bidi", standard: "25GBASE-BX", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, modulation: "NRZ", reachMeters: 10000, reachLabel: "10-30km", fiberType: "SMF", wavelengths: "1270nm TX / 1330nm RX (or reverse)", connector: "LC", powerConsumptionW: 1.2, tempRange: "COM", category: "BiDi", priceTier: "Standard", useCase: "Bidirectional 25G over single fiber. Sold in pairs. Ideal for 5G fronthaul with limited fiber.", vendors: [{ vendor: "Cisco", partPattern: "SFP-25G-BX*" }, V_JUNIPER, V_ARISTA, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["25G", "BiDi", "single-fiber", "singlemode", "5G", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Growth" },
|
||||||
|
{ id: "sfp28-cwdm", standard: "25G SFP28 CWDM", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, modulation: "NRZ", reachMeters: 40000, reachLabel: "10-40km", fiberType: "SMF", wavelengths: "1270-1610nm (CWDM channels, 20nm spacing)", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "CWDM", priceTier: "Standard", useCase: "25G CWDM for wavelength multiplexing. Enables multiple 25G channels over a single fiber pair for 5G midhaul aggregation.", vendors: carrierVendors(), tags: ["25G", "CWDM", "WDM", "5G", "aggregation", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Growth" },
|
||||||
|
{ id: "sfp28-lr-ind", standard: "25GBASE-LR Industrial", ieeeReference: "IEEE 802.3cc", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, modulation: "NRZ", reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.2, tempRange: "IND", category: "5G", priceTier: "Premium", useCase: "Industrial temperature 25G SFP28 for 5G fronthaul in outdoor cabinets. Rated -40C to +85C.", vendors: carrierVendors(), tags: ["25G", "industrial", "outdoor", "5G", "fronthaul", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Growth" },
|
||||||
|
|
||||||
|
// ── SFP56 — 50G ──
|
||||||
|
{ id: "sfp56-sr", standard: "50GBASE-SR", ieeeReference: "IEEE 802.3cd", formFactor: "SFP56", speed: "50G", speedGbps: 50, lanes: 1, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "50G single-lane for emerging high-density server access. Used in 200G-SR4 breakout scenarios.", vendors: dcVendors(), tags: ["50G", "multimode", "short-reach", "PAM4", "SFP56", "data-center"], generation: "Gen2 PAM4", marketStatus: "Growth", yearIntroduced: 2019 },
|
||||||
|
{ id: "sfp56-lr", standard: "50GBASE-LR", formFactor: "SFP56", speed: "50G", speedGbps: 50, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "50G single-mode for high-bandwidth campus and metro links.", vendors: dcVendors(), tags: ["50G", "PAM4", "singlemode", "10km", "SFP56"] },
|
||||||
|
|
||||||
|
// ── QSFP+ — 40G ──
|
||||||
|
{ id: "qsfpp-sr4", standard: "40GBASE-SR4", formFactor: "QSFP+", speed: "40G", speedGbps: 40, reachMeters: 150, reachLabel: "100m (OM3) / 150m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "MPO-12", powerConsumptionW: 2.5, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "Standard 40G multimode for data center spine-leaf links using MPO cabling.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-40G-SR4" }, { vendor: "Juniper", partPattern: "QSFP-40G-SR4" }, { vendor: "Arista", partPattern: "QSFP-40G-SR4" }, { vendor: "Huawei", partPattern: "QSFP-40G-SR4" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["40G", "multimode", "short-reach", "data-center", "MPO", "QSFP+"] },
|
||||||
|
{ id: "qsfpp-lr4", standard: "40GBASE-LR4", formFactor: "QSFP+", speed: "40G", speedGbps: 40, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (4 CWDM lanes)", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "40G single-mode for campus backbone and DCI links up to 10km using LC duplex fiber.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-40G-LR4" }, { vendor: "Juniper", partPattern: "QSFP-40G-LR4" }, { vendor: "Arista", partPattern: "QSFP-40G-LR4" }, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["40G", "singlemode", "10km", "metro", "CWDM", "DCI", "QSFP+"] },
|
||||||
|
{ id: "qsfpp-er4", standard: "40GBASE-ER4", formFactor: "QSFP+", speed: "40G", speedGbps: 40, reachMeters: 40000, reachLabel: "40km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (4 CWDM lanes)", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "40G extended reach for metro and DCI links up to 40km.", vendors: carrierVendors(), tags: ["40G", "singlemode", "40km", "metro", "extended-reach", "QSFP+"] },
|
||||||
|
{ id: "qsfpp-sr-bidi", standard: "40GBASE-SR-BiDi", formFactor: "QSFP+", speed: "40G", speedGbps: 40, reachMeters: 150, reachLabel: "100m (OM3) / 150m (OM4)", fiberType: "MMF", wavelengths: "832nm / 918nm BiDi", connector: "LC", powerConsumptionW: 2.5, tempRange: "COM", category: "BiDi", priceTier: "Standard", useCase: "40G BiDi multimode over LC duplex. Enables 40G upgrade reusing existing 10G LC cabling.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-40G-SR-BD" }, { vendor: "Arista", partPattern: "QSFP-40G-SRBD" }, V_JUNIPER, V_HUAWEI, V_DELL, V_EXTREME], tags: ["40G", "BiDi", "multimode", "LC-reuse", "data-center", "QSFP+"] },
|
||||||
|
|
||||||
|
// ── QSFP28 — 100G ──
|
||||||
|
{ id: "qsfp28-sr4", standard: "100GBASE-SR4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "MPO-12", powerConsumptionW: 3.5, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "Standard 100G multimode for data center spine-leaf. The most deployed 100G optic. Breakout to 4x25G possible.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-SR4-S" }, { vendor: "Juniper", partPattern: "QSFP-100G-SR4" }, { vendor: "Arista", partPattern: "QSFP-100G-SR4" }, { vendor: "Huawei", partPattern: "QSFP-100G-SR4" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["100G", "multimode", "short-reach", "data-center", "MPO", "breakout", "QSFP28", "IXP"] },
|
||||||
|
{ id: "qsfp28-sr1", standard: "100GBASE-SR1", ieeeReference: "IEEE 802.3cd", formFactor: "QSFP28", speed: "100G", speedGbps: 100, lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "100G single-lane over duplex LC MMF. Uses PAM4 modulation. Simpler and cheaper than SR4.", vendors: dcVendors(), tags: ["100G", "multimode", "short-reach", "PAM4", "duplex-LC", "QSFP28"], generation: "Gen2 PAM4", marketStatus: "Growth", yearIntroduced: 2018 },
|
||||||
|
{ id: "qsfp28-sr2", standard: "100GBASE-SR2", ieeeReference: "100G Lambda MSA", formFactor: "QSFP28", speed: "100G", speedGbps: 100, lanes: 2, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 100, reachLabel: "100m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "100G SR2 uses 2x50G PAM4 over duplex LC. No MPO needed. Popular in DCs moving to duplex fiber.", vendors: dcVendors(), tags: ["100G", "multimode", "duplex-LC", "short-reach", "data-center", "PAM4", "QSFP28"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2018 },
|
||||||
|
{ id: "qsfp28-dr1", standard: "100GBASE-DR1", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "100G single-lane single-mode for intra-campus data center. Uses PAM4 modulation.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-DR-S" }, { vendor: "Arista", partPattern: "QSFP-100G-DR" }, V_JUNIPER, V_HUAWEI, V_DELL, V_EXTREME], tags: ["100G", "singlemode", "500m", "data-center", "PAM4", "single-lane", "QSFP28"] },
|
||||||
|
{ id: "qsfp28-fr1", standard: "100GBASE-FR1", ieeeReference: "IEEE 802.3cu", formFactor: "QSFP28", speed: "100G", speedGbps: 100, lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "DCI", priceTier: "Standard", useCase: "100G single-lambda for 2km campus DCI. Interworks with 400GBASE-FR4 breakout.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-FR-S" }, { vendor: "Juniper", partPattern: "QSFP-100G-FR" }, { vendor: "Arista", partPattern: "QSFP-100G-FR" }, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["100G", "singlemode", "2km", "DCI", "campus", "PAM4", "QSFP28"], generation: "Gen2 PAM4", marketStatus: "Growth", yearIntroduced: 2021 },
|
||||||
|
{ id: "qsfp28-lr1", standard: "100GBASE-LR1", ieeeReference: "IEEE 802.3cu", formFactor: "QSFP28", speed: "100G", speedGbps: 100, lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "IXP", priceTier: "Standard", useCase: "100G single-lambda for 10km. The new IXP standard (100G LR-1) replacing LR4 at major exchanges.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-LR1-S" }, { vendor: "Juniper", partPattern: "QSFP-100G-LR1" }, { vendor: "Nokia", partPattern: "3HE*" }, V_ARISTA, V_HUAWEI, V_HPE, V_DELL, V_EXTREME], tags: ["100G", "singlemode", "10km", "IXP", "LR-1", "PAM4", "QSFP28"], generation: "Gen2 PAM4", marketStatus: "Growth", yearIntroduced: 2021 },
|
||||||
|
{ id: "qsfp28-lr4", standard: "100GBASE-LR4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm (4 LAN-WDM lanes)", connector: "LC", powerConsumptionW: 4.5, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "100G single-mode for metro DCI and campus backbone up to 10km. Uses 4x25G LAN-WDM lanes.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-LR4-S" }, { vendor: "Juniper", partPattern: "QSFP-100G-LR4" }, { vendor: "Arista", partPattern: "QSFP-100G-LR4" }, { vendor: "Huawei", partPattern: "QSFP-100G-LR4" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["100G", "singlemode", "10km", "metro", "DCI", "LAN-WDM", "QSFP28", "IXP"] },
|
||||||
|
{ id: "qsfp28-cwdm4", standard: "100G CWDM4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (4 CWDM lanes)", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "DCI", priceTier: "Budget", useCase: "100G CWDM4 for short-reach DCI up to 2km. Lower cost than LR4 for inter-building links.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-CWDM4-S" }, { vendor: "Juniper", partPattern: "QSFP-100G-CWDM4" }, { vendor: "Arista", partPattern: "QSFP-100G-CWDM4" }, V_HUAWEI, V_DELL, V_EXTREME], tags: ["100G", "CWDM4", "singlemode", "2km", "DCI", "cost-effective", "QSFP28"] },
|
||||||
|
{ id: "qsfp28-er4", standard: "100GBASE-ER4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 40000, reachLabel: "40km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm (4 LAN-WDM lanes)", connector: "LC", powerConsumptionW: 4.5, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "100G extended reach for metro rings and longer DCI links up to 40km.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-ER4L" }, { vendor: "Juniper", partPattern: "QSFP-100G-ER4" }, V_HUAWEI, V_NOKIA], tags: ["100G", "singlemode", "40km", "metro", "extended-reach", "QSFP28"] },
|
||||||
|
{ id: "qsfp28-zr4", standard: "100GBASE-ZR4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 80000, reachLabel: "80km", fiberType: "SMF", wavelengths: "1296/1300/1305/1309nm (4 LAN-WDM lanes)", connector: "LC", powerConsumptionW: 5.0, tempRange: "COM", category: "LongHaul", priceTier: "Premium", useCase: "100G long-haul for regional networks up to 80km without amplification.", vendors: carrierVendors(), tags: ["100G", "singlemode", "80km", "long-haul", "regional", "QSFP28"] },
|
||||||
|
{ id: "qsfp28-lr8", standard: "100G LR8", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "8 CWDM wavelengths (1271-1411nm)", connector: "LC", powerConsumptionW: 4.0, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "100G using 8 CWDM lanes at 12.5G each. For platforms that do not support 25G-per-lane optics.", vendors: carrierVendors(), tags: ["100G", "singlemode", "10km", "CWDM", "8-lane", "legacy-platform", "QSFP28"] },
|
||||||
|
{ id: "qsfp28-psm4", standard: "100G PSM4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm (4 parallel fibers)", connector: "MPO-12", powerConsumptionW: 3.5, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "100G parallel single-mode for intra-DC links up to 500m. MPO-12 connector.", vendors: dcVendors(), tags: ["100G", "singlemode", "500m", "parallel", "MPO", "data-center", "QSFP28"] },
|
||||||
|
{ id: "qsfp28-sr-bidi", standard: "100GBASE-SR BiDi", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "832nm / 918nm BiDi (PAM4)", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "BiDi", priceTier: "Standard", useCase: "100G BiDi over LC duplex multimode. Enables 100G upgrade reusing existing 10G/25G LC MMF cabling.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-SR1.2" }, { vendor: "Arista", partPattern: "QSFP-100G-SRBD" }], tags: ["100G", "BiDi", "multimode", "LC-reuse", "data-center", "QSFP28"] },
|
||||||
|
{ id: "qsfp28-dwdm", standard: "100G QSFP28 DWDM Tunable", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 80000, reachLabel: "80km", fiberType: "SMF", wavelengths: "C-band tunable (1528-1565nm)", connector: "LC", powerConsumptionW: 5.0, tempRange: "COM", category: "DWDM", priceTier: "Premium", useCase: "100G DWDM tunable for metro/regional WDM networks. Integrates with ROADM systems.", vendors: carrierVendors(), tags: ["100G", "DWDM", "tunable", "C-band", "metro", "ROADM", "QSFP28"] },
|
||||||
|
{ id: "qsfp28-zr-coherent", standard: "100GBASE-ZR (Coherent)", ieeeReference: "IEEE 802.3ct", formFactor: "QSFP28", speed: "100G", speedGbps: 100, lanes: 1, laneRate: "~64 Gbaud", modulation: "DP-QPSK (coherent)", reachMeters: 80000, reachLabel: "80km+ (DWDM amplified)", fiberType: "SMF", wavelengths: "C-band (tunable, DWDM)", connector: "LC", powerConsumptionW: 5.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "100G coherent pluggable for DWDM long-haul. Phase/amplitude modulation with coherent detection.", vendors: carrierVendors(), tags: ["100G", "coherent", "DWDM", "long-haul", "C-band", "tunable", "QSFP28"], generation: "Coherent", marketStatus: "Mainstream", yearIntroduced: 2021 },
|
||||||
|
|
||||||
|
// ── CXP — 100G/120G Legacy ──
|
||||||
|
{ id: "cxp-sr10", standard: "100GBASE-SR10", formFactor: "CXP", speed: "100G", speedGbps: 100, reachMeters: 150, reachLabel: "100m (OM3) / 150m (OM4)", fiberType: "MMF", wavelengths: "850nm (10 lanes x 10G)", connector: "MPO-24", powerConsumptionW: 6.0, tempRange: "COM", category: "Legacy", priceTier: "Standard", useCase: "Legacy 100G/120G CXP form factor for InfiniBand and early 100G Ethernet.", vendors: [{ vendor: "Cisco", partPattern: "CXP-100G-SR10" }], tags: ["100G", "120G", "legacy", "InfiniBand", "CXP", "multimode"] },
|
||||||
|
|
||||||
|
// ── CFP / CFP2 / CFP4 — 100G ──
|
||||||
|
{ id: "cfp-lr4", standard: "100GBASE-LR4 CFP", formFactor: "CFP", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm", connector: "LC", powerConsumptionW: 24.0, tempRange: "COM", category: "Legacy", priceTier: "Standard", useCase: "First-generation 100G CFP form factor. Large footprint, being replaced by CFP2/QSFP28.", vendors: carrierVendors(), tags: ["100G", "singlemode", "10km", "legacy", "CFP"] },
|
||||||
|
{ id: "cfp2-lr4", standard: "100GBASE-LR4 CFP2", formFactor: "CFP2", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm", connector: "LC", powerConsumptionW: 9.0, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "100G CFP2 for carrier/service provider routers with CFP2 slots.", vendors: carrierVendors(), tags: ["100G", "singlemode", "10km", "carrier", "CFP2"] },
|
||||||
|
{ id: "cfp4-lr4", standard: "100GBASE-LR4 CFP4", formFactor: "CFP4", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm", connector: "LC", powerConsumptionW: 6.0, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "Compact 100G CFP4 for high-density router line cards.", vendors: carrierVendors(), tags: ["100G", "singlemode", "10km", "carrier", "high-density", "CFP4"] },
|
||||||
|
|
||||||
|
// ── CFP2-DCO — Coherent 100G-400G ──
|
||||||
|
{ id: "cfp2dco-100g", standard: "100G CFP2-DCO Coherent", formFactor: "CFP2-DCO", speed: "100G", speedGbps: 100, reachMeters: 2000000, reachLabel: "2000km+", fiberType: "SMF", wavelengths: "C-band tunable (1528-1565nm)", connector: "LC", powerConsumptionW: 18.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "100G coherent for long-haul and submarine links. DP-QPSK modulation.", vendors: carrierVendors(), tags: ["100G", "coherent", "DP-QPSK", "long-haul", "submarine", "DWDM", "tunable", "CFP2-DCO"] },
|
||||||
|
{ id: "cfp2dco-200g", standard: "200G CFP2-DCO Coherent", formFactor: "CFP2-DCO", speed: "200G", speedGbps: 200, reachMeters: 1000000, reachLabel: "1000km+", fiberType: "SMF", wavelengths: "C-band tunable (1528-1565nm)", connector: "LC", powerConsumptionW: 20.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "200G coherent for long-haul transport. DP-16QAM or DP-QPSK modulation.", vendors: carrierVendors(), tags: ["200G", "coherent", "DP-16QAM", "long-haul", "DWDM", "tunable", "CFP2-DCO"] },
|
||||||
|
{ id: "cfp2dco-400g", standard: "400G CFP2-DCO Coherent", formFactor: "CFP2-DCO", speed: "400G", speedGbps: 400, reachMeters: 600000, reachLabel: "600km+", fiberType: "SMF", wavelengths: "C-band tunable (1528-1565nm)", connector: "LC", powerConsumptionW: 22.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "400G coherent for long-haul and DCI. DP-16QAM modulation at high baud rate.", vendors: carrierVendors(), tags: ["400G", "coherent", "DP-16QAM", "long-haul", "DCI", "DWDM", "tunable", "CFP2-DCO"] },
|
||||||
|
|
||||||
|
// ── QSFP56 — 200G ──
|
||||||
|
{ id: "qsfp56-sr4", standard: "200GBASE-SR4", ieeeReference: "IEEE 802.3cd", formFactor: "QSFP56", speed: "200G", speedGbps: 200, lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm (4x50G PAM4)", connector: "MPO-12", powerConsumptionW: 7.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "200G multimode for data center links. 4 lanes at 50G PAM4 each.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-200G-SR4" }, { vendor: "Arista", partPattern: "QSFP-200G-SR4" }, V_JUNIPER, V_HUAWEI, V_DELL, V_EXTREME], tags: ["200G", "multimode", "PAM4", "data-center", "QSFP56"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2019, breakoutCapable: true, breakoutTo: "4x50GBASE-SR or 2x100GBASE-SR2" },
|
||||||
|
{ id: "qsfp56-dr4", standard: "200GBASE-DR4", ieeeReference: "IEEE 802.3cd", formFactor: "QSFP56", speed: "200G", speedGbps: 200, lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm (4x50G PAM4 parallel)", connector: "MPO-12", powerConsumptionW: 7.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "200G parallel single-mode for intra-DC links up to 500m. Breakout to 4x50G possible.", vendors: dcVendors(), tags: ["200G", "singlemode", "500m", "parallel", "breakout", "data-center", "QSFP56"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2019, breakoutCapable: true, breakoutTo: "2x100GBASE-DR or 4x50G" },
|
||||||
|
{ id: "qsfp56-fr4", standard: "200GBASE-FR4", ieeeReference: "IEEE 802.3cu", formFactor: "QSFP56", speed: "200G", speedGbps: 200, lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (CWDM4)", connector: "LC", powerConsumptionW: 7.0, tempRange: "COM", category: "DCI", priceTier: "Standard", useCase: "200G for short-reach DCI up to 2km. Uses CWDM wavelengths over LC duplex fiber.", vendors: dcVendors(), tags: ["200G", "CWDM", "singlemode", "2km", "DCI", "QSFP56"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2021 },
|
||||||
|
{ id: "qsfp56-lr4", standard: "200GBASE-LR4", ieeeReference: "IEEE 802.3cu", formFactor: "QSFP56", speed: "200G", speedGbps: 200, lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm (4 LAN-WDM lanes x 50G)", connector: "LC", powerConsumptionW: 8.0, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "200G metro DCI links up to 10km using LAN-WDM.", vendors: carrierVendors(), tags: ["200G", "singlemode", "10km", "metro", "DCI", "LAN-WDM", "QSFP56"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2021 },
|
||||||
|
|
||||||
|
// ── QSFP-DD — 400G ──
|
||||||
|
{ id: "qsfpdd-sr8", standard: "400GBASE-SR8", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm (8x50G PAM4)", connector: "MPO-16", powerConsumptionW: 12.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "400G multimode for short-reach data center links. 8 lanes at 50G PAM4.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-SR8" }, { vendor: "Arista", partPattern: "QDD-400G-SR8" }, V_JUNIPER, V_HUAWEI, V_DELL, V_EXTREME], tags: ["400G", "multimode", "short-reach", "data-center", "MPO-16", "PAM4", "QSFP-DD"] },
|
||||||
|
{ id: "qsfpdd-sr4-2", standard: "400GBASE-SR4.2", ieeeReference: "IEEE 802.3cm", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 100, reachLabel: "100m (OM4)", fiberType: "MMF", wavelengths: "850nm + 910nm (bidirectional)", connector: "MPO-12", powerConsumptionW: 12.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "400G BiDi over MPO-12 using two wavelengths. Saves fiber vs SR8.", vendors: dcVendors(), tags: ["400G", "multimode", "BiDi", "data-center", "MPO-12", "cabling-reuse", "QSFP-DD"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2020, breakoutCapable: true, breakoutTo: "2x200GBASE-SR4 or 4x100GBASE-SR" },
|
||||||
|
{ id: "qsfpdd-dr4", standard: "400GBASE-DR4", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm (4x100G PAM4 parallel)", connector: "MPO-12", powerConsumptionW: 12.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "400G parallel single-mode for intra-DC. Breakout to 4x100G DR1 possible. Key DCI building block.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-DR4-S" }, { vendor: "Juniper", partPattern: "QDD-400G-DR4" }, { vendor: "Arista", partPattern: "QDD-400G-DR4" }, V_HUAWEI, V_DELL, V_EXTREME], tags: ["400G", "singlemode", "500m", "parallel", "breakout", "data-center", "DCI", "QSFP-DD"] },
|
||||||
|
{ id: "qsfpdd-fr4", standard: "400GBASE-FR4", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (4 CWDM lanes x 100G)", connector: "LC", powerConsumptionW: 12.0, tempRange: "COM", category: "DCI", priceTier: "Standard", useCase: "400G FR4 for DCI up to 2km using CWDM4 over LC duplex. Most popular 400G DCI optic.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-FR4-S" }, { vendor: "Juniper", partPattern: "QDD-400G-FR4" }, { vendor: "Arista", partPattern: "QDD-400G-FR4" }, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["400G", "CWDM", "singlemode", "2km", "DCI", "popular", "QSFP-DD"] },
|
||||||
|
{ id: "qsfpdd-lr4", standard: "400GBASE-LR4", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm (4 LAN-WDM lanes x 100G)", connector: "LC", powerConsumptionW: 14.0, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "400G for metro DCI and campus backbone up to 10km.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-LR4-S" }, { vendor: "Juniper", partPattern: "QDD-400G-LR4" }, V_HUAWEI, V_NOKIA], tags: ["400G", "singlemode", "10km", "metro", "DCI", "LAN-WDM", "QSFP-DD"] },
|
||||||
|
{ id: "qsfpdd-lr8", standard: "400GBASE-LR8", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "8 CWDM wavelengths (1271-1411nm, 8x50G)", connector: "LC", powerConsumptionW: 14.0, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "400G LR8 using 8 CWDM lanes at 50G each. For platforms with 50G-per-lane electronics.", vendors: carrierVendors(), tags: ["400G", "CWDM8", "singlemode", "10km", "metro", "8-lane", "QSFP-DD"] },
|
||||||
|
{ id: "qsfpdd-er4", standard: "400GBASE-ER4", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 40000, reachLabel: "40km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm (4 LAN-WDM lanes, amplified)", connector: "LC", powerConsumptionW: 16.0, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "400G extended reach for metro rings up to 40km.", vendors: carrierVendors(), tags: ["400G", "singlemode", "40km", "metro", "extended-reach", "SOA", "QSFP-DD"] },
|
||||||
|
{ id: "qsfpdd-xdr4", standard: "400GBASE-XDR4 (4x100G-FR)", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1310nm", connector: "MPO-12", powerConsumptionW: 12.0, tempRange: "COM", category: "DCI", priceTier: "Standard", useCase: "400G XDR4 = 4 parallel SMF lanes at 100G each for 2km. Breaks out to 4x100G-FR1.", vendors: [{ vendor: "Arista", partPattern: "QDD-400G-XDR4" }, { vendor: "Juniper", partPattern: "QDD-4X100G-FR" }, V_CISCO, V_HUAWEI, V_DELL, V_EXTREME], tags: ["400G", "singlemode", "2km", "parallel-SMF", "DCI", "breakout", "PAM4", "QSFP-DD"], generation: "Gen2 PAM4", marketStatus: "Growth", breakoutCapable: true, breakoutTo: "4x100GBASE-FR1" },
|
||||||
|
{ id: "qsfpdd-plr4", standard: "400G-PLR4 (4x100G-LR)", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "MPO-12", powerConsumptionW: 14.0, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "400G parallel-LR for 10km. 4 parallel SMF lanes. Breaks out to 4x100G-LR1.", vendors: [{ vendor: "Arista", partPattern: "QDD-400G-PLR4" }, { vendor: "Juniper", partPattern: "QDD-400G-PLR4" }, V_CISCO, V_HUAWEI], tags: ["400G", "singlemode", "10km", "parallel-SMF", "metro", "breakout", "QSFP-DD"], generation: "Gen2 PAM4", marketStatus: "Growth", breakoutCapable: true, breakoutTo: "4x100GBASE-LR1" },
|
||||||
|
{ id: "qsfpdd-zr", standard: "400G-ZR (OIF)", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 120000, reachLabel: "120km (unamplified)", fiberType: "SMF", wavelengths: "C-band tunable (1528-1565nm), DP-16QAM", connector: "LC", powerConsumptionW: 18.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "400G-ZR coherent in QSFP-DD form factor. Industry standard (OIF) for DCI up to 120km unamplified.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-ZR-S" }, { vendor: "Juniper", partPattern: "QDD-400G-ZR" }, { vendor: "Arista", partPattern: "QDD-400G-ZR" }, V_HUAWEI, V_NOKIA], tags: ["400G", "coherent", "ZR", "DP-16QAM", "DCI", "120km", "OIF", "pluggable", "QSFP-DD"] },
|
||||||
|
{ id: "qsfpdd-zrp", standard: "400G-ZR+ (OpenZR+)", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 2000000, reachLabel: "500km+ (amplified)", fiberType: "SMF", wavelengths: "C-band tunable, flexible modulation (QPSK/8QAM/16QAM)", connector: "LC", powerConsumptionW: 20.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "400G-ZR+ coherent with flexible modulation for metro-to-long-haul. OpenZR+ MSA standard.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-ZRP-S" }, { vendor: "Juniper", partPattern: "QDD-400G-ZRP" }, { vendor: "Arista", partPattern: "QDD-400G-ZRP" }, V_HUAWEI, V_NOKIA], tags: ["400G", "coherent", "ZR+", "OpenZR+", "flexible-modulation", "metro", "long-haul", "DCI", "pluggable", "QSFP-DD"] },
|
||||||
|
|
||||||
|
// ── OSFP — 400G / 800G ──
|
||||||
|
{ id: "osfp-sr8", standard: "400GBASE-SR8 OSFP", formFactor: "OSFP", speed: "400G", speedGbps: 400, reachMeters: 100, reachLabel: "100m (OM4)", fiberType: "MMF", wavelengths: "850nm (8x50G PAM4)", connector: "MPO-16", powerConsumptionW: 15.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "400G multimode OSFP for platforms with OSFP cages.", vendors: [{ vendor: "Arista", partPattern: "OSFP-400G-SR8" }, V_CISCO, V_JUNIPER, V_HUAWEI, V_DELL, V_EXTREME], tags: ["400G", "multimode", "data-center", "OSFP"] },
|
||||||
|
{ id: "osfp-dr4", standard: "400GBASE-DR4 OSFP", formFactor: "OSFP", speed: "400G", speedGbps: 400, reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm (4x100G PAM4)", connector: "MPO-12", powerConsumptionW: 15.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "400G parallel single-mode OSFP for next-gen data center fabrics.", vendors: dcVendors(), tags: ["400G", "singlemode", "500m", "data-center", "OSFP"] },
|
||||||
|
{ id: "osfp-fr4", standard: "400GBASE-FR4 OSFP", formFactor: "OSFP", speed: "400G", speedGbps: 400, reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (4 CWDM x 100G)", connector: "LC", powerConsumptionW: 15.0, tempRange: "COM", category: "DCI", priceTier: "Standard", useCase: "400G FR4 in OSFP form factor for DCI links up to 2km.", vendors: dcVendors(), tags: ["400G", "CWDM", "singlemode", "2km", "DCI", "OSFP"] },
|
||||||
|
{ id: "osfp-800g-sr8", standard: "800GBASE-SR8", formFactor: "OSFP", speed: "800G", speedGbps: 800, reachMeters: 50, reachLabel: "30m (OM3) / 50m (OM4)", fiberType: "MMF", wavelengths: "850nm (8x100G PAM4)", connector: "MPO-16", powerConsumptionW: 22.0, tempRange: "COM", category: "DataCenter", priceTier: "Premium", useCase: "800G multimode for AI/ML GPU cluster interconnects in hyperscale data centers.", vendors: [{ vendor: "Cisco", partPattern: "OSFP-800G-SR8" }, { vendor: "Arista", partPattern: "OSFP-800G-SR8" }], tags: ["800G", "multimode", "data-center", "AI", "GPU", "hyperscale", "OSFP"] },
|
||||||
|
{ id: "osfp-800g-dr8", standard: "800GBASE-DR8", formFactor: "OSFP", speed: "800G", speedGbps: 800, reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm (8x100G PAM4 parallel)", connector: "MPO-16", powerConsumptionW: 22.0, tempRange: "COM", category: "DataCenter", priceTier: "Premium", useCase: "800G parallel single-mode for hyperscale DC fabrics. Breakout to 2x400G or 8x100G possible.", vendors: [{ vendor: "Cisco", partPattern: "OSFP-800G-DR8" }, { vendor: "Arista", partPattern: "OSFP-800G-DR8" }], tags: ["800G", "singlemode", "500m", "parallel", "breakout", "hyperscale", "data-center", "OSFP"] },
|
||||||
|
{ id: "osfp-800g-2fr4", standard: "800G-2FR4", formFactor: "OSFP", speed: "800G", speedGbps: 800, reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "8 CWDM wavelengths (4 per fiber direction)", connector: "CS", powerConsumptionW: 22.0, tempRange: "COM", category: "DCI", priceTier: "Premium", useCase: "800G for DCI up to 2km using duplex CS connector. Dual FR4 in a single module.", vendors: dcVendors(), tags: ["800G", "CWDM", "singlemode", "2km", "DCI", "CS-connector", "OSFP"] },
|
||||||
|
{ id: "osfp-800g-zr", standard: "800G-ZR", formFactor: "OSFP", speed: "800G", speedGbps: 800, reachMeters: 120000, reachLabel: "80-120km", fiberType: "SMF", wavelengths: "C-band tunable, DP-64QAM/DP-16QAM", connector: "LC", powerConsumptionW: 25.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "800G coherent pluggable for DCI. Next-gen after 400G-ZR.", vendors: [{ vendor: "Cisco", partPattern: "OSFP-800G-ZR" }, { vendor: "Arista", partPattern: "OSFP-800G-ZR" }, V_JUNIPER, V_HUAWEI, V_NOKIA], tags: ["800G", "coherent", "ZR", "DCI", "pluggable", "next-gen", "OSFP"] },
|
||||||
|
|
||||||
|
// ── DAC — Direct Attach Cables ──
|
||||||
|
{ id: "dac-sfpp-1m", standard: "10G SFP+ DAC", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 5, reachLabel: "1-5m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 0.5, tempRange: "COM", category: "DAC", priceTier: "Budget", useCase: "10G passive copper DAC for in-rack server-to-switch links. Lowest cost and latency option.", vendors: allMajorVendors(), tags: ["10G", "DAC", "copper", "passive", "in-rack", "low-latency", "SFP+"] },
|
||||||
|
{ id: "dac-sfp28-3m", standard: "25G SFP28 DAC", formFactor: "SFP28", speed: "25G", speedGbps: 25, reachMeters: 5, reachLabel: "1-5m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 0.5, tempRange: "COM", category: "DAC", priceTier: "Budget", useCase: "25G passive copper DAC for in-rack 25G server connections.", vendors: allMajorVendors(), tags: ["25G", "DAC", "copper", "passive", "in-rack", "leaf-spine", "SFP28"] },
|
||||||
|
{ id: "dac-qsfpp-3m", standard: "40G QSFP+ DAC", formFactor: "QSFP+", speed: "40G", speedGbps: 40, reachMeters: 5, reachLabel: "1-5m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 0.5, tempRange: "COM", category: "DAC", priceTier: "Budget", useCase: "40G passive copper DAC for spine-to-leaf and storage links within a rack.", vendors: allMajorVendors(), tags: ["40G", "DAC", "copper", "passive", "in-rack", "QSFP+"] },
|
||||||
|
{ id: "dac-qsfp28-3m", standard: "100G QSFP28 DAC", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 5, reachLabel: "1-5m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 1.0, tempRange: "COM", category: "DAC", priceTier: "Budget", useCase: "100G passive copper DAC for spine-leaf and storage interconnects within racks.", vendors: allMajorVendors(), tags: ["100G", "DAC", "copper", "passive", "in-rack", "QSFP28"] },
|
||||||
|
{ id: "dac-qsfpdd-3m", standard: "400G QSFP-DD DAC", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 3, reachLabel: "1-3m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 1.5, tempRange: "COM", category: "DAC", priceTier: "Budget", useCase: "400G passive copper DAC for in-rack high-bandwidth interconnects.", vendors: dcVendors(), tags: ["400G", "DAC", "copper", "passive", "in-rack", "QSFP-DD"] },
|
||||||
|
{ id: "dac-osfp-800g", standard: "800G OSFP DAC", formFactor: "OSFP", speed: "800G", speedGbps: 800, reachMeters: 2, reachLabel: "1-2m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 2.0, tempRange: "COM", category: "DAC", priceTier: "Standard", useCase: "800G DAC for GPU-to-switch and AI cluster interconnects. Shortest latency option for 800G.", vendors: [{ vendor: "Cisco", partPattern: "OSFP-800G-CU*" }, { vendor: "Arista", partPattern: "OSFP-800G-DAC*" }], tags: ["800G", "DAC", "copper", "AI", "GPU", "OSFP"] },
|
||||||
|
|
||||||
|
// ── AOC — Active Optical Cables ──
|
||||||
|
{ id: "aoc-sfpp-10m", standard: "10G SFP+ AOC", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 100, reachLabel: "1-100m", fiberType: "MMF", wavelengths: "850nm (embedded)", connector: "None", powerConsumptionW: 1.0, tempRange: "COM", category: "AOC", priceTier: "Budget", useCase: "10G AOC for inter-rack links beyond DAC reach. Lighter than copper.", vendors: allMajorVendors(), tags: ["10G", "AOC", "multimode", "inter-rack", "SFP+"] },
|
||||||
|
{ id: "aoc-qsfp28-30m", standard: "100G QSFP28 AOC", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 100, reachLabel: "1-100m", fiberType: "MMF", wavelengths: "850nm (embedded)", connector: "None", powerConsumptionW: 3.0, tempRange: "COM", category: "AOC", priceTier: "Budget", useCase: "100G AOC for inter-rack DC links. Lighter and cheaper than SR4 + MPO patch cords.", vendors: allMajorVendors(), tags: ["100G", "AOC", "multimode", "inter-rack", "data-center", "QSFP28"] },
|
||||||
|
{ id: "aoc-qsfpdd-30m", standard: "400G QSFP-DD AOC", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 100, reachLabel: "1-100m", fiberType: "MMF", wavelengths: "850nm (embedded)", connector: "None", powerConsumptionW: 10.0, tempRange: "COM", category: "AOC", priceTier: "Standard", useCase: "400G AOC for short inter-rack links in high-density data centers.", vendors: dcVendors(), tags: ["400G", "AOC", "multimode", "inter-rack", "QSFP-DD"] },
|
||||||
|
|
||||||
|
// ── Industrial Temperature Variants ──
|
||||||
|
{ id: "sfpp-lr-ind", standard: "10GBASE-LR Industrial", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.2, tempRange: "IND", category: "Access", priceTier: "Standard", useCase: "10G industrial-temp (-40 to +85C) for outdoor deployments, telecom shelters, and cell towers.", vendors: allMajorVendors(), tags: ["10G", "industrial", "outdoor", "telecom", "cell-tower", "SFP+"] },
|
||||||
|
{ id: "sfp-lx-ind", standard: "1000BASE-LX Industrial", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "IND", category: "Access", priceTier: "Standard", useCase: "1G industrial-temp for outdoor access networks, utility SCADA, and harsh environments.", vendors: allMajorVendors(), tags: ["1G", "industrial", "outdoor", "SCADA", "utility", "SFP"] },
|
||||||
|
{ id: "qsfp28-lr4-ind", standard: "100GBASE-LR4 Industrial", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm", connector: "LC", powerConsumptionW: 5.0, tempRange: "IND", category: "Metro", priceTier: "Premium", useCase: "100G industrial-temp for telecom outdoor cabinets and cell-site aggregation.", vendors: carrierVendors(), tags: ["100G", "industrial", "outdoor", "telecom", "QSFP28"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Search & Filter Functions ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search transceivers by any keyword. Searches across standard, form factor,
|
||||||
|
* speed, use case, tags, and vendor names.
|
||||||
|
*/
|
||||||
|
export function searchTransceivers(query: string): Transceiver[] {
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
return transceivers.filter(
|
||||||
|
(t) =>
|
||||||
|
t.standard.toLowerCase().includes(q) ||
|
||||||
|
t.formFactor.toLowerCase().includes(q) ||
|
||||||
|
t.speed.toLowerCase().includes(q) ||
|
||||||
|
t.useCase.toLowerCase().includes(q) ||
|
||||||
|
t.category.toLowerCase().includes(q) ||
|
||||||
|
t.wavelengths.toLowerCase().includes(q) ||
|
||||||
|
t.tags.some((tag) => tag.toLowerCase().includes(q)) ||
|
||||||
|
t.vendors.some((v) => v.vendor.toLowerCase().includes(q)) ||
|
||||||
|
(t.modulation && t.modulation.toLowerCase().includes(q)) ||
|
||||||
|
(t.generation && t.generation.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filter by form factor (e.g., "SFP+", "QSFP-DD", "OSFP"). */
|
||||||
|
export function getByFormFactor(formFactor: string): Transceiver[] {
|
||||||
|
return transceivers.filter(
|
||||||
|
(t) => t.formFactor.toLowerCase() === formFactor.toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filter by speed tier (e.g., "10G", "100G", "400G", "800G"). */
|
||||||
|
export function getBySpeed(speed: string): Transceiver[] {
|
||||||
|
return transceivers.filter(
|
||||||
|
(t) => t.speed.toLowerCase() === speed.toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filter by maximum reach in meters. Returns transceivers that reach at least `minMeters`. */
|
||||||
|
export function getByReach(minMeters: number): Transceiver[] {
|
||||||
|
return transceivers.filter((t) => t.reachMeters >= minMeters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filter by product category (e.g., "DataCenter", "Coherent", "DAC"). */
|
||||||
|
export function getByCategory(category: string): Transceiver[] {
|
||||||
|
return transceivers.filter(
|
||||||
|
(t) => t.category.toLowerCase() === category.toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a single transceiver by its unique ID. */
|
||||||
|
export function getById(id: string): Transceiver | undefined {
|
||||||
|
return transceivers.find((t) => t.id === id);
|
||||||
|
}
|
||||||
37
packages/core/src/index.ts
Normal file
37
packages/core/src/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* transceiver-db — Open-source optical transceiver database
|
||||||
|
*
|
||||||
|
* 159 products, 42 IEEE/MSA standards, 16 form factors, 9 speed tiers.
|
||||||
|
* From 1G SFP to 800G OSFP. Zero dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
transceivers,
|
||||||
|
searchTransceivers,
|
||||||
|
getByFormFactor,
|
||||||
|
getBySpeed,
|
||||||
|
getByReach,
|
||||||
|
getByCategory,
|
||||||
|
getById,
|
||||||
|
} from "./database";
|
||||||
|
|
||||||
|
export { standards, getStandard, searchStandards } from "./standards";
|
||||||
|
|
||||||
|
export { competitors, getCompetitor } from "./market";
|
||||||
|
|
||||||
|
export { breakouts } from "./breakouts";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Transceiver,
|
||||||
|
Standard,
|
||||||
|
Competitor,
|
||||||
|
Breakout,
|
||||||
|
FormFactor,
|
||||||
|
FiberType,
|
||||||
|
ConnectorType,
|
||||||
|
TempRange,
|
||||||
|
ProductCategory,
|
||||||
|
PriceTier,
|
||||||
|
MarketStatus,
|
||||||
|
VendorCompat,
|
||||||
|
} from "./types";
|
||||||
28
packages/core/src/market.ts
Normal file
28
packages/core/src/market.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Competitor landscape — neutral industry data.
|
||||||
|
* No vendor bias. No sales language.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Competitor } from "./types";
|
||||||
|
|
||||||
|
export const competitors: readonly Competitor[] = [
|
||||||
|
{ name: "Cisco", type: "OEM", headquarters: "San Jose, USA", marketPosition: "Largest networking vendor globally. Sells branded optics. Acquired Acacia Communications for coherent technology.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD", "OSFP", "CFP2-DCO"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G", "800G"], strengths: ["Dominant installed base", "End-to-end solution", "TAC support tied to branded optics"], weaknesses: ["Significant price premium over compatible optics", "Vendor lock-in practices"] },
|
||||||
|
{ name: "Juniper Networks", type: "OEM", headquarters: "Sunnyvale, USA (acquired by HPE 2024)", marketPosition: "Strong in service provider and large enterprise. MX/PTX series for routing, QFX/EX for switching.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD", "CFP2-DCO"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G"], strengths: ["Junos OS reliability", "Strong SP/carrier presence", "PTX for massive scale"], weaknesses: ["Optics premium over compatible alternatives", "Smaller market share than Cisco"] },
|
||||||
|
{ name: "Arista Networks", type: "OEM", headquarters: "Santa Clara, USA", marketPosition: "Dominant in hyperscale data centers and cloud. Largest DC switching vendor by port volume. Leader in 400G/800G deployments.", formFactorsOffered: ["SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD", "OSFP"], speedTiersOffered: ["10G", "25G", "40G", "100G", "400G", "800G"], strengths: ["EOS software quality", "Hyperscale dominance", "Early 800G adoption", "Liquid-cooled optics for AI"], weaknesses: ["Optics premium over compatible alternatives", "Primarily DC focused"] },
|
||||||
|
{ name: "Huawei", type: "OEM", headquarters: "Shenzhen, China", marketPosition: "Largest carrier/SP equipment vendor globally. Strong in EMEA, APAC, Middle East. CloudEngine for DC switching.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD", "OSFP", "CFP2-DCO"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G", "800G"], strengths: ["Aggressive pricing vs Western OEMs", "Massive carrier installed base", "Strong in coherent/DWDM"], weaknesses: ["Geopolitical restrictions in some markets"] },
|
||||||
|
{ name: "Nokia", type: "OEM", headquarters: "Espoo, Finland", marketPosition: "Major carrier/SP vendor. 7750 SR series for routing, 7250 IXR for DC. Strong in IXP infrastructure.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD", "CFP2-DCO"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G", "800G"], strengths: ["IXP platform dominance", "FP5 silicon for high-performance routing", "Strong coherent optics"], weaknesses: ["Optics premium", "Smaller DC switching presence"] },
|
||||||
|
{ name: "FLEXOPTIX", type: "Compatible", headquarters: "Darmstadt, Germany", marketPosition: "Premium compatible optics vendor with unique FlexBox hardware programmer. Codes transceivers on-site for any vendor. 300+ supported switch/router vendors. Lifetime warranty. Strong in service provider, IXP, and enterprise markets across EMEA.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "SFP56", "QSFP+", "QSFP28", "QSFP56", "QSFP-DD", "OSFP", "CFP", "CFP2", "CFP4", "CFP2-DCO", "XFP", "CXP"], speedTiersOffered: ["1G", "10G", "25G", "40G", "50G", "100G", "200G", "400G", "800G"], strengths: ["FlexBox: on-site hardware programmer — recode any transceiver for any vendor in seconds", "300+ supported vendors (largest compatibility matrix in the industry)", "Multi-vendor infrastructure support — one transceiver works across Cisco, Juniper, Arista, Nokia, Huawei and 295+ more", "Lifetime warranty on all products", "Technical support by network engineers, not call center scripts", "Same-day shipping from German warehouse", "Strong NOG/peering community presence"], weaknesses: ["Not the cheapest option (premium quality positioning)", "European-focused logistics (expanding globally)"] },
|
||||||
|
{ name: "FS.COM", type: "Whitebox", headquarters: "Wilmington, USA (manufacturing in China)", marketPosition: "Major compatible optics vendor. Massive online catalog. Tests compatibility with Cisco, Arista, Juniper, NVIDIA.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "SFP56", "QSFP+", "QSFP28", "QSFP56", "QSFP-DD", "OSFP"], speedTiersOffered: ["1G", "10G", "25G", "40G", "50G", "100G", "200G", "400G", "800G", "1.6T"], strengths: ["Aggressive pricing", "Huge catalog", "Online ordering", "NVIDIA InfiniBand compatibility"], weaknesses: ["Pre-coded only — no field recoding", "Limited field customization", "Quality varies by batch", "No equivalent to FlexBox programmer"] },
|
||||||
|
{ name: "Innolight Technology", type: "Manufacturer", headquarters: "Suzhou, China", marketPosition: "Top optical transceiver manufacturer by revenue. Primary supplier to NVIDIA and major hyperscalers. Dominates 800G market.", formFactorsOffered: ["SFP28", "QSFP28", "QSFP56", "QSFP-DD", "OSFP", "OSFP-XD"], speedTiersOffered: ["25G", "100G", "200G", "400G", "800G", "1.6T"], strengths: ["Primary supplier to NVIDIA", "Massive manufacturing scale", "Silicon photonics leader", "800G/1.6T early adopter"], weaknesses: ["Primarily sells to hyperscalers/OEMs", "Not direct to enterprise"] },
|
||||||
|
{ name: "Coherent Corp", type: "Manufacturer", headquarters: "Saxonburg, USA", marketPosition: "Top-tier transceiver manufacturer (formerly II-VI/Finisar). Vertically integrated. Strong in coherent optics and telecom.", formFactorsOffered: ["SFP+", "SFP28", "QSFP28", "QSFP-DD", "OSFP", "CFP2-DCO", "OSFP-XD"], speedTiersOffered: ["10G", "25G", "100G", "400G", "800G", "1.6T"], strengths: ["Vertical integration", "Coherent optics leadership", "InP and SiPh capabilities"], weaknesses: ["Premium pricing", "OEM/hyperscale focus"] },
|
||||||
|
{ name: "Broadcom", type: "Manufacturer", headquarters: "San Jose, USA", marketPosition: "Major silicon + optics vendor. Tomahawk switch ASICs plus transceiver modules. Co-packaged optics initiative.", formFactorsOffered: ["SFP+", "SFP28", "QSFP28", "QSFP-DD", "OSFP"], speedTiersOffered: ["10G", "25G", "100G", "400G", "800G"], strengths: ["Switch ASIC + optics synergy", "Co-packaged optics leadership"], weaknesses: ["Premium pricing", "Mostly hyperscale/OEM channel"] },
|
||||||
|
{ name: "HPE/Aruba", type: "OEM", headquarters: "Houston, USA", marketPosition: "Strong in campus/enterprise networking. Aruba CX for modern campus. Juniper acquisition expanding portfolio.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G"], strengths: ["Campus/enterprise dominance", "Aruba CX modern OS", "Juniper integration expanding"], weaknesses: ["Optics premium", "Less DC presence (pre-Juniper)"] },
|
||||||
|
{ name: "Dell Technologies", type: "OEM", headquarters: "Round Rock, USA", marketPosition: "PowerSwitch for DC switching. Strong server attach rate driving optics demand.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G"], strengths: ["Server + switch bundle deals", "OS10/FTOS flexibility", "Open networking friendly"], weaknesses: ["Optics premium", "Smaller networking market share"] },
|
||||||
|
{ name: "Extreme Networks", type: "OEM", headquarters: "Morrisville, USA", marketPosition: "Consolidated multiple brands (Brocade, Avaya Networking, Enterasys, Aerohive). Strong in campus and education.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G"], strengths: ["Campus/education market", "Fabric Connect technology", "Unified management"], weaknesses: ["Optics premium", "Complex legacy product lines"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Find a competitor by name (partial match). */
|
||||||
|
export function getCompetitor(name: string): Competitor | undefined {
|
||||||
|
const q = name.toLowerCase();
|
||||||
|
return competitors.find((c) => c.name.toLowerCase().includes(q));
|
||||||
|
}
|
||||||
82
packages/core/src/standards.ts
Normal file
82
packages/core/src/standards.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* IEEE 802.3 and MSA standards reference — 42 standards.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Standard } from "./types";
|
||||||
|
|
||||||
|
export const standards: readonly Standard[] = [
|
||||||
|
// 1G
|
||||||
|
{ standard: "1000BASE-SX", ieeeReference: "IEEE 802.3z", speed: "1G", lanes: 1, laneRate: "1.25 Gbaud", modulation: "NRZ", fiberType: "MMF (OM1-OM4)", wavelength: "850nm", maxReachMeters: 550, maxReachLabel: "220m (OM1) / 550m (OM2+)", connector: "LC/SC", fecRequired: false, formFactors: ["SFP", "GBIC"], yearRatified: 1998, notes: "Original Gigabit Ethernet multimode standard." },
|
||||||
|
{ standard: "1000BASE-LX", ieeeReference: "IEEE 802.3z", speed: "1G", lanes: 1, laneRate: "1.25 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC/SC", fecRequired: false, formFactors: ["SFP", "GBIC"], yearRatified: 1998, notes: "Standard 1G single-mode. Also works on MMF with mode conditioning patch cable." },
|
||||||
|
{ standard: "1000BASE-ZX", ieeeReference: "Vendor-defined (not IEEE)", speed: "1G", lanes: 1, laneRate: "1.25 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1550nm", maxReachMeters: 80000, maxReachLabel: "70-80km", connector: "LC", fecRequired: false, formFactors: ["SFP"], yearRatified: 0, notes: "Not an IEEE standard. Vendor-defined. Uses 1550nm for extended reach." },
|
||||||
|
{ standard: "1000BASE-T", ieeeReference: "IEEE 802.3ab", speed: "1G", lanes: 4, laneRate: "250 Mbaud", modulation: "PAM5", fiberType: "Copper (Cat5e+)", wavelength: "N/A", maxReachMeters: 100, maxReachLabel: "100m", connector: "RJ45", fecRequired: false, formFactors: ["SFP"], yearRatified: 1999, notes: "Gigabit over copper. SFP form factor draws ~1W." },
|
||||||
|
{ standard: "1000BASE-BX10", ieeeReference: "IEEE 802.3ah", speed: "1G", lanes: 1, laneRate: "1.25 Gbaud", modulation: "NRZ", fiberType: "SMF (single fiber)", wavelength: "1310/1490nm", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: false, formFactors: ["SFP"], yearRatified: 2004, notes: "Bidirectional over single fiber strand. Sold in pairs." },
|
||||||
|
|
||||||
|
// 10G
|
||||||
|
{ standard: "10GBASE-SR", ieeeReference: "IEEE 802.3ae", speed: "10G", lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "MMF (OM3/OM4)", wavelength: "850nm", maxReachMeters: 400, maxReachLabel: "300m (OM3) / 400m (OM4)", connector: "LC", fecRequired: false, formFactors: ["SFP+", "XFP"], yearRatified: 2002, notes: "Most deployed 10G optic worldwide." },
|
||||||
|
{ standard: "10GBASE-LR", ieeeReference: "IEEE 802.3ae", speed: "10G", lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: false, formFactors: ["SFP+", "XFP"], yearRatified: 2002, notes: "Standard 10G single-mode. Backbone of campus and metro networks." },
|
||||||
|
{ standard: "10GBASE-ER", ieeeReference: "IEEE 802.3ae", speed: "10G", lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1550nm", maxReachMeters: 40000, maxReachLabel: "40km", connector: "LC", fecRequired: false, formFactors: ["SFP+", "XFP"], yearRatified: 2002, notes: "Extended reach 10G for metro rings and inter-city links." },
|
||||||
|
{ standard: "10GBASE-ZR", ieeeReference: "Vendor-defined (not IEEE)", speed: "10G", lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1550nm", maxReachMeters: 80000, maxReachLabel: "80km", connector: "LC", fecRequired: false, formFactors: ["SFP+", "XFP"], yearRatified: 0, notes: "Not an IEEE standard. Vendor-defined. 80km reach with high-power laser." },
|
||||||
|
{ standard: "10GBASE-T", ieeeReference: "IEEE 802.3an", speed: "10G", lanes: 4, laneRate: "2.5 Gbaud", modulation: "PAM16/DSQ128", fiberType: "Copper (Cat6a/Cat7)", wavelength: "N/A", maxReachMeters: 100, maxReachLabel: "100m (Cat6a)", connector: "RJ45", fecRequired: false, formFactors: ["SFP+"], yearRatified: 2006, notes: "10G over copper. 30m on Cat6, 100m on Cat6a." },
|
||||||
|
{ standard: "10GBASE-LRM", ieeeReference: "IEEE 802.3aq", speed: "10G", lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "MMF (legacy OM1/OM2)", wavelength: "1310nm", maxReachMeters: 220, maxReachLabel: "220m", connector: "LC", fecRequired: false, formFactors: ["SFP+"], yearRatified: 2006, notes: "10G over legacy multimode fiber." },
|
||||||
|
|
||||||
|
// 25G
|
||||||
|
{ standard: "25GBASE-SR", ieeeReference: "IEEE 802.3by", speed: "25G", lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "MMF (OM3/OM4)", wavelength: "850nm", maxReachMeters: 100, maxReachLabel: "70m (OM3) / 100m (OM4)", connector: "LC", fecRequired: true, formFactors: ["SFP28"], yearRatified: 2016, notes: "Standard 25G data center server access." },
|
||||||
|
{ standard: "25GBASE-LR", ieeeReference: "IEEE 802.3cc", speed: "25G", lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: false, formFactors: ["SFP28"], yearRatified: 2017, notes: "25G single-mode for campus/metro. Critical for 5G fronthaul (eCPRI)." },
|
||||||
|
{ standard: "25GBASE-ER", ieeeReference: "IEEE 802.3cc", speed: "25G", lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 30000, maxReachLabel: "30km", connector: "LC", fecRequired: true, formFactors: ["SFP28"], yearRatified: 2017, notes: "Extended reach 25G for metro and 5G midhaul." },
|
||||||
|
|
||||||
|
// 40G
|
||||||
|
{ standard: "40GBASE-SR4", ieeeReference: "IEEE 802.3ba", speed: "40G", lanes: 4, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "MMF (OM3/OM4)", wavelength: "850nm", maxReachMeters: 150, maxReachLabel: "100m (OM3) / 150m (OM4)", connector: "MPO-12", fecRequired: false, formFactors: ["QSFP+"], yearRatified: 2010, notes: "4x10G parallel optics. Can break out to 4x10GBASE-SR." },
|
||||||
|
{ standard: "40GBASE-LR4", ieeeReference: "IEEE 802.3ba", speed: "40G", lanes: 4, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm (4 CWDM wavelengths)", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: false, formFactors: ["QSFP+"], yearRatified: 2010, notes: "4 CWDM wavelengths over duplex LC." },
|
||||||
|
{ standard: "40GBASE-ER4", ieeeReference: "IEEE 802.3bm", speed: "40G", lanes: 4, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm (4 CWDM wavelengths)", maxReachMeters: 40000, maxReachLabel: "40km", connector: "LC", fecRequired: false, formFactors: ["QSFP+"], yearRatified: 2015, notes: "Extended reach 40G for metro ring and DCI." },
|
||||||
|
|
||||||
|
// 100G
|
||||||
|
{ standard: "100GBASE-SR4", ieeeReference: "IEEE 802.3bm", speed: "100G", lanes: 4, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "MMF (OM3/OM4)", wavelength: "850nm", maxReachMeters: 100, maxReachLabel: "70m (OM3) / 100m (OM4)", connector: "MPO-12", fecRequired: true, formFactors: ["QSFP28"], yearRatified: 2015, notes: "4x25G parallel. Breakout to 4x25GBASE-SR." },
|
||||||
|
{ standard: "100GBASE-SR2", ieeeReference: "100G Lambda MSA", speed: "100G", lanes: 2, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "MMF (OM4)", wavelength: "850nm", maxReachMeters: 100, maxReachLabel: "100m (OM4)", connector: "LC", fecRequired: true, formFactors: ["QSFP28"], yearRatified: 2018, notes: "MSA-defined. 2x50G PAM4 over duplex LC." },
|
||||||
|
{ standard: "100GBASE-DR", ieeeReference: "IEEE 802.3cd", speed: "100G", lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 500, maxReachLabel: "500m", connector: "LC", fecRequired: true, formFactors: ["QSFP28", "SFP-DD"], yearRatified: 2018, notes: "Single-lambda 100G. Key for leaf-spine architectures." },
|
||||||
|
{ standard: "100GBASE-FR1", ieeeReference: "IEEE 802.3cu", speed: "100G", lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 2000, maxReachLabel: "2km", connector: "LC", fecRequired: true, formFactors: ["QSFP28"], yearRatified: 2021, notes: "Single-lambda 100G for 2km." },
|
||||||
|
{ standard: "100GBASE-LR1", ieeeReference: "IEEE 802.3cu", speed: "100G", lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: true, formFactors: ["QSFP28"], yearRatified: 2021, notes: "Single-lambda 100G for 10km. New IXP standard replacing LR4." },
|
||||||
|
{ standard: "100GBASE-LR4", ieeeReference: "IEEE 802.3ba", speed: "100G", lanes: 4, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm (4 LAN-WDM wavelengths)", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: false, formFactors: ["QSFP28", "CFP", "CFP2", "CFP4"], yearRatified: 2010, notes: "4x25G LAN-WDM over duplex LC. Being replaced by LR1 single-lambda." },
|
||||||
|
{ standard: "100GBASE-CWDM4", ieeeReference: "100G CWDM4 MSA", speed: "100G", lanes: 4, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1271/1291/1311/1331nm", maxReachMeters: 2000, maxReachLabel: "2km", connector: "LC", fecRequired: false, formFactors: ["QSFP28"], yearRatified: 2014, notes: "MSA-defined. Lower-cost alternative to LR4 for 2km." },
|
||||||
|
{ standard: "100GBASE-PSM4", ieeeReference: "100G PSM4 MSA", speed: "100G", lanes: 4, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "SMF (parallel)", wavelength: "1310nm", maxReachMeters: 500, maxReachLabel: "500m", connector: "MPO-12", fecRequired: false, formFactors: ["QSFP28"], yearRatified: 2014, notes: "MSA-defined. 4x25G parallel single-mode." },
|
||||||
|
{ standard: "100GBASE-ZR", ieeeReference: "IEEE 802.3ct", speed: "100G", lanes: 1, laneRate: "~64 Gbaud", modulation: "DP-QPSK (coherent)", fiberType: "SMF (DWDM)", wavelength: "C-band (tunable)", maxReachMeters: 80000, maxReachLabel: "80km+ (DWDM amplified)", connector: "LC", fecRequired: true, formFactors: ["QSFP28"], yearRatified: 2021, notes: "Coherent 100G over DWDM systems." },
|
||||||
|
|
||||||
|
// 200G
|
||||||
|
{ standard: "200GBASE-SR4", ieeeReference: "IEEE 802.3cd", speed: "200G", lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "MMF (OM4)", wavelength: "850nm", maxReachMeters: 100, maxReachLabel: "70m (OM3) / 100m (OM4)", connector: "MPO-12", fecRequired: true, formFactors: ["QSFP56", "QSFP-DD"], yearRatified: 2018, notes: "4x50G PAM4 parallel." },
|
||||||
|
{ standard: "200GBASE-DR4", ieeeReference: "IEEE 802.3cd", speed: "200G", lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "SMF (parallel)", wavelength: "1310nm", maxReachMeters: 500, maxReachLabel: "500m", connector: "MPO-12", fecRequired: true, formFactors: ["QSFP56", "QSFP-DD"], yearRatified: 2018, notes: "4x50G parallel SMF. Can break out to 2x100G-DR or 4x50G." },
|
||||||
|
{ standard: "200GBASE-FR4", ieeeReference: "IEEE 802.3cu", speed: "200G", lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1310nm (4 CWDM wavelengths)", maxReachMeters: 2000, maxReachLabel: "2km", connector: "LC", fecRequired: true, formFactors: ["QSFP56", "QSFP-DD"], yearRatified: 2021, notes: "4x50G CWDM over duplex LC for 2km reach." },
|
||||||
|
{ standard: "200GBASE-LR4", ieeeReference: "IEEE 802.3cu", speed: "200G", lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1310nm (4 CWDM wavelengths)", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: true, formFactors: ["QSFP56", "QSFP-DD"], yearRatified: 2021, notes: "4x50G CWDM over duplex LC for 10km reach." },
|
||||||
|
|
||||||
|
// 400G
|
||||||
|
{ standard: "400GBASE-SR8", ieeeReference: "IEEE 802.3cm", speed: "400G", lanes: 8, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "MMF (OM4)", wavelength: "850nm", maxReachMeters: 100, maxReachLabel: "100m (OM4)", connector: "MPO-16", fecRequired: true, formFactors: ["QSFP-DD", "OSFP"], yearRatified: 2020, notes: "8x50G PAM4 parallel." },
|
||||||
|
{ standard: "400GBASE-SR4.2", ieeeReference: "IEEE 802.3cm", speed: "400G", lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "MMF", wavelength: "850nm + 910nm (BiDi)", maxReachMeters: 100, maxReachLabel: "100m", connector: "MPO-12", fecRequired: true, formFactors: ["QSFP-DD", "OSFP"], yearRatified: 2020, notes: "BiDi 400G over MPO-12 using two wavelengths." },
|
||||||
|
{ standard: "400GBASE-DR4", ieeeReference: "IEEE 802.3bs", speed: "400G", lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF (parallel)", wavelength: "1310nm", maxReachMeters: 500, maxReachLabel: "500m", connector: "MPO-12", fecRequired: true, formFactors: ["QSFP-DD", "OSFP"], yearRatified: 2017, notes: "4x100G parallel SMF. THE key 400G data center optic." },
|
||||||
|
{ standard: "400GBASE-FR4", ieeeReference: "IEEE 802.3cu", speed: "400G", lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1271/1291/1311/1331nm (CWDM4)", maxReachMeters: 2000, maxReachLabel: "2km", connector: "LC", fecRequired: true, formFactors: ["QSFP-DD", "OSFP"], yearRatified: 2021, notes: "4x100G CWDM over duplex LC." },
|
||||||
|
{ standard: "400GBASE-LR4-10", ieeeReference: "IEEE 802.3cu", speed: "400G", lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1271/1291/1311/1331nm (CWDM4)", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: true, formFactors: ["QSFP-DD", "OSFP"], yearRatified: 2021, notes: "4x100G CWDM for 10km. Standard for metro/IXP 400G." },
|
||||||
|
{ standard: "400GBASE-ZR (OIF 400ZR)", ieeeReference: "OIF-400ZR-01.0", speed: "400G", lanes: 1, laneRate: "~60 Gbaud", modulation: "DP-16QAM (coherent)", fiberType: "SMF", wavelength: "C-band (tunable, 75 GHz DWDM grid)", maxReachMeters: 120000, maxReachLabel: "up to 120km (amplified)", connector: "LC", fecRequired: true, formFactors: ["QSFP-DD", "OSFP", "CFP2-DCO"], yearRatified: 2020, notes: "OIF interoperable coherent 400G. Collapses IP/optical layers." },
|
||||||
|
|
||||||
|
// 800G
|
||||||
|
{ standard: "800GBASE-SR8", ieeeReference: "IEEE 802.3df", speed: "800G", lanes: 8, laneRate: "106.25 Gbaud", modulation: "PAM4", fiberType: "MMF (OM4)", wavelength: "850nm", maxReachMeters: 50, maxReachLabel: "50m (OM3) / 100m (OM4)", connector: "2x MPO-12", fecRequired: true, formFactors: ["OSFP"], yearRatified: 2024, notes: "8x100G PAM4 parallel." },
|
||||||
|
{ standard: "800GBASE-DR8", ieeeReference: "IEEE 802.3df", speed: "800G", lanes: 8, laneRate: "106.25 Gbaud", modulation: "PAM4", fiberType: "SMF (parallel)", wavelength: "1310nm", maxReachMeters: 500, maxReachLabel: "500m", connector: "2x MPO-12", fecRequired: true, formFactors: ["OSFP", "QSFP-DD800"], yearRatified: 2024, notes: "Primary 800G DC optic. 8x100G parallel SMF." },
|
||||||
|
{ standard: "OIF 800ZR", ieeeReference: "OIF-800ZR", speed: "800G", lanes: 1, laneRate: "~90 Gbaud", modulation: "DP-16QAM / DP-64QAM (coherent)", fiberType: "SMF", wavelength: "C-band (tunable)", maxReachMeters: 120000, maxReachLabel: "up to 120km+ (amplified)", connector: "LC", fecRequired: true, formFactors: ["OSFP", "QSFP-DD800", "CFP2-DCO"], yearRatified: 2024, notes: "800G pluggable coherent. Building on 400ZR success for DCI." },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Find a standard by exact or partial name. */
|
||||||
|
export function getStandard(name: string): Standard | undefined {
|
||||||
|
const q = name.toLowerCase();
|
||||||
|
return standards.find((s) => s.standard.toLowerCase() === q) ||
|
||||||
|
standards.find((s) => s.standard.toLowerCase().includes(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Search standards by keyword (speed, modulation, IEEE reference, etc.). */
|
||||||
|
export function searchStandards(query: string): Standard[] {
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
return standards.filter(
|
||||||
|
(s) =>
|
||||||
|
s.standard.toLowerCase().includes(q) ||
|
||||||
|
s.speed.toLowerCase().includes(q) ||
|
||||||
|
s.ieeeReference.toLowerCase().includes(q) ||
|
||||||
|
s.modulation.toLowerCase().includes(q) ||
|
||||||
|
s.notes.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
137
packages/core/src/types.ts
Normal file
137
packages/core/src/types.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* Core type definitions for the transceiver database.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type FormFactor =
|
||||||
|
| "SFP"
|
||||||
|
| "SFP+"
|
||||||
|
| "SFP28"
|
||||||
|
| "SFP56"
|
||||||
|
| "QSFP+"
|
||||||
|
| "QSFP28"
|
||||||
|
| "QSFP56"
|
||||||
|
| "QSFP-DD"
|
||||||
|
| "OSFP"
|
||||||
|
| "CFP"
|
||||||
|
| "CFP2"
|
||||||
|
| "CFP4"
|
||||||
|
| "CFP2-DCO"
|
||||||
|
| "XFP"
|
||||||
|
| "GBIC"
|
||||||
|
| "CXP"
|
||||||
|
| "SFP-DD"
|
||||||
|
| "SFP56-DD"
|
||||||
|
| "QSFP-DD800"
|
||||||
|
| "OSFP-XD";
|
||||||
|
|
||||||
|
export type FiberType = "MMF" | "SMF" | "MMF/SMF" | "Copper" | "N/A";
|
||||||
|
|
||||||
|
export type ConnectorType =
|
||||||
|
| "LC"
|
||||||
|
| "SC"
|
||||||
|
| "MPO-12"
|
||||||
|
| "MPO-16"
|
||||||
|
| "MPO-24"
|
||||||
|
| "RJ45"
|
||||||
|
| "None"
|
||||||
|
| "CS"
|
||||||
|
| "SN"
|
||||||
|
| "2xMPO-12";
|
||||||
|
|
||||||
|
export type TempRange = "COM" | "IND";
|
||||||
|
|
||||||
|
export type ProductCategory =
|
||||||
|
| "DataCenter"
|
||||||
|
| "Metro"
|
||||||
|
| "LongHaul"
|
||||||
|
| "DCI"
|
||||||
|
| "Access"
|
||||||
|
| "Coherent"
|
||||||
|
| "CWDM"
|
||||||
|
| "DWDM"
|
||||||
|
| "BiDi"
|
||||||
|
| "AOC"
|
||||||
|
| "DAC"
|
||||||
|
| "Breakout"
|
||||||
|
| "Legacy"
|
||||||
|
| "IXP"
|
||||||
|
| "5G"
|
||||||
|
| "AI";
|
||||||
|
|
||||||
|
export type PriceTier = "Budget" | "Standard" | "Premium";
|
||||||
|
|
||||||
|
export type MarketStatus = "Mainstream" | "Growth" | "Emerging" | "Legacy" | "EOL";
|
||||||
|
|
||||||
|
export interface VendorCompat {
|
||||||
|
vendor: string;
|
||||||
|
partPattern: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transceiver {
|
||||||
|
id: string;
|
||||||
|
standard: string;
|
||||||
|
ieeeReference?: string;
|
||||||
|
formFactor: FormFactor;
|
||||||
|
speed: string;
|
||||||
|
speedGbps: number;
|
||||||
|
lanes?: number;
|
||||||
|
laneRate?: string;
|
||||||
|
modulation?: string;
|
||||||
|
reachMeters: number;
|
||||||
|
reachLabel: string;
|
||||||
|
fiberType: FiberType;
|
||||||
|
wavelengths: string;
|
||||||
|
connector: ConnectorType;
|
||||||
|
powerConsumptionW: number;
|
||||||
|
tempRange: TempRange;
|
||||||
|
category: ProductCategory;
|
||||||
|
priceTier: PriceTier;
|
||||||
|
useCase: string;
|
||||||
|
vendors: VendorCompat[];
|
||||||
|
tags: string[];
|
||||||
|
generation?: string;
|
||||||
|
marketStatus?: MarketStatus;
|
||||||
|
yearIntroduced?: number;
|
||||||
|
breakoutCapable?: boolean;
|
||||||
|
breakoutTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Standard {
|
||||||
|
standard: string;
|
||||||
|
ieeeReference: string;
|
||||||
|
speed: string;
|
||||||
|
lanes: number;
|
||||||
|
laneRate: string;
|
||||||
|
modulation: string;
|
||||||
|
fiberType: string;
|
||||||
|
wavelength: string;
|
||||||
|
maxReachMeters: number;
|
||||||
|
maxReachLabel: string;
|
||||||
|
connector: string;
|
||||||
|
fecRequired: boolean;
|
||||||
|
formFactors: string[];
|
||||||
|
yearRatified: number;
|
||||||
|
notes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Competitor {
|
||||||
|
name: string;
|
||||||
|
type: "OEM" | "Whitebox" | "Manufacturer" | "Distributor" | "Compatible";
|
||||||
|
headquarters: string;
|
||||||
|
marketPosition: string;
|
||||||
|
formFactorsOffered: string[];
|
||||||
|
speedTiersOffered: string[];
|
||||||
|
strengths: string[];
|
||||||
|
weaknesses: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Breakout {
|
||||||
|
id: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
formFactor: string;
|
||||||
|
description: string;
|
||||||
|
cableType: "Passive" | "Active";
|
||||||
|
maxLength: string;
|
||||||
|
speedPerLane: string;
|
||||||
|
}
|
||||||
20
packages/core/tsconfig.json
Normal file
20
packages/core/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
31
packages/scraper/package.json
Normal file
31
packages/scraper/package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "@tip/scraper",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "TIP scraper engine — Crawlee + Playwright for competitor pricing, stock, datasheets, FAQs",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsx src/index.ts",
|
||||||
|
"scrape:fs": "tsx src/scrapers/fs-com.ts",
|
||||||
|
"scrape:cisco": "tsx src/scrapers/cisco-tmg.ts",
|
||||||
|
"scrape:optcore": "tsx src/scrapers/optcore.ts",
|
||||||
|
"scrape:news": "tsx src/scrapers/news.ts",
|
||||||
|
"scrape:all": "tsx src/index.ts --all"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"crawlee": "^3.12.0",
|
||||||
|
"playwright": "^1.50.0",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
"pg-boss": "^10.1.5",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"cheerio": "^1.0.0",
|
||||||
|
"xml2js": "^0.6.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/pg": "^8.11.11",
|
||||||
|
"@types/xml2js": "^0.4.14",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"tsx": "^4.19.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
69
packages/scraper/src/index.ts
Normal file
69
packages/scraper/src/index.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* TIP Scraper Engine — Main entry point.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* tsx src/index.ts — Start scheduler (production mode)
|
||||||
|
* tsx src/index.ts --all — Run all scrapers once
|
||||||
|
* tsx src/index.ts --fs — Run FS.com scraper once
|
||||||
|
* tsx src/index.ts --cisco — Run Cisco TMG scraper once
|
||||||
|
* tsx src/index.ts --optcore — Run Optcore scraper once
|
||||||
|
* tsx src/index.ts --news — Run news aggregator once
|
||||||
|
*/
|
||||||
|
import { createScheduler, registerSchedules, registerWorkers } from "./scheduler";
|
||||||
|
import { scrapeFs } from "./scrapers/fs-com";
|
||||||
|
import { scrapeCiscoTmg } from "./scrapers/cisco-tmg";
|
||||||
|
import { scrapeOptcore } from "./scrapers/optcore";
|
||||||
|
import { scrapeNews } from "./scrapers/news";
|
||||||
|
import { pool } from "./utils/db";
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
async function runOnce(): Promise<void> {
|
||||||
|
if (args.includes("--fs") || args.includes("--all")) {
|
||||||
|
await scrapeFs();
|
||||||
|
}
|
||||||
|
if (args.includes("--cisco") || args.includes("--all")) {
|
||||||
|
await scrapeCiscoTmg();
|
||||||
|
}
|
||||||
|
if (args.includes("--optcore") || args.includes("--all")) {
|
||||||
|
await scrapeOptcore();
|
||||||
|
}
|
||||||
|
if (args.includes("--news") || args.includes("--all")) {
|
||||||
|
await scrapeNews();
|
||||||
|
}
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runScheduler(): Promise<void> {
|
||||||
|
console.log("=== TIP Scraper Engine ===\n");
|
||||||
|
console.log("Mode: Scheduler (pg-boss)\n");
|
||||||
|
|
||||||
|
const boss = await createScheduler();
|
||||||
|
await registerSchedules(boss);
|
||||||
|
await registerWorkers(boss);
|
||||||
|
|
||||||
|
console.log("\nScheduler running. Press Ctrl+C to stop.\n");
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
const shutdown = async () => {
|
||||||
|
console.log("\nShutting down...");
|
||||||
|
await boss.stop();
|
||||||
|
await pool.end();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGINT", shutdown);
|
||||||
|
process.on("SIGTERM", shutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.some((a) => ["--all", "--fs", "--cisco", "--optcore", "--news"].includes(a))) {
|
||||||
|
runOnce().catch((err) => {
|
||||||
|
console.error("Fatal:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
runScheduler().catch((err) => {
|
||||||
|
console.error("Fatal:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
127
packages/scraper/src/scheduler.ts
Normal file
127
packages/scraper/src/scheduler.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* pg-boss Job Scheduler — manages scrape jobs with adaptive timing.
|
||||||
|
*
|
||||||
|
* Job types:
|
||||||
|
* scrape:pricing:fs — Every 4 hours for FS.com prices/stock
|
||||||
|
* scrape:pricing:optcore — Every 6 hours for Optcore prices/stock
|
||||||
|
* scrape:compat:cisco — Weekly for OEM compatibility matrices
|
||||||
|
* scrape:news — Every 6 hours for trade press and news
|
||||||
|
* scrape:docs — Weekly for manuals and datasheets
|
||||||
|
* scrape:faq — Weekly for vendor FAQ/troubleshooting pages
|
||||||
|
*/
|
||||||
|
import PgBoss from "pg-boss";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
config({ path: join(__dirname, "..", "..", "..", ".env") });
|
||||||
|
|
||||||
|
const connectionString = `postgres://${process.env.POSTGRES_USER || "tip"}:${process.env.POSTGRES_PASSWORD || "tip_dev_2026"}@${process.env.POSTGRES_HOST || "localhost"}:${process.env.POSTGRES_PORT || "5433"}/${process.env.POSTGRES_DB || "transceiver_db"}`;
|
||||||
|
|
||||||
|
export async function createScheduler(): Promise<PgBoss> {
|
||||||
|
const boss = new PgBoss({
|
||||||
|
connectionString,
|
||||||
|
retryLimit: 3,
|
||||||
|
retryDelay: 30,
|
||||||
|
retryBackoff: true,
|
||||||
|
expireInSeconds: 300, // 5 min timeout per job
|
||||||
|
monitorStateIntervalSeconds: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
boss.on("error", (error) => console.error("pg-boss error:", error));
|
||||||
|
|
||||||
|
await boss.start();
|
||||||
|
console.log("pg-boss scheduler started");
|
||||||
|
|
||||||
|
return boss;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerSchedules(boss: PgBoss): Promise<void> {
|
||||||
|
// pg-boss v10: create queues before scheduling
|
||||||
|
const queues = [
|
||||||
|
"scrape:pricing:fs",
|
||||||
|
"scrape:pricing:optcore",
|
||||||
|
"scrape:compat:cisco",
|
||||||
|
"scrape:news",
|
||||||
|
"scrape:faq",
|
||||||
|
"scrape:docs",
|
||||||
|
];
|
||||||
|
for (const q of queues) {
|
||||||
|
await boss.createQueue(q).catch(() => { /* already exists */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
// FS.com pricing (every 4 hours — JS rendering is slow)
|
||||||
|
await boss.schedule("scrape:pricing:fs", "0 */4 * * *", {}, {
|
||||||
|
retryLimit: 2,
|
||||||
|
expireInSeconds: 3600,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optcore pricing (every 6 hours — WP API enumeration + Playwright)
|
||||||
|
await boss.schedule("scrape:pricing:optcore", "0 */6 * * *", {}, {
|
||||||
|
retryLimit: 2,
|
||||||
|
expireInSeconds: 7200,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compatibility matrices (every Sunday at 3am)
|
||||||
|
await boss.schedule("scrape:compat:cisco", "0 3 * * 0", {}, {
|
||||||
|
retryLimit: 3,
|
||||||
|
expireInSeconds: 3600,
|
||||||
|
});
|
||||||
|
|
||||||
|
// News aggregation (every 6 hours)
|
||||||
|
await boss.schedule("scrape:news", "0 */6 * * *", {}, {
|
||||||
|
retryLimit: 2,
|
||||||
|
expireInSeconds: 1800,
|
||||||
|
});
|
||||||
|
|
||||||
|
// FAQ/KB scraping (every Wednesday at 2am)
|
||||||
|
await boss.schedule("scrape:faq", "0 2 * * 3", {}, {
|
||||||
|
retryLimit: 3,
|
||||||
|
expireInSeconds: 3600,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Document/datasheet check (every Saturday at 4am)
|
||||||
|
await boss.schedule("scrape:docs", "0 4 * * 6", {}, {
|
||||||
|
retryLimit: 3,
|
||||||
|
expireInSeconds: 7200,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("All schedules registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerWorkers(boss: PgBoss): Promise<void> {
|
||||||
|
// Lazy-load scrapers to avoid circular deps
|
||||||
|
const { scrapeFs } = await import("./scrapers/fs-com");
|
||||||
|
const { scrapeCiscoTmg } = await import("./scrapers/cisco-tmg");
|
||||||
|
const { scrapeOptcore } = await import("./scrapers/optcore");
|
||||||
|
const { scrapeNews } = await import("./scrapers/news");
|
||||||
|
|
||||||
|
await boss.work("scrape:pricing:fs", async (_job) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] Running: FS.com pricing`);
|
||||||
|
await scrapeFs();
|
||||||
|
});
|
||||||
|
|
||||||
|
await boss.work("scrape:pricing:optcore", async (_job) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] Running: Optcore pricing`);
|
||||||
|
await scrapeOptcore();
|
||||||
|
});
|
||||||
|
|
||||||
|
await boss.work("scrape:compat:cisco", async (_job) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] Running: Cisco TMG`);
|
||||||
|
await scrapeCiscoTmg();
|
||||||
|
});
|
||||||
|
|
||||||
|
await boss.work("scrape:news", async (_job) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] Running: News aggregation`);
|
||||||
|
await scrapeNews();
|
||||||
|
});
|
||||||
|
|
||||||
|
await boss.work("scrape:faq", async (_job) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] FAQ scraper — not yet implemented`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await boss.work("scrape:docs", async (_job) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] Docs scraper — not yet implemented`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("All workers registered");
|
||||||
|
}
|
||||||
155
packages/scraper/src/scrapers/cisco-tmg.ts
Normal file
155
packages/scraper/src/scrapers/cisco-tmg.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* Cisco TMG Matrix Scraper — Transceiver Compatibility
|
||||||
|
*
|
||||||
|
* Source: tmgmatrix.cisco.com
|
||||||
|
* Extracts: Switch model ↔ Transceiver compatibility data
|
||||||
|
* Stores: switches, compatibility table
|
||||||
|
*
|
||||||
|
* The TMG Matrix has a JSON API behind the scenes.
|
||||||
|
*/
|
||||||
|
import { CheerioCrawler } from "crawlee";
|
||||||
|
import { pool, ensureVendor } from "../utils/db";
|
||||||
|
|
||||||
|
const TMG_BASE = "https://tmgmatrix.cisco.com";
|
||||||
|
|
||||||
|
interface TmgEntry {
|
||||||
|
switchModel: string;
|
||||||
|
switchSeries: string;
|
||||||
|
transceiverPid: string;
|
||||||
|
transceiverDescription: string;
|
||||||
|
speed: string;
|
||||||
|
reach: string;
|
||||||
|
cableType: string;
|
||||||
|
connector: string;
|
||||||
|
minSoftware: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertCiscoSwitch(vendorId: string, model: string, series: string): Promise<string> {
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO switches (vendor_id, model, series, category, layer, managed)
|
||||||
|
VALUES ($1, $2, $3, 'DataCenter', 'L3', true)
|
||||||
|
ON CONFLICT (vendor_id, model) DO UPDATE SET series = EXCLUDED.series
|
||||||
|
RETURNING id`,
|
||||||
|
[vendorId, model, series]
|
||||||
|
);
|
||||||
|
return result.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertCompatibility(
|
||||||
|
switchId: string,
|
||||||
|
transceiverId: string,
|
||||||
|
firmwareMin: string
|
||||||
|
): Promise<void> {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO compatibility (switch_id, transceiver_id, verified_by, verification_method, status, firmware_min, source_url)
|
||||||
|
VALUES ($1, $2, 'Cisco TMG Matrix', 'vendor_matrix', 'compatible', $3, $4)
|
||||||
|
ON CONFLICT (switch_id, transceiver_id) DO UPDATE SET firmware_min = EXCLUDED.firmware_min`,
|
||||||
|
[switchId, transceiverId, firmwareMin || null, TMG_BASE]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scrapeCiscoTmg(): Promise<void> {
|
||||||
|
console.log("=== Cisco TMG Matrix Scraper Starting ===\n");
|
||||||
|
|
||||||
|
const ciscoVendorId = await ensureVendor(
|
||||||
|
"Cisco",
|
||||||
|
"oem",
|
||||||
|
"https://www.cisco.com",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const entries: TmgEntry[] = [];
|
||||||
|
|
||||||
|
// TMG Matrix uses a search API
|
||||||
|
// First, try the public HTML interface
|
||||||
|
const crawler = new CheerioCrawler({
|
||||||
|
maxConcurrency: 1,
|
||||||
|
maxRequestsPerMinute: 10, // Very respectful — Cisco rate limits aggressively
|
||||||
|
|
||||||
|
async requestHandler({ request, $, log }) {
|
||||||
|
log.info(`Scraping: ${request.url}`);
|
||||||
|
|
||||||
|
// The TMG Matrix renders a table with compatibility data
|
||||||
|
$("table tbody tr, .matrix-row, [class*='result-row']").each((_i, el) => {
|
||||||
|
const $row = $(el);
|
||||||
|
const cells = $row.find("td").map((_j, td) => $(td).text().trim()).get();
|
||||||
|
|
||||||
|
if (cells.length >= 4) {
|
||||||
|
entries.push({
|
||||||
|
switchModel: cells[0] || "",
|
||||||
|
switchSeries: cells[0]?.split(" ")[0] || "Nexus",
|
||||||
|
transceiverPid: cells[1] || "",
|
||||||
|
transceiverDescription: cells[2] || "",
|
||||||
|
speed: cells[3] || "",
|
||||||
|
reach: cells[4] || "",
|
||||||
|
cableType: cells[5] || "",
|
||||||
|
connector: cells[6] || "",
|
||||||
|
minSoftware: cells[7] || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start with Nexus switches (most relevant for Flexoptix)
|
||||||
|
await crawler.run([
|
||||||
|
`${TMG_BASE}/public/tmg?searchValue=Nexus+9000`,
|
||||||
|
`${TMG_BASE}/public/tmg?searchValue=Nexus+3000`,
|
||||||
|
`${TMG_BASE}/public/tmg?searchValue=Nexus+7000`,
|
||||||
|
`${TMG_BASE}/public/tmg?searchValue=Catalyst+9000`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(`\nEntries found: ${entries.length}`);
|
||||||
|
|
||||||
|
// Write to database
|
||||||
|
let switches = 0;
|
||||||
|
let compat = 0;
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.switchModel || !entry.transceiverPid) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const switchId = await upsertCiscoSwitch(
|
||||||
|
ciscoVendorId,
|
||||||
|
entry.switchModel,
|
||||||
|
entry.switchSeries
|
||||||
|
);
|
||||||
|
switches++;
|
||||||
|
|
||||||
|
// Try to match transceiver in our DB
|
||||||
|
const txResult = await pool.query(
|
||||||
|
`SELECT id FROM transceivers
|
||||||
|
WHERE part_number = $1
|
||||||
|
OR slug LIKE $2
|
||||||
|
OR standard_name ILIKE $3
|
||||||
|
LIMIT 1`,
|
||||||
|
[
|
||||||
|
entry.transceiverPid,
|
||||||
|
`%${entry.transceiverPid.toLowerCase().replace(/[^a-z0-9]/g, "")}%`,
|
||||||
|
`%${entry.speed}%${entry.reach}%`,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (txResult.rows.length > 0) {
|
||||||
|
await upsertCompatibility(switchId, txResult.rows[0].id, entry.minSoftware);
|
||||||
|
compat++;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Skip duplicates silently
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Switches upserted: ${switches}`);
|
||||||
|
console.log(`Compatibility entries: ${compat}`);
|
||||||
|
console.log("=== Cisco TMG Scraper Complete ===\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
scrapeCiscoTmg()
|
||||||
|
.then(() => pool.end())
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Fatal:", err);
|
||||||
|
pool.end();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
277
packages/scraper/src/scrapers/fs-com.ts
Normal file
277
packages/scraper/src/scrapers/fs-com.ts
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
/**
|
||||||
|
* FS.com Scraper — Prices, Stock, Product Catalog
|
||||||
|
*
|
||||||
|
* FS.com renders products client-side (JS), so we use PlaywrightCrawler.
|
||||||
|
* Categories: /c/optical-transceivers-9
|
||||||
|
*
|
||||||
|
* Respects: robots.txt, rate limiting (2s between requests)
|
||||||
|
*/
|
||||||
|
import { PlaywrightCrawler } from "crawlee";
|
||||||
|
import { ensureVendor, upsertPriceObservation, findOrCreateScrapedTransceiver, pool } from "../utils/db";
|
||||||
|
import { contentHash, parsePrice, parseStockLevel, parseQuantity } from "../utils/hash";
|
||||||
|
|
||||||
|
const BASE_URL = "https://www.fs.com";
|
||||||
|
|
||||||
|
const CATEGORY_URLS = [
|
||||||
|
"/c/1g-sfp-modules-702",
|
||||||
|
"/c/10g-sfp-plus-modules-703",
|
||||||
|
"/c/25g-sfp28-modules-704",
|
||||||
|
"/c/40g-qsfp-plus-modules-705",
|
||||||
|
"/c/100g-qsfp28-modules-706",
|
||||||
|
"/c/400g-qsfp-dd-modules-3102",
|
||||||
|
"/c/800g-osfp-modules-3449",
|
||||||
|
];
|
||||||
|
|
||||||
|
interface FsProduct {
|
||||||
|
partNumber: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
currency: string;
|
||||||
|
stockLevel: string;
|
||||||
|
quantity?: number;
|
||||||
|
url: string;
|
||||||
|
formFactor?: string;
|
||||||
|
speedGbps?: number;
|
||||||
|
speed?: string;
|
||||||
|
reachLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFormFactor(text: string): string | undefined {
|
||||||
|
const lower = text.toLowerCase();
|
||||||
|
if (lower.includes("osfp") && !lower.includes("qsfp")) return "OSFP";
|
||||||
|
if (lower.includes("qsfp-dd800") || lower.includes("qsfp-dd 800")) return "QSFP-DD800";
|
||||||
|
if (lower.includes("qsfp-dd")) return "QSFP-DD";
|
||||||
|
if (lower.includes("qsfp56")) return "QSFP56";
|
||||||
|
if (lower.includes("qsfp28")) return "QSFP28";
|
||||||
|
if (lower.includes("qsfp+") || lower.includes("qsfp plus")) return "QSFP+";
|
||||||
|
if (lower.includes("sfp56")) return "SFP56";
|
||||||
|
if (lower.includes("sfp28")) return "SFP28";
|
||||||
|
if (lower.includes("sfp+") || lower.includes("sfp plus")) return "SFP+";
|
||||||
|
if (lower.includes("sfp") && !lower.includes("qsfp")) return "SFP";
|
||||||
|
if (lower.includes("cfp2")) return "CFP2";
|
||||||
|
if (lower.includes("xfp")) return "XFP";
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectSpeed(text: string): { speed: string; speedGbps: number } | undefined {
|
||||||
|
const patterns: [RegExp, string, number][] = [
|
||||||
|
[/800\s*g/i, "800G", 800],
|
||||||
|
[/400\s*g/i, "400G", 400],
|
||||||
|
[/200\s*g/i, "200G", 200],
|
||||||
|
[/100\s*g/i, "100G", 100],
|
||||||
|
[/50\s*g/i, "50G", 50],
|
||||||
|
[/40\s*g/i, "40G", 40],
|
||||||
|
[/25\s*g/i, "25G", 25],
|
||||||
|
[/10\s*g/i, "10G", 10],
|
||||||
|
[/1\s*g\b/i, "1G", 1],
|
||||||
|
];
|
||||||
|
for (const [re, speed, gbps] of patterns) {
|
||||||
|
if (re.test(text)) return { speed, speedGbps: gbps };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectReach(text: string): string | undefined {
|
||||||
|
const match = text.match(/(\d+)\s*(m|km)\b/i);
|
||||||
|
if (match) return `${match[1]}${match[2].toLowerCase()}`;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scrapeFs(): Promise<void> {
|
||||||
|
console.log("=== FS.com Scraper Starting ===\n");
|
||||||
|
|
||||||
|
const vendorId = await ensureVendor(
|
||||||
|
"FS.COM",
|
||||||
|
"compatible",
|
||||||
|
"https://www.fs.com",
|
||||||
|
"https://www.fs.com/c/optical-transceivers-9"
|
||||||
|
);
|
||||||
|
console.log(`Vendor ID: ${vendorId}`);
|
||||||
|
|
||||||
|
const products: FsProduct[] = [];
|
||||||
|
let pagesScraped = 0;
|
||||||
|
|
||||||
|
const crawler = new PlaywrightCrawler({
|
||||||
|
maxConcurrency: 1,
|
||||||
|
maxRequestsPerMinute: 15,
|
||||||
|
requestHandlerTimeoutSecs: 60,
|
||||||
|
headless: true,
|
||||||
|
launchContext: {
|
||||||
|
launchOptions: {
|
||||||
|
args: ["--disable-blink-features=AutomationControlled"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async requestHandler({ page, request, log }) {
|
||||||
|
const url = request.url;
|
||||||
|
log.info(`Scraping: ${url}`);
|
||||||
|
|
||||||
|
// Wait for product list to render
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Try multiple selectors — FS.com changes DOM frequently
|
||||||
|
const productData = await page.evaluate(() => {
|
||||||
|
const results: Array<{
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
price: string;
|
||||||
|
stock: string;
|
||||||
|
partNumber: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// Strategy 1: Look for product links with prices nearby
|
||||||
|
const productLinks = document.querySelectorAll(
|
||||||
|
'a[href*="/products/"], a[href*="/product/"], .product-item a, .o-list-product a, [class*="product"] a[href]'
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const link of productLinks) {
|
||||||
|
const el = link as HTMLAnchorElement;
|
||||||
|
const name = el.textContent?.trim() || "";
|
||||||
|
const href = el.getAttribute("href") || "";
|
||||||
|
|
||||||
|
if (!name || name.length < 5 || !href) continue;
|
||||||
|
|
||||||
|
// Find price in parent/sibling elements
|
||||||
|
const container =
|
||||||
|
el.closest('[class*="product"]') ||
|
||||||
|
el.closest('[class*="item"]') ||
|
||||||
|
el.closest("li") ||
|
||||||
|
el.parentElement?.parentElement;
|
||||||
|
|
||||||
|
let price = "";
|
||||||
|
let stock = "";
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
const priceEl = container.querySelector(
|
||||||
|
'[class*="price"], [class*="Price"], .o-price, span[data-price]'
|
||||||
|
);
|
||||||
|
price = priceEl?.textContent?.trim() || "";
|
||||||
|
|
||||||
|
const stockEl = container.querySelector(
|
||||||
|
'[class*="stock"], [class*="Stock"], [class*="avail"], .o-stock'
|
||||||
|
);
|
||||||
|
stock = stockEl?.textContent?.trim() || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract part number from URL or text
|
||||||
|
const pn = href.split("/").pop()?.replace(".html", "")?.replace("#", "") || "";
|
||||||
|
|
||||||
|
if (name && (price || href.includes("/product"))) {
|
||||||
|
results.push({ name, href, price, stock, partNumber: pn });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Look for any element with $ or US$ price pattern
|
||||||
|
if (results.length === 0) {
|
||||||
|
const allText = document.querySelectorAll("*");
|
||||||
|
for (const el of allText) {
|
||||||
|
const text = el.textContent || "";
|
||||||
|
if (/US?\$\s*\d+\.\d{2}/.test(text) && text.length < 200) {
|
||||||
|
const linkEl = el.closest("a") || el.querySelector("a");
|
||||||
|
if (linkEl) {
|
||||||
|
results.push({
|
||||||
|
name: linkEl.textContent?.trim() || text.slice(0, 100),
|
||||||
|
href: linkEl.getAttribute("href") || "",
|
||||||
|
price: text.match(/US?\$\s*[\d,.]+/)?.[0] || "",
|
||||||
|
stock: "",
|
||||||
|
partNumber: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const item of productData) {
|
||||||
|
if (!item.name || !item.price) continue;
|
||||||
|
|
||||||
|
const { price, currency } = parsePrice(item.price);
|
||||||
|
const speedInfo = detectSpeed(item.name);
|
||||||
|
|
||||||
|
if (price > 0) {
|
||||||
|
products.push({
|
||||||
|
partNumber: item.partNumber || item.name.slice(0, 50),
|
||||||
|
name: item.name,
|
||||||
|
price,
|
||||||
|
currency,
|
||||||
|
stockLevel: item.stock ? parseStockLevel(item.stock) : "on_request",
|
||||||
|
quantity: item.stock ? parseQuantity(item.stock) : undefined,
|
||||||
|
url: item.href.startsWith("http") ? item.href : `${BASE_URL}${item.href}`,
|
||||||
|
formFactor: detectFormFactor(item.name),
|
||||||
|
speedGbps: speedInfo?.speedGbps,
|
||||||
|
speed: speedInfo?.speed,
|
||||||
|
reachLabel: detectReach(item.name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pagesScraped++;
|
||||||
|
log.info(` Found ${productData.length} items on page`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const startUrls = CATEGORY_URLS.map((path) => `${BASE_URL}${path}`);
|
||||||
|
await crawler.run(startUrls);
|
||||||
|
|
||||||
|
console.log(`\nPages scraped: ${pagesScraped}`);
|
||||||
|
console.log(`Products found: ${products.length}`);
|
||||||
|
|
||||||
|
// Deduplicate by partNumber
|
||||||
|
const uniqueProducts = new Map<string, FsProduct>();
|
||||||
|
for (const p of products) {
|
||||||
|
const key = p.partNumber || p.name;
|
||||||
|
if (!uniqueProducts.has(key)) {
|
||||||
|
uniqueProducts.set(key, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to database
|
||||||
|
let written = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
for (const p of uniqueProducts.values()) {
|
||||||
|
try {
|
||||||
|
const transceiverId = await findOrCreateScrapedTransceiver({
|
||||||
|
partNumber: p.partNumber,
|
||||||
|
vendorId,
|
||||||
|
formFactor: p.formFactor,
|
||||||
|
speedGbps: p.speedGbps,
|
||||||
|
speed: p.speed,
|
||||||
|
reachLabel: p.reachLabel,
|
||||||
|
category: "DataCenter",
|
||||||
|
});
|
||||||
|
|
||||||
|
const hash = contentHash({ price: p.price, stock: p.stockLevel, qty: p.quantity });
|
||||||
|
const isNew = await upsertPriceObservation({
|
||||||
|
transceiverId,
|
||||||
|
sourceVendorId: vendorId,
|
||||||
|
price: p.price,
|
||||||
|
currency: p.currency,
|
||||||
|
stockLevel: p.stockLevel,
|
||||||
|
quantityAvailable: p.quantity,
|
||||||
|
url: p.url,
|
||||||
|
contentHash: hash,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNew) written++;
|
||||||
|
else skipped++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` Error: ${p.partNumber}:`, (err as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nDatabase: ${written} new, ${skipped} unchanged (${uniqueProducts.size} unique)`);
|
||||||
|
console.log("=== FS.com Scraper Complete ===\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
scrapeFs()
|
||||||
|
.then(() => pool.end())
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Fatal:", err);
|
||||||
|
pool.end();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
269
packages/scraper/src/scrapers/news.ts
Normal file
269
packages/scraper/src/scrapers/news.ts
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
/**
|
||||||
|
* News Aggregator — Optics & Fiber Trade Press RSS Scraper
|
||||||
|
*
|
||||||
|
* Sources:
|
||||||
|
* - optics.org (photonics industry news)
|
||||||
|
* - SPIE Newsroom (photonics research)
|
||||||
|
* - Network World (data center / networking)
|
||||||
|
* - Light Reading (telecom)
|
||||||
|
* - Telecom Ramblings (industry commentary)
|
||||||
|
*
|
||||||
|
* Stores articles in news_articles table.
|
||||||
|
* Relevance filtering: keyword scoring for transceiver/optics topics.
|
||||||
|
*/
|
||||||
|
import { pool } from "../utils/db";
|
||||||
|
import { contentHash } from "../utils/hash";
|
||||||
|
import { parseStringPromise } from "xml2js";
|
||||||
|
|
||||||
|
// Categories allowed by news_articles CHECK constraint
|
||||||
|
type NewsCategory = "product_launch" | "market_report" | "standard" | "m_and_a" | "factory" | "event";
|
||||||
|
|
||||||
|
interface RssFeed {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
category: NewsCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NewsArticle {
|
||||||
|
title: string;
|
||||||
|
sourceUrl: string;
|
||||||
|
summary: string;
|
||||||
|
publishedAt: Date;
|
||||||
|
source: string;
|
||||||
|
category: NewsCategory | null;
|
||||||
|
relevanceScore: number;
|
||||||
|
contentHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FEEDS: RssFeed[] = [
|
||||||
|
{
|
||||||
|
name: "Optics.org",
|
||||||
|
url: "https://optics.org/rss/news",
|
||||||
|
category: "market_report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SPIE Newsroom",
|
||||||
|
url: "https://www.spie.org/newsroom/rss.xml",
|
||||||
|
category: "market_report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Network World - Data Center",
|
||||||
|
url: "https://www.networkworld.com/category/data-center/index.rss",
|
||||||
|
category: "market_report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CableFree",
|
||||||
|
url: "https://www.cablefree.net/rss",
|
||||||
|
category: "market_report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nature Photonics",
|
||||||
|
url: "https://www.nature.com/nphoton.rss",
|
||||||
|
category: "standard",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Keywords for relevance scoring
|
||||||
|
const HIGH_RELEVANCE = [
|
||||||
|
"transceiver", "sfp", "qsfp", "xfp", "cfp", "osfp",
|
||||||
|
"optical module", "fiber optic", "wavelength", "dwdm", "cwdm",
|
||||||
|
"400g", "800g", "1.6t", "coherent", "pluggable",
|
||||||
|
"ofc", "ecoc", "cioe",
|
||||||
|
];
|
||||||
|
|
||||||
|
const MEDIUM_RELEVANCE = [
|
||||||
|
"data center", "datacenter", "interconnect", "bandwidth",
|
||||||
|
"switch", "router", "cisco", "arista", "juniper",
|
||||||
|
"100g", "40g", "25g", "10g",
|
||||||
|
"silicon photonics", "photonic",
|
||||||
|
"ii-vi", "coherent", "lumentum", "inphi",
|
||||||
|
"flexoptix", "prolabs",
|
||||||
|
];
|
||||||
|
|
||||||
|
function scoreRelevance(title: string, summary: string): number {
|
||||||
|
const text = `${title} ${summary}`.toLowerCase();
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
for (const kw of HIGH_RELEVANCE) {
|
||||||
|
if (text.includes(kw)) score += 3;
|
||||||
|
}
|
||||||
|
for (const kw of MEDIUM_RELEVANCE) {
|
||||||
|
if (text.includes(kw)) score += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFeed(feed: RssFeed): Promise<NewsArticle[]> {
|
||||||
|
const articles: NewsArticle[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(feed.url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (compatible; TIP-NewsBot/1.0; +https://flexoptix.net)",
|
||||||
|
Accept: "application/rss+xml, application/xml, text/xml",
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
console.warn(` Feed ${feed.name} returned ${resp.status}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawXml = await resp.text();
|
||||||
|
// Sanitize common RSS issues: unescaped & in URLs, attribute-without-value
|
||||||
|
const xml = rawXml
|
||||||
|
.replace(/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[\dA-Fa-f]+;)/g, "&")
|
||||||
|
.replace(/(<\w[^>]*)\s+(\w+)=([^"'\s>]+)(?=[\s/>])/g, '$1 $2="$3"');
|
||||||
|
const parsed = await parseStringPromise(xml, { explicitArray: false, strict: false });
|
||||||
|
|
||||||
|
// strict: false makes keys uppercase; support both
|
||||||
|
const rss = parsed?.rss || parsed?.RSS;
|
||||||
|
const channel = rss?.channel || rss?.CHANNEL || parsed?.feed || parsed?.FEED;
|
||||||
|
if (!channel) return [];
|
||||||
|
|
||||||
|
const items = channel.item || channel.ITEM || channel.entry || channel.ENTRY || [];
|
||||||
|
const itemArray = Array.isArray(items) ? items : [items];
|
||||||
|
|
||||||
|
for (const item of itemArray) {
|
||||||
|
const title = extractText(item.title || item.TITLE) || "";
|
||||||
|
const url = extractLink(item) || "";
|
||||||
|
const summary = extractText(
|
||||||
|
item.description || item.DESCRIPTION || item.summary || item.SUMMARY || item["content:encoded"]
|
||||||
|
) || "";
|
||||||
|
const pubDate = item.pubDate || item.PUBDATE || item.published || item.updated || "";
|
||||||
|
|
||||||
|
if (!title || !url) continue;
|
||||||
|
|
||||||
|
const publishedAt = pubDate ? new Date(pubDate) : new Date();
|
||||||
|
if (isNaN(publishedAt.getTime())) continue;
|
||||||
|
|
||||||
|
// Skip articles older than 7 days
|
||||||
|
const ageMs = Date.now() - publishedAt.getTime();
|
||||||
|
if (ageMs > 7 * 24 * 60 * 60 * 1000) continue;
|
||||||
|
|
||||||
|
const relevanceScore = scoreRelevance(title, summary);
|
||||||
|
const hash = contentHash({ title, url });
|
||||||
|
|
||||||
|
articles.push({
|
||||||
|
title: title.slice(0, 500),
|
||||||
|
sourceUrl: url.slice(0, 1000),
|
||||||
|
summary: stripHtml(summary).slice(0, 2000),
|
||||||
|
publishedAt,
|
||||||
|
source: feed.name,
|
||||||
|
category: feed.category as NewsCategory,
|
||||||
|
relevanceScore,
|
||||||
|
contentHash: hash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(` Feed ${feed.name} error:`, (err as Error).message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return articles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractText(value: unknown): string {
|
||||||
|
if (!value) return "";
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
const obj = value as Record<string, unknown>;
|
||||||
|
return String(obj._ || obj["#text"] || "");
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLink(item: Record<string, unknown>): string {
|
||||||
|
const link = item.link || item.LINK;
|
||||||
|
if (typeof link === "string") return link;
|
||||||
|
if (Array.isArray(link)) {
|
||||||
|
const rel = (link as Array<Record<string, unknown>>).find(
|
||||||
|
(l) => !l["$"] || (l["$"] as Record<string, string>).rel === "alternate"
|
||||||
|
);
|
||||||
|
return String((rel?.["$"] as Record<string, string>)?.href || rel?._ || "");
|
||||||
|
}
|
||||||
|
if (typeof link === "object" && link !== null) {
|
||||||
|
const l = link as Record<string, unknown>;
|
||||||
|
return String((l["$"] as Record<string, string>)?.href || l._ || "");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripHtml(html: string): string {
|
||||||
|
return html
|
||||||
|
.replace(/<[^>]+>/g, " ")
|
||||||
|
.replace(/ /g, " ")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertArticle(article: NewsArticle): Promise<boolean> {
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO news_articles (title, source_url, summary, published_at, source, category, relevance_score, content_hash)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
ON CONFLICT (source_url) DO UPDATE
|
||||||
|
SET relevance_score = EXCLUDED.relevance_score,
|
||||||
|
content_hash = EXCLUDED.content_hash
|
||||||
|
RETURNING (xmax = 0) AS inserted`,
|
||||||
|
[
|
||||||
|
article.title,
|
||||||
|
article.sourceUrl,
|
||||||
|
article.summary,
|
||||||
|
article.publishedAt,
|
||||||
|
article.source,
|
||||||
|
article.category,
|
||||||
|
article.relevanceScore,
|
||||||
|
article.contentHash,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return result.rows[0]?.inserted ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scrapeNews(): Promise<void> {
|
||||||
|
console.log("=== News Scraper Starting ===\n");
|
||||||
|
|
||||||
|
let totalFetched = 0;
|
||||||
|
let totalWritten = 0;
|
||||||
|
let totalRelevant = 0;
|
||||||
|
|
||||||
|
for (const feed of FEEDS) {
|
||||||
|
console.log(`Fetching: ${feed.name} (${feed.url})`);
|
||||||
|
const articles = await fetchFeed(feed);
|
||||||
|
console.log(` → ${articles.length} articles (last 7 days)`);
|
||||||
|
|
||||||
|
for (const article of articles) {
|
||||||
|
totalFetched++;
|
||||||
|
if (article.relevanceScore > 0) totalRelevant++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isNew = await upsertArticle(article);
|
||||||
|
if (isNew) totalWritten++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` Error saving article:`, (err as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit between feeds
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nFetched: ${totalFetched} articles`);
|
||||||
|
console.log(`Relevant (score > 0): ${totalRelevant}`);
|
||||||
|
console.log(`Written: ${totalWritten} new`);
|
||||||
|
console.log("=== News Scraper Complete ===\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
scrapeNews()
|
||||||
|
.then(() => pool.end())
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Fatal:", err);
|
||||||
|
pool.end();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
297
packages/scraper/src/scrapers/optcore.ts
Normal file
297
packages/scraper/src/scrapers/optcore.ts
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
/**
|
||||||
|
* Optcore.net Scraper — Most transparent pricing in the industry.
|
||||||
|
* Prices start at $5.50, fully public, no bot protection.
|
||||||
|
*
|
||||||
|
* Strategy: WP REST API to enumerate transceiver product URLs,
|
||||||
|
* then PlaywrightCrawler to render each page and extract price.
|
||||||
|
*
|
||||||
|
* Optcore uses Flatsome WooCommerce with Cloudflare Rocket Loader
|
||||||
|
* (JS lazy-loading) — static HTML has no product data.
|
||||||
|
*/
|
||||||
|
import { PlaywrightCrawler } from "crawlee";
|
||||||
|
import { ensureVendor, upsertPriceObservation, findOrCreateScrapedTransceiver, pool } from "../utils/db";
|
||||||
|
import { contentHash, parsePrice, parseStockLevel } from "../utils/hash";
|
||||||
|
|
||||||
|
const BASE_URL = "https://www.optcore.net";
|
||||||
|
|
||||||
|
// Transceiver category IDs from /wp-json/wp/v2/product_cat
|
||||||
|
// Filtered to optical transceiver categories with products
|
||||||
|
const TRANSCEIVER_CATEGORY_IDS = [
|
||||||
|
309, // 10G SFP+
|
||||||
|
173, // 1G SFP
|
||||||
|
76, // 100G QSFP28
|
||||||
|
79, // 25G SFP28
|
||||||
|
73, // 40G QSFP+
|
||||||
|
311, // 10G BiDi SFP+
|
||||||
|
313, // 10G CWDM SFP+
|
||||||
|
312, // 10G DWDM SFP+
|
||||||
|
333, // 10G XFP
|
||||||
|
1088, // 10GBase-T SFP+
|
||||||
|
59, // 8G/10G/16G SFP+
|
||||||
|
1102, // BiDi SFP
|
||||||
|
4097, // 400G QSFP-DD
|
||||||
|
77, // 100G CFP/CFP2/CFP4
|
||||||
|
4101, // 200G QSFP56
|
||||||
|
4092, // 50G SFP56
|
||||||
|
6441, // 800G OSFP
|
||||||
|
];
|
||||||
|
|
||||||
|
interface OptcoreProduct {
|
||||||
|
partNumber: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
currency: string;
|
||||||
|
stockLevel: string;
|
||||||
|
url: string;
|
||||||
|
formFactor?: string;
|
||||||
|
speedGbps?: number;
|
||||||
|
speed?: string;
|
||||||
|
reachLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFormFactor(text: string): string | undefined {
|
||||||
|
const lower = text.toLowerCase();
|
||||||
|
if (lower.includes("osfp") && !lower.includes("qsfp")) return "OSFP";
|
||||||
|
if (lower.includes("qsfp-dd")) return "QSFP-DD";
|
||||||
|
if (lower.includes("qsfp56")) return "QSFP56";
|
||||||
|
if (lower.includes("qsfp28")) return "QSFP28";
|
||||||
|
if (lower.includes("qsfp+") || lower.includes("qsfp plus")) return "QSFP+";
|
||||||
|
if (lower.includes("sfp28")) return "SFP28";
|
||||||
|
if (lower.includes("sfp56")) return "SFP56";
|
||||||
|
if (lower.includes("sfp+") || lower.includes("sfp plus")) return "SFP+";
|
||||||
|
if (lower.includes("cfp4")) return "CFP4";
|
||||||
|
if (lower.includes("cfp2")) return "CFP2";
|
||||||
|
if (lower.includes("cfp")) return "CFP";
|
||||||
|
if (lower.includes("xfp")) return "XFP";
|
||||||
|
if (lower.includes("sfp") && !lower.includes("qsfp")) return "SFP";
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectSpeed(text: string): { speed: string; speedGbps: number } | undefined {
|
||||||
|
const patterns: [RegExp, string, number][] = [
|
||||||
|
[/800\s*g/i, "800G", 800],
|
||||||
|
[/400\s*g/i, "400G", 400],
|
||||||
|
[/200\s*g/i, "200G", 200],
|
||||||
|
[/100\s*g/i, "100G", 100],
|
||||||
|
[/50\s*g/i, "50G", 50],
|
||||||
|
[/40\s*g/i, "40G", 40],
|
||||||
|
[/25\s*g/i, "25G", 25],
|
||||||
|
[/16\s*g/i, "16G", 16],
|
||||||
|
[/10\s*g/i, "10G", 10],
|
||||||
|
[/1000\s*base/i, "1G", 1],
|
||||||
|
[/1\s*g\b/i, "1G", 1],
|
||||||
|
];
|
||||||
|
for (const [re, speed, gbps] of patterns) {
|
||||||
|
if (re.test(text)) return { speed, speedGbps: gbps };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectReach(text: string): string | undefined {
|
||||||
|
const match = text.match(/(\d+)\s*(m|km)\b/i);
|
||||||
|
if (match) return `${match[1]}${match[2].toLowerCase()}`;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch product URLs for transceiver categories via WP REST API.
|
||||||
|
* Returns up to 2000 product URLs with title + slug for metadata.
|
||||||
|
*/
|
||||||
|
async function fetchTransceiverUrls(): Promise<Array<{ url: string; title: string; partNumber: string }>> {
|
||||||
|
const results: Array<{ url: string; title: string; partNumber: string }> = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
for (const catId of TRANSCEIVER_CATEGORY_IDS) {
|
||||||
|
let page = 1;
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const apiUrl = `${BASE_URL}/wp-json/wp/v2/product?product_cat=${catId}&per_page=100&page=${page}&_fields=slug,link,title`;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(apiUrl, {
|
||||||
|
headers: { "User-Agent": "Mozilla/5.0 (compatible; TIP-Scraper/1.0)" },
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) break;
|
||||||
|
|
||||||
|
const totalPages = parseInt(resp.headers.get("X-WP-TotalPages") || "1");
|
||||||
|
const products: Array<{ slug: string; link: string; title: { rendered: string } }> = await resp.json();
|
||||||
|
|
||||||
|
for (const p of products) {
|
||||||
|
if (!seen.has(p.slug)) {
|
||||||
|
seen.add(p.slug);
|
||||||
|
results.push({
|
||||||
|
url: p.link,
|
||||||
|
title: p.title.rendered,
|
||||||
|
partNumber: p.slug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMore = page < totalPages;
|
||||||
|
page++;
|
||||||
|
|
||||||
|
// Rate limit: 10 req/sec max
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
} catch {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scrapeOptcore(): Promise<void> {
|
||||||
|
console.log("=== Optcore.net Scraper Starting ===\n");
|
||||||
|
|
||||||
|
const vendorId = await ensureVendor(
|
||||||
|
"Optcore",
|
||||||
|
"compatible",
|
||||||
|
"https://www.optcore.net",
|
||||||
|
"https://www.optcore.net/product-category/optical-transceiver/"
|
||||||
|
);
|
||||||
|
console.log(`Vendor ID: ${vendorId}`);
|
||||||
|
|
||||||
|
// Step 1: Enumerate transceiver product URLs via WP REST API
|
||||||
|
console.log("Fetching product URLs via WP REST API...");
|
||||||
|
const productMeta = await fetchTransceiverUrls();
|
||||||
|
console.log(`Found ${productMeta.length} transceiver product URLs`);
|
||||||
|
|
||||||
|
// Build a map for quick metadata lookup
|
||||||
|
const metaByUrl = new Map(productMeta.map((p) => [p.url, p]));
|
||||||
|
|
||||||
|
const products: OptcoreProduct[] = [];
|
||||||
|
let pagesScraped = 0;
|
||||||
|
|
||||||
|
// Step 2: Render each product page with Playwright to extract price
|
||||||
|
const crawler = new PlaywrightCrawler({
|
||||||
|
maxConcurrency: 3,
|
||||||
|
maxRequestsPerMinute: 30,
|
||||||
|
requestHandlerTimeoutSecs: 30,
|
||||||
|
headless: true,
|
||||||
|
launchContext: {
|
||||||
|
launchOptions: {
|
||||||
|
args: ["--disable-blink-features=AutomationControlled", "--no-sandbox"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async requestHandler({ page, request, log }) {
|
||||||
|
const url = request.url;
|
||||||
|
log.info(`Scraping: ${url}`);
|
||||||
|
|
||||||
|
// Wait for WooCommerce price element to appear
|
||||||
|
try {
|
||||||
|
await page.waitForSelector(".woocommerce-Price-amount, .price .amount, [class*=\"price\"]", {
|
||||||
|
timeout: 8000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Price element not found — might be out of stock or JS failed
|
||||||
|
log.warning(`No price element found: ${url}`);
|
||||||
|
pagesScraped++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await page.evaluate(() => {
|
||||||
|
// Product title
|
||||||
|
const title =
|
||||||
|
document.querySelector("h1.product_title, h1.entry-title, h1")?.textContent?.trim() || "";
|
||||||
|
|
||||||
|
// Price — WooCommerce renders: <span class="price"><bdi><span class="woocommerce-Price-currencySymbol">$</span>5.50</bdi></span>
|
||||||
|
const priceEl = document.querySelector(
|
||||||
|
".price ins .woocommerce-Price-amount, .price .woocommerce-Price-amount, .woocommerce-Price-amount"
|
||||||
|
);
|
||||||
|
const priceText = priceEl?.textContent?.trim() || "";
|
||||||
|
|
||||||
|
// Stock
|
||||||
|
const stockEl = document.querySelector(".stock, .availability, [class*=\"stock\"]");
|
||||||
|
const stockText = stockEl?.textContent?.trim() || "";
|
||||||
|
|
||||||
|
return { title, priceText, stockText };
|
||||||
|
});
|
||||||
|
|
||||||
|
const meta = metaByUrl.get(url);
|
||||||
|
const name = data.title || meta?.title || url.split("/").filter(Boolean).pop() || "";
|
||||||
|
const partNumber = meta?.partNumber || url.split("/").filter(Boolean).pop() || "";
|
||||||
|
|
||||||
|
const { price, currency } = parsePrice(data.priceText);
|
||||||
|
if (price > 0) {
|
||||||
|
const speedInfo = detectSpeed(name);
|
||||||
|
products.push({
|
||||||
|
partNumber,
|
||||||
|
name,
|
||||||
|
price,
|
||||||
|
currency,
|
||||||
|
stockLevel: data.stockText ? parseStockLevel(data.stockText) : "in_stock",
|
||||||
|
url,
|
||||||
|
formFactor: detectFormFactor(name),
|
||||||
|
speedGbps: speedInfo?.speedGbps,
|
||||||
|
speed: speedInfo?.speed,
|
||||||
|
reachLabel: detectReach(name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pagesScraped++;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const urls = productMeta.map((p) => p.url);
|
||||||
|
await crawler.run(urls);
|
||||||
|
|
||||||
|
console.log(`\nPages scraped: ${pagesScraped}`);
|
||||||
|
console.log(`Products with price: ${products.length}`);
|
||||||
|
|
||||||
|
// Deduplicate
|
||||||
|
const unique = new Map<string, OptcoreProduct>();
|
||||||
|
for (const p of products) {
|
||||||
|
if (!unique.has(p.partNumber)) unique.set(p.partNumber, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to DB
|
||||||
|
let written = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
for (const p of unique.values()) {
|
||||||
|
try {
|
||||||
|
const transceiverId = await findOrCreateScrapedTransceiver({
|
||||||
|
partNumber: p.partNumber,
|
||||||
|
vendorId,
|
||||||
|
formFactor: p.formFactor,
|
||||||
|
speedGbps: p.speedGbps,
|
||||||
|
speed: p.speed,
|
||||||
|
reachLabel: p.reachLabel,
|
||||||
|
category: "DataCenter",
|
||||||
|
});
|
||||||
|
|
||||||
|
const hash = contentHash({ price: p.price, stock: p.stockLevel });
|
||||||
|
const isNew = await upsertPriceObservation({
|
||||||
|
transceiverId,
|
||||||
|
sourceVendorId: vendorId,
|
||||||
|
price: p.price,
|
||||||
|
currency: p.currency,
|
||||||
|
stockLevel: p.stockLevel,
|
||||||
|
url: p.url,
|
||||||
|
contentHash: hash,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNew) written++;
|
||||||
|
else skipped++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` Error: ${p.partNumber}:`, (err as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nDatabase: ${written} new, ${skipped} unchanged (${unique.size} unique)`);
|
||||||
|
console.log("=== Optcore.net Scraper Complete ===\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
scrapeOptcore()
|
||||||
|
.then(() => pool.end())
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Fatal:", err);
|
||||||
|
pool.end();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
123
packages/scraper/src/utils/db.ts
Normal file
123
packages/scraper/src/utils/db.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { Pool } from "pg";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
config({ path: join(__dirname, "..", "..", "..", "..", ".env") });
|
||||||
|
|
||||||
|
export const pool = new Pool({
|
||||||
|
host: process.env.POSTGRES_HOST || "localhost",
|
||||||
|
port: parseInt(process.env.POSTGRES_PORT || "5433"),
|
||||||
|
database: process.env.POSTGRES_DB || "transceiver_db",
|
||||||
|
user: process.env.POSTGRES_USER || "tip",
|
||||||
|
password: process.env.POSTGRES_PASSWORD || "tip_dev_2026",
|
||||||
|
max: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function upsertPriceObservation(params: {
|
||||||
|
transceiverId: string;
|
||||||
|
sourceVendorId: string;
|
||||||
|
price: number;
|
||||||
|
currency: string;
|
||||||
|
stockLevel: string;
|
||||||
|
quantityAvailable?: number;
|
||||||
|
leadTimeDays?: number;
|
||||||
|
url?: string;
|
||||||
|
contentHash: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
// Check if price changed via content hash
|
||||||
|
const existing = await pool.query(
|
||||||
|
`SELECT content_hash FROM price_observations
|
||||||
|
WHERE transceiver_id = $1 AND source_vendor_id = $2
|
||||||
|
ORDER BY time DESC LIMIT 1`,
|
||||||
|
[params.transceiverId, params.sourceVendorId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing.rows.length > 0 && existing.rows[0].content_hash === params.contentHash) {
|
||||||
|
return false; // No change
|
||||||
|
}
|
||||||
|
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO price_observations (time, transceiver_id, source_vendor_id, price, currency, stock_level, quantity_available, lead_time_days, url, content_hash)
|
||||||
|
VALUES (NOW(), $1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||||
|
[
|
||||||
|
params.transceiverId,
|
||||||
|
params.sourceVendorId,
|
||||||
|
params.price,
|
||||||
|
params.currency,
|
||||||
|
params.stockLevel,
|
||||||
|
params.quantityAvailable || null,
|
||||||
|
params.leadTimeDays || null,
|
||||||
|
params.url || null,
|
||||||
|
params.contentHash,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return true; // New observation written
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findOrCreateScrapedTransceiver(params: {
|
||||||
|
partNumber: string;
|
||||||
|
vendorId: string;
|
||||||
|
formFactor?: string;
|
||||||
|
speedGbps?: number;
|
||||||
|
speed?: string;
|
||||||
|
reachMeters?: number;
|
||||||
|
reachLabel?: string;
|
||||||
|
fiberType?: string;
|
||||||
|
wavelengths?: string;
|
||||||
|
category?: string;
|
||||||
|
}): Promise<string> {
|
||||||
|
// Try to match existing transceiver by part number + vendor
|
||||||
|
const existing = await pool.query(
|
||||||
|
`SELECT id FROM transceivers WHERE part_number = $1 AND vendor_id = $2`,
|
||||||
|
[params.partNumber, params.vendorId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing.rows.length > 0) {
|
||||||
|
return existing.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new transceiver entry
|
||||||
|
const slug = `scraped-${params.partNumber.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO transceivers (slug, part_number, vendor_id, form_factor, speed_gbps, speed, reach_meters, reach_label, fiber_type, wavelengths, category, market_status)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'Mainstream')
|
||||||
|
ON CONFLICT (slug) DO UPDATE SET updated_at = NOW()
|
||||||
|
RETURNING id`,
|
||||||
|
[
|
||||||
|
slug,
|
||||||
|
params.partNumber,
|
||||||
|
params.vendorId,
|
||||||
|
params.formFactor || "SFP",
|
||||||
|
params.speedGbps || 0,
|
||||||
|
params.speed || "Unknown",
|
||||||
|
params.reachMeters || 0,
|
||||||
|
params.reachLabel || "",
|
||||||
|
params.fiberType || "",
|
||||||
|
params.wavelengths || "",
|
||||||
|
params.category || "DataCenter",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return result.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVendorId(name: string): Promise<string | null> {
|
||||||
|
const result = await pool.query(`SELECT id FROM vendors WHERE name = $1`, [name]);
|
||||||
|
return result.rows[0]?.id || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureVendor(
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
website?: string,
|
||||||
|
shopUrl?: string
|
||||||
|
): Promise<string> {
|
||||||
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO vendors (name, slug, type, website, shop_url, is_competitor)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, true)
|
||||||
|
ON CONFLICT (name) DO UPDATE SET shop_url = COALESCE(EXCLUDED.shop_url, vendors.shop_url)
|
||||||
|
RETURNING id`,
|
||||||
|
[name, slug, type, website || null, shopUrl || null]
|
||||||
|
);
|
||||||
|
return result.rows[0].id;
|
||||||
|
}
|
||||||
71
packages/scraper/src/utils/hash.ts
Normal file
71
packages/scraper/src/utils/hash.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { createHash } from "crypto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate SHA-256 content hash for change detection.
|
||||||
|
* Only hashes the fields that matter (price, stock, quantity).
|
||||||
|
*/
|
||||||
|
export function contentHash(data: Record<string, unknown>): string {
|
||||||
|
const normalized = JSON.stringify(data, Object.keys(data).sort());
|
||||||
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse price string into number.
|
||||||
|
* Handles: "$12.50", "12,50 €", "US$12.50", "12.50 USD"
|
||||||
|
*/
|
||||||
|
export function parsePrice(raw: string): { price: number; currency: string } {
|
||||||
|
const cleaned = raw.replace(/[^\d.,]/g, "").replace(",", ".");
|
||||||
|
const price = parseFloat(cleaned);
|
||||||
|
const currency = raw.includes("€")
|
||||||
|
? "EUR"
|
||||||
|
: raw.includes("£")
|
||||||
|
? "GBP"
|
||||||
|
: raw.includes("¥")
|
||||||
|
? "CNY"
|
||||||
|
: "USD";
|
||||||
|
return { price: isNaN(price) ? 0 : price, currency };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine stock level from various text representations.
|
||||||
|
*/
|
||||||
|
export function parseStockLevel(
|
||||||
|
raw: string
|
||||||
|
): "in_stock" | "low_stock" | "out_of_stock" | "on_request" | "discontinued" {
|
||||||
|
const lower = raw.toLowerCase();
|
||||||
|
if (lower.includes("in stock") || lower.includes("auf lager") || lower.includes("available"))
|
||||||
|
return "in_stock";
|
||||||
|
if (lower.includes("low stock") || lower.includes("few left") || lower.includes("limited"))
|
||||||
|
return "low_stock";
|
||||||
|
if (
|
||||||
|
lower.includes("out of stock") ||
|
||||||
|
lower.includes("sold out") ||
|
||||||
|
lower.includes("nicht verfügbar") ||
|
||||||
|
lower.includes("unavailable")
|
||||||
|
)
|
||||||
|
return "out_of_stock";
|
||||||
|
if (lower.includes("discontinued") || lower.includes("eol") || lower.includes("end of life"))
|
||||||
|
return "discontinued";
|
||||||
|
return "on_request";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract numeric quantity from stock text.
|
||||||
|
* "23 in stock" → 23, "500+ available" → 500
|
||||||
|
*/
|
||||||
|
export function parseQuantity(raw: string): number | undefined {
|
||||||
|
const match = raw.match(/(\d+)\+?\s*(in stock|available|auf lager|stück|units|pcs)/i);
|
||||||
|
return match ? parseInt(match[1]) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse lead time from text.
|
||||||
|
* "Ships in 3-5 days" → 5, "2 weeks" → 14
|
||||||
|
*/
|
||||||
|
export function parseLeadTime(raw: string): number | undefined {
|
||||||
|
const dayMatch = raw.match(/(\d+)\s*(business\s+)?days?/i);
|
||||||
|
if (dayMatch) return parseInt(dayMatch[1]);
|
||||||
|
const weekMatch = raw.match(/(\d+)\s*weeks?/i);
|
||||||
|
if (weekMatch) return parseInt(weekMatch[1]) * 7;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
18
packages/scraper/tsconfig.json
Normal file
18
packages/scraper/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
42
scripts/migrate.ts
Normal file
42
scripts/migrate.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { readFileSync, readdirSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
|
||||||
|
config();
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
host: process.env.POSTGRES_HOST || "localhost",
|
||||||
|
port: parseInt(process.env.POSTGRES_PORT || "5432"),
|
||||||
|
database: process.env.POSTGRES_DB || "transceiver_db",
|
||||||
|
user: process.env.POSTGRES_USER || "tip",
|
||||||
|
password: process.env.POSTGRES_PASSWORD || "tip_dev_2026",
|
||||||
|
});
|
||||||
|
|
||||||
|
async function migrate(): Promise<void> {
|
||||||
|
const sqlDir = join(__dirname, "..", "sql");
|
||||||
|
const files = readdirSync(sqlDir)
|
||||||
|
.filter((f) => f.endsWith(".sql"))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
console.log(`Found ${files.length} migration files`);
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
for (const file of files) {
|
||||||
|
const sql = readFileSync(join(sqlDir, file), "utf-8");
|
||||||
|
console.log(`Running: ${file}...`);
|
||||||
|
await client.query(sql);
|
||||||
|
console.log(` Done: ${file}`);
|
||||||
|
}
|
||||||
|
console.log("\nAll migrations completed successfully.");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Migration failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate();
|
||||||
280
scripts/seed-from-npm.ts
Normal file
280
scripts/seed-from-npm.ts
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* Seed PostgreSQL from the existing @tip/core npm package data.
|
||||||
|
* Imports: 159 transceivers, 42 standards, 12 competitors, 11 breakouts.
|
||||||
|
*/
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
config();
|
||||||
|
|
||||||
|
// Dynamic import of core package (ESM compat)
|
||||||
|
async function loadCoreData() {
|
||||||
|
const corePath = join(__dirname, "..", "packages", "core", "src");
|
||||||
|
|
||||||
|
// We need to use tsx to run this, so we can import .ts files directly
|
||||||
|
const { transceivers } = await import(join(corePath, "database"));
|
||||||
|
const { standards } = await import(join(corePath, "standards"));
|
||||||
|
const { competitors } = await import(join(corePath, "market"));
|
||||||
|
const { breakouts } = await import(join(corePath, "breakouts"));
|
||||||
|
|
||||||
|
return { transceivers, standards, competitors, breakouts };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
host: process.env.POSTGRES_HOST || "localhost",
|
||||||
|
port: parseInt(process.env.POSTGRES_PORT || "5432"),
|
||||||
|
database: process.env.POSTGRES_DB || "transceiver_db",
|
||||||
|
user: process.env.POSTGRES_USER || "tip",
|
||||||
|
password: process.env.POSTGRES_PASSWORD || "tip_dev_2026",
|
||||||
|
});
|
||||||
|
|
||||||
|
function slugify(text: string): string {
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-|-$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedVendors(client: any, competitors: readonly any[]): Promise<Map<string, string>> {
|
||||||
|
console.log("\nSeeding vendors...");
|
||||||
|
const vendorIdMap = new Map<string, string>();
|
||||||
|
|
||||||
|
// Insert Flexoptix first as primary vendor
|
||||||
|
const flexResult = await client.query(
|
||||||
|
`INSERT INTO vendors (name, slug, type, headquarters, country, website, shop_url, is_competitor, market_position, specialties, strengths, weaknesses)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
|
ON CONFLICT (name) DO UPDATE SET updated_at = NOW()
|
||||||
|
RETURNING id`,
|
||||||
|
[
|
||||||
|
"FLEXOPTIX",
|
||||||
|
"flexoptix",
|
||||||
|
"compatible",
|
||||||
|
"Mainz, Germany",
|
||||||
|
"Germany",
|
||||||
|
"https://www.flexoptix.net",
|
||||||
|
"https://www.flexoptix.net/en/",
|
||||||
|
false,
|
||||||
|
"Premium compatible optics with FlexBox programmer, 300+ vendor support",
|
||||||
|
["compatible optics", "FlexBox", "all speeds", "premium quality"],
|
||||||
|
["FlexBox programmer", "300+ vendor support", "lifetime warranty", "German engineering"],
|
||||||
|
[],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
vendorIdMap.set("FLEXOPTIX", flexResult.rows[0].id);
|
||||||
|
console.log(" Inserted: FLEXOPTIX (primary)");
|
||||||
|
|
||||||
|
for (const comp of competitors) {
|
||||||
|
if (comp.name === "FLEXOPTIX") continue; // Already inserted
|
||||||
|
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
OEM: "oem",
|
||||||
|
Whitebox: "compatible",
|
||||||
|
Manufacturer: "manufacturer",
|
||||||
|
Distributor: "distributor",
|
||||||
|
Compatible: "compatible",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await client.query(
|
||||||
|
`INSERT INTO vendors (name, slug, type, headquarters, is_competitor, market_position, specialties, strengths, weaknesses)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
ON CONFLICT (name) DO UPDATE SET updated_at = NOW()
|
||||||
|
RETURNING id`,
|
||||||
|
[
|
||||||
|
comp.name,
|
||||||
|
slugify(comp.name),
|
||||||
|
typeMap[comp.type] || "oem",
|
||||||
|
comp.headquarters,
|
||||||
|
true,
|
||||||
|
comp.marketPosition,
|
||||||
|
[...comp.formFactorsOffered, ...comp.speedTiersOffered],
|
||||||
|
comp.strengths,
|
||||||
|
comp.weaknesses,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
vendorIdMap.set(comp.name, result.rows[0].id);
|
||||||
|
console.log(` Inserted: ${comp.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Total vendors: ${vendorIdMap.size}`);
|
||||||
|
return vendorIdMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedStandards(client: any, standards: readonly any[]): Promise<Map<string, string>> {
|
||||||
|
console.log("\nSeeding standards...");
|
||||||
|
const standardIdMap = new Map<string, string>();
|
||||||
|
|
||||||
|
for (const std of standards) {
|
||||||
|
const body = std.ieeeReference?.startsWith("IEEE")
|
||||||
|
? "IEEE"
|
||||||
|
: std.standard.includes("ZR")
|
||||||
|
? "OIF"
|
||||||
|
: std.ieeeReference
|
||||||
|
? "IEEE"
|
||||||
|
: "de_facto";
|
||||||
|
|
||||||
|
const result = await client.query(
|
||||||
|
`INSERT INTO standards (name, ieee_reference, body, speed, speed_gbps, lanes, lane_rate, modulation, fiber_type, wavelength, max_reach_meters, max_reach_label, connector, fec_required, form_factors, year_ratified, notes)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||||
|
ON CONFLICT (name) DO UPDATE SET notes = EXCLUDED.notes
|
||||||
|
RETURNING id`,
|
||||||
|
[
|
||||||
|
std.standard,
|
||||||
|
std.ieeeReference || null,
|
||||||
|
body,
|
||||||
|
std.speed,
|
||||||
|
parseFloat(std.speed) || null,
|
||||||
|
std.lanes,
|
||||||
|
std.laneRate,
|
||||||
|
std.modulation,
|
||||||
|
std.fiberType,
|
||||||
|
std.wavelength,
|
||||||
|
std.maxReachMeters,
|
||||||
|
std.maxReachLabel,
|
||||||
|
std.connector,
|
||||||
|
std.fecRequired,
|
||||||
|
std.formFactors,
|
||||||
|
std.yearRatified,
|
||||||
|
std.notes,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
standardIdMap.set(std.standard, result.rows[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Total standards: ${standardIdMap.size}`);
|
||||||
|
return standardIdMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedTransceivers(
|
||||||
|
client: any,
|
||||||
|
transceivers: readonly any[],
|
||||||
|
standardIdMap: Map<string, string>
|
||||||
|
): Promise<void> {
|
||||||
|
console.log("\nSeeding transceivers...");
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const t of transceivers) {
|
||||||
|
const standardId = standardIdMap.get(t.standard) || null;
|
||||||
|
|
||||||
|
// Detect WDM type
|
||||||
|
let wdmType = null;
|
||||||
|
if (t.category === "CWDM" || t.wavelengths?.includes("CWDM")) wdmType = "CWDM";
|
||||||
|
if (t.category === "DWDM" || t.wavelengths?.includes("DWDM") || t.standard?.includes("DWDM"))
|
||||||
|
wdmType = "DWDM";
|
||||||
|
|
||||||
|
// Detect coherent
|
||||||
|
const coherent =
|
||||||
|
t.category === "Coherent" ||
|
||||||
|
t.standard?.includes("ZR") ||
|
||||||
|
t.modulation?.includes("DP-") ||
|
||||||
|
false;
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO transceivers (
|
||||||
|
slug, standard_name, standard_id, ieee_reference, form_factor,
|
||||||
|
speed, speed_gbps, lanes, lane_rate, modulation,
|
||||||
|
reach_meters, reach_label, fiber_type, wavelengths, connector,
|
||||||
|
power_consumption_w, temp_range, category, price_tier, use_case,
|
||||||
|
vendor_compat, tags, generation, market_status, year_introduced,
|
||||||
|
breakout_capable, breakout_to, wdm_type, coherent
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||||
|
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
|
||||||
|
$21, $22, $23, $24, $25, $26, $27, $28, $29
|
||||||
|
) ON CONFLICT (slug) DO UPDATE SET updated_at = NOW()`,
|
||||||
|
[
|
||||||
|
t.id,
|
||||||
|
t.standard,
|
||||||
|
standardId,
|
||||||
|
t.ieeeReference || null,
|
||||||
|
t.formFactor,
|
||||||
|
t.speed,
|
||||||
|
t.speedGbps,
|
||||||
|
t.lanes || null,
|
||||||
|
t.laneRate || null,
|
||||||
|
t.modulation || null,
|
||||||
|
t.reachMeters,
|
||||||
|
t.reachLabel,
|
||||||
|
t.fiberType,
|
||||||
|
t.wavelengths,
|
||||||
|
t.connector,
|
||||||
|
t.powerConsumptionW,
|
||||||
|
t.tempRange,
|
||||||
|
t.category,
|
||||||
|
t.priceTier,
|
||||||
|
t.useCase,
|
||||||
|
JSON.stringify(t.vendors),
|
||||||
|
t.tags,
|
||||||
|
t.generation || null,
|
||||||
|
t.marketStatus || "Mainstream",
|
||||||
|
t.yearIntroduced || null,
|
||||||
|
t.breakoutCapable || false,
|
||||||
|
t.breakoutTo || null,
|
||||||
|
wdmType,
|
||||||
|
coherent,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Total transceivers: ${count}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedBreakouts(client: any, breakouts: readonly any[]): Promise<void> {
|
||||||
|
console.log("\nSeeding breakouts...");
|
||||||
|
|
||||||
|
for (const b of breakouts) {
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO breakouts (slug, from_standard, to_standard, form_factor, description, cable_type, max_length, speed_per_lane)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
ON CONFLICT (slug) DO UPDATE SET from_standard = $2`,
|
||||||
|
[b.id, b.from, b.to, b.formFactor, b.description, b.cableType, b.maxLength, b.speedPerLane]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Total breakouts: ${breakouts.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
console.log("=== TIP Seed: Importing from @tip/core ===\n");
|
||||||
|
|
||||||
|
const { transceivers, standards, competitors, breakouts } = await loadCoreData();
|
||||||
|
|
||||||
|
console.log(`Source data: ${transceivers.length} transceivers, ${standards.length} standards, ${competitors.length} competitors, ${breakouts.length} breakouts`);
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
|
const vendorIdMap = await seedVendors(client, competitors);
|
||||||
|
const standardIdMap = await seedStandards(client, standards);
|
||||||
|
await seedTransceivers(client, transceivers, standardIdMap);
|
||||||
|
await seedBreakouts(client, breakouts);
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
console.log("\n=== Seed completed successfully ===");
|
||||||
|
|
||||||
|
// Print summary
|
||||||
|
const counts = await client.query(`
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM vendors) as vendors,
|
||||||
|
(SELECT COUNT(*) FROM standards) as standards,
|
||||||
|
(SELECT COUNT(*) FROM transceivers) as transceivers,
|
||||||
|
(SELECT COUNT(*) FROM breakouts) as breakouts
|
||||||
|
`);
|
||||||
|
console.log("\nDatabase summary:");
|
||||||
|
console.log(` Vendors: ${counts.rows[0].vendors}`);
|
||||||
|
console.log(` Standards: ${counts.rows[0].standards}`);
|
||||||
|
console.log(` Transceivers: ${counts.rows[0].transceivers}`);
|
||||||
|
console.log(` Breakouts: ${counts.rows[0].breakouts}`);
|
||||||
|
} catch (err) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
console.error("\nSeed failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
73
scripts/setup-erik.sh
Executable file
73
scripts/setup-erik.sh
Executable file
@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# TIP: Setup script for Erik server (.82)
|
||||||
|
# Run as root or with sudo
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "=== TIP: Erik Server Setup ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. PostgreSQL 17 + TimescaleDB
|
||||||
|
echo "--- Installing PostgreSQL 17 + TimescaleDB ---"
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y gnupg2 lsb-release
|
||||||
|
|
||||||
|
# TimescaleDB repo (includes PostgreSQL 17)
|
||||||
|
echo "deb https://packagecloud.io/timescale/timescaledb/debian/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/timescaledb.list
|
||||||
|
curl -fsSL https://packagecloud.io/timescale/timescaledb/gpgkey | gpg --dearmor -o /etc/apt/trusted.gpg.d/timescaledb.gpg
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y timescaledb-2-postgresql-17
|
||||||
|
|
||||||
|
# Enable TimescaleDB
|
||||||
|
timescaledb-tune --quiet --yes
|
||||||
|
|
||||||
|
# pgvector
|
||||||
|
apt-get install -y postgresql-17-pgvector
|
||||||
|
|
||||||
|
systemctl restart postgresql
|
||||||
|
systemctl enable postgresql
|
||||||
|
|
||||||
|
# Create DB and user
|
||||||
|
sudo -u postgres psql <<SQL
|
||||||
|
CREATE USER tip WITH PASSWORD '${POSTGRES_PASSWORD:-***REDACTED***}';
|
||||||
|
CREATE DATABASE transceiver_db OWNER tip;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE transceiver_db TO tip;
|
||||||
|
\c transceiver_db
|
||||||
|
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
SQL
|
||||||
|
|
||||||
|
echo "PostgreSQL 17 + TimescaleDB + pgvector ready."
|
||||||
|
|
||||||
|
# 2. Qdrant
|
||||||
|
echo ""
|
||||||
|
echo "--- Installing Qdrant (Docker) ---"
|
||||||
|
docker pull qdrant/qdrant:latest
|
||||||
|
docker run -d \
|
||||||
|
--name tip-qdrant \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 6333:6333 \
|
||||||
|
-p 6334:6334 \
|
||||||
|
-v /opt/tip/qdrant:/qdrant/storage \
|
||||||
|
qdrant/qdrant:latest
|
||||||
|
|
||||||
|
echo "Qdrant running on port 6333."
|
||||||
|
|
||||||
|
# 3. App directory
|
||||||
|
echo ""
|
||||||
|
echo "--- Setting up app directory ---"
|
||||||
|
mkdir -p /opt/tip
|
||||||
|
cd /opt/tip
|
||||||
|
|
||||||
|
echo "=== Setup complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Clone repo: git clone <repo> /opt/tip/app"
|
||||||
|
echo " 2. cd /opt/tip/app && npm install"
|
||||||
|
echo " 3. cp .env.example .env && edit .env"
|
||||||
|
echo " 4. npm run migrate"
|
||||||
|
echo " 5. npm run seed"
|
||||||
|
echo " 6. pm2 start ecosystem.config.js"
|
||||||
7
sql/001-extensions.sql
Normal file
7
sql/001-extensions.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-- TIP: Transceiver Intelligence Platform
|
||||||
|
-- Migration 001: Extensions
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector; -- pgvector
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm; -- Trigram similarity for fuzzy search
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID generation
|
||||||
469
sql/002-core-tables.sql
Normal file
469
sql/002-core-tables.sql
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
-- TIP: Transceiver Intelligence Platform
|
||||||
|
-- Migration 002: Core Tables
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- VENDORS (Hersteller, Distributoren, Reseller, OEMs)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS vendors (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
slug TEXT NOT NULL UNIQUE,
|
||||||
|
type TEXT NOT NULL CHECK (type IN ('manufacturer','distributor','oem','reseller','compatible')),
|
||||||
|
headquarters TEXT,
|
||||||
|
country TEXT,
|
||||||
|
website TEXT,
|
||||||
|
shop_url TEXT,
|
||||||
|
api_available BOOLEAN DEFAULT FALSE,
|
||||||
|
api_endpoint TEXT,
|
||||||
|
logo_r2_key TEXT,
|
||||||
|
founded_year INTEGER,
|
||||||
|
revenue_usd BIGINT,
|
||||||
|
employee_count INTEGER,
|
||||||
|
market_position TEXT,
|
||||||
|
specialties TEXT[] DEFAULT '{}',
|
||||||
|
scrape_config JSONB DEFAULT '{}',
|
||||||
|
last_scraped TIMESTAMPTZ,
|
||||||
|
is_competitor BOOLEAN DEFAULT FALSE,
|
||||||
|
is_factory BOOLEAN DEFAULT FALSE,
|
||||||
|
factory_locations TEXT[] DEFAULT '{}',
|
||||||
|
certifications TEXT[] DEFAULT '{}',
|
||||||
|
strengths TEXT[] DEFAULT '{}',
|
||||||
|
weaknesses TEXT[] DEFAULT '{}',
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- STANDARDS (IEEE, OIF, MSA)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS standards (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
ieee_reference TEXT,
|
||||||
|
body TEXT CHECK (body IN ('IEEE','OIF','MSA','de_facto','proprietary')),
|
||||||
|
speed TEXT,
|
||||||
|
speed_gbps NUMERIC,
|
||||||
|
lanes INTEGER,
|
||||||
|
lane_rate TEXT,
|
||||||
|
lane_rate_gbps NUMERIC,
|
||||||
|
modulation TEXT,
|
||||||
|
fiber_type TEXT,
|
||||||
|
wavelength TEXT,
|
||||||
|
max_reach_meters INTEGER,
|
||||||
|
max_reach_label TEXT,
|
||||||
|
connector TEXT,
|
||||||
|
fec_required BOOLEAN DEFAULT FALSE,
|
||||||
|
form_factors TEXT[] DEFAULT '{}',
|
||||||
|
year_draft INTEGER,
|
||||||
|
year_ratified INTEGER,
|
||||||
|
year_revised INTEGER,
|
||||||
|
status TEXT DEFAULT 'ratified' CHECK (status IN ('draft','ratified','revised','superseded')),
|
||||||
|
superseded_by TEXT,
|
||||||
|
member_count INTEGER,
|
||||||
|
notes TEXT,
|
||||||
|
url TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- TRANSCEIVERS
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS transceivers (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
slug TEXT NOT NULL UNIQUE,
|
||||||
|
vendor_id UUID REFERENCES vendors(id),
|
||||||
|
part_number TEXT,
|
||||||
|
standard_name TEXT,
|
||||||
|
standard_id UUID REFERENCES standards(id),
|
||||||
|
ieee_reference TEXT,
|
||||||
|
form_factor TEXT NOT NULL,
|
||||||
|
speed TEXT NOT NULL,
|
||||||
|
speed_gbps NUMERIC NOT NULL,
|
||||||
|
lanes INTEGER,
|
||||||
|
lane_rate TEXT,
|
||||||
|
lane_rate_gbps NUMERIC,
|
||||||
|
modulation TEXT,
|
||||||
|
reach_meters INTEGER NOT NULL,
|
||||||
|
reach_label TEXT,
|
||||||
|
fiber_type TEXT,
|
||||||
|
wavelengths TEXT,
|
||||||
|
connector TEXT,
|
||||||
|
power_consumption_w NUMERIC,
|
||||||
|
temp_range TEXT DEFAULT 'COM' CHECK (temp_range IN ('COM','IND')),
|
||||||
|
category TEXT,
|
||||||
|
dom_support BOOLEAN DEFAULT TRUE,
|
||||||
|
digital_diagnostics TEXT,
|
||||||
|
|
||||||
|
-- CWDM/DWDM
|
||||||
|
wdm_type TEXT CHECK (wdm_type IN ('CWDM','DWDM',NULL)),
|
||||||
|
channel_count INTEGER,
|
||||||
|
channel_spacing_ghz NUMERIC,
|
||||||
|
tunable BOOLEAN DEFAULT FALSE,
|
||||||
|
itu_grid TEXT,
|
||||||
|
|
||||||
|
-- Coherent
|
||||||
|
coherent BOOLEAN DEFAULT FALSE,
|
||||||
|
baud_rate_gbaud NUMERIC,
|
||||||
|
fec_type TEXT,
|
||||||
|
dsp_vendor TEXT,
|
||||||
|
|
||||||
|
-- Lifecycle
|
||||||
|
year_introduced INTEGER,
|
||||||
|
year_mainstream INTEGER,
|
||||||
|
year_peak INTEGER,
|
||||||
|
year_decline INTEGER,
|
||||||
|
market_status TEXT DEFAULT 'Mainstream' CHECK (market_status IN ('Mainstream','Growth','Emerging','Legacy','EOL')),
|
||||||
|
hype_cycle_phase TEXT,
|
||||||
|
generation TEXT,
|
||||||
|
|
||||||
|
-- Pricing
|
||||||
|
price_tier TEXT CHECK (price_tier IN ('Budget','Standard','Premium')),
|
||||||
|
msrp_usd NUMERIC,
|
||||||
|
street_price_usd NUMERIC,
|
||||||
|
|
||||||
|
-- Technical
|
||||||
|
optical_budget_db NUMERIC,
|
||||||
|
tx_power_min_dbm NUMERIC,
|
||||||
|
tx_power_max_dbm NUMERIC,
|
||||||
|
rx_sensitivity_dbm NUMERIC,
|
||||||
|
|
||||||
|
-- Breakout
|
||||||
|
breakout_capable BOOLEAN DEFAULT FALSE,
|
||||||
|
breakout_to TEXT,
|
||||||
|
|
||||||
|
-- Storage
|
||||||
|
datasheet_r2_key TEXT,
|
||||||
|
image_r2_key TEXT,
|
||||||
|
|
||||||
|
-- Meta
|
||||||
|
use_case TEXT,
|
||||||
|
tags TEXT[] DEFAULT '{}',
|
||||||
|
vendor_compat JSONB DEFAULT '[]',
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
-- Search vector (auto-populated by trigger)
|
||||||
|
search_vector TSVECTOR,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- SWITCHES
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS switches (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
vendor_id UUID REFERENCES vendors(id),
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
series TEXT,
|
||||||
|
category TEXT CHECK (category IN ('DataCenter','Campus','Edge','Core','SP','Industrial')),
|
||||||
|
layer TEXT CHECK (layer IN ('L2','L3','L2/L3')),
|
||||||
|
managed BOOLEAN DEFAULT TRUE,
|
||||||
|
|
||||||
|
-- Ports
|
||||||
|
ports_config JSONB DEFAULT '{}',
|
||||||
|
total_ports INTEGER,
|
||||||
|
uplink_speed_gbps NUMERIC,
|
||||||
|
max_speed_gbps NUMERIC,
|
||||||
|
|
||||||
|
-- Performance
|
||||||
|
switching_capacity_tbps NUMERIC,
|
||||||
|
forwarding_rate_mpps NUMERIC,
|
||||||
|
latency_ns NUMERIC,
|
||||||
|
buffer_mb NUMERIC,
|
||||||
|
|
||||||
|
-- ASIC
|
||||||
|
asic_vendor TEXT,
|
||||||
|
asic_model TEXT,
|
||||||
|
asic_generation TEXT,
|
||||||
|
|
||||||
|
-- Features
|
||||||
|
poe_support TEXT DEFAULT 'None',
|
||||||
|
stacking_support BOOLEAN DEFAULT FALSE,
|
||||||
|
vxlan_support BOOLEAN DEFAULT FALSE,
|
||||||
|
evpn_support BOOLEAN DEFAULT FALSE,
|
||||||
|
bgp_support BOOLEAN DEFAULT FALSE,
|
||||||
|
mpls_support BOOLEAN DEFAULT FALSE,
|
||||||
|
openconfig_support BOOLEAN DEFAULT FALSE,
|
||||||
|
sonic_compatible BOOLEAN DEFAULT FALSE,
|
||||||
|
macsec_support BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Lifecycle
|
||||||
|
release_date DATE,
|
||||||
|
eos_date DATE,
|
||||||
|
eol_date DATE,
|
||||||
|
last_support_date DATE,
|
||||||
|
lifecycle_status TEXT DEFAULT 'Active' CHECK (lifecycle_status IN ('Active','EoS_Announced','EoL','Legacy')),
|
||||||
|
successor_model TEXT,
|
||||||
|
|
||||||
|
-- Physical
|
||||||
|
rack_units NUMERIC,
|
||||||
|
max_power_w NUMERIC,
|
||||||
|
typical_power_w NUMERIC,
|
||||||
|
weight_kg NUMERIC,
|
||||||
|
airflow TEXT,
|
||||||
|
|
||||||
|
-- Pricing
|
||||||
|
msrp_usd NUMERIC,
|
||||||
|
street_price_usd NUMERIC,
|
||||||
|
|
||||||
|
-- Documentation
|
||||||
|
manual_r2_key TEXT,
|
||||||
|
datasheet_r2_key TEXT,
|
||||||
|
config_guide_r2_key TEXT,
|
||||||
|
compatibility_list_url TEXT,
|
||||||
|
|
||||||
|
-- Meta
|
||||||
|
tags TEXT[] DEFAULT '{}',
|
||||||
|
search_vector TSVECTOR,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
UNIQUE(vendor_id, model)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- COMPATIBILITY (Switch <-> Transceiver)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS compatibility (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
switch_id UUID REFERENCES switches(id) ON DELETE CASCADE,
|
||||||
|
transceiver_id UUID REFERENCES transceivers(id) ON DELETE CASCADE,
|
||||||
|
verified_by TEXT,
|
||||||
|
verification_date DATE,
|
||||||
|
verification_method TEXT CHECK (verification_method IN ('tested','vendor_matrix','datasheet','community')),
|
||||||
|
status TEXT DEFAULT 'compatible' CHECK (status IN ('compatible','incompatible','partial','unknown')),
|
||||||
|
notes TEXT,
|
||||||
|
firmware_min TEXT,
|
||||||
|
known_issues TEXT,
|
||||||
|
source_url TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
UNIQUE(switch_id, transceiver_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- BREAKOUTS
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS breakouts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
slug TEXT NOT NULL UNIQUE,
|
||||||
|
from_standard TEXT NOT NULL,
|
||||||
|
to_standard TEXT NOT NULL,
|
||||||
|
form_factor TEXT,
|
||||||
|
description TEXT,
|
||||||
|
cable_type TEXT CHECK (cable_type IN ('Passive','Active')),
|
||||||
|
max_length TEXT,
|
||||||
|
speed_per_lane TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- TEMPLATES (FlexBox Coding + Switch Config)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS templates (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
type TEXT NOT NULL CHECK (type IN ('flexbox_coding','switch_config')),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
switch_vendor TEXT,
|
||||||
|
switch_series TEXT,
|
||||||
|
transceiver_type TEXT,
|
||||||
|
speed_gbps NUMERIC,
|
||||||
|
technology TEXT,
|
||||||
|
template_content TEXT NOT NULL,
|
||||||
|
variables JSONB DEFAULT '{}',
|
||||||
|
tags TEXT[] DEFAULT '{}',
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- DOCUMENTS (PDFs in R2)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS documents (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
entity_type TEXT NOT NULL CHECK (entity_type IN ('transceiver','switch','vendor','standard')),
|
||||||
|
entity_id UUID,
|
||||||
|
doc_type TEXT NOT NULL CHECK (doc_type IN ('manual','datasheet','config_guide','compatibility_list','faq','whitepaper')),
|
||||||
|
title TEXT,
|
||||||
|
filename TEXT,
|
||||||
|
r2_key TEXT NOT NULL,
|
||||||
|
source_url TEXT,
|
||||||
|
file_size_bytes BIGINT,
|
||||||
|
page_count INTEGER,
|
||||||
|
ocr_status TEXT DEFAULT 'pending' CHECK (ocr_status IN ('pending','processing','completed','failed')),
|
||||||
|
ocr_text TEXT,
|
||||||
|
language TEXT DEFAULT 'en',
|
||||||
|
content_hash TEXT,
|
||||||
|
last_checked TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- KNOWLEDGE BASE (FAQs, Troubleshooting)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS knowledge_base (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
category TEXT NOT NULL CHECK (category IN ('troubleshooting','faq','best_practice','known_issue','compatibility_tip')),
|
||||||
|
subcategory TEXT,
|
||||||
|
question TEXT NOT NULL,
|
||||||
|
answer TEXT NOT NULL,
|
||||||
|
source_vendor TEXT,
|
||||||
|
source_url TEXT,
|
||||||
|
applies_to_form_factors TEXT[] DEFAULT '{}',
|
||||||
|
applies_to_speeds TEXT[] DEFAULT '{}',
|
||||||
|
applies_to_vendors TEXT[] DEFAULT '{}',
|
||||||
|
severity TEXT CHECK (severity IN ('critical','high','medium','low','info')),
|
||||||
|
resolution_steps JSONB,
|
||||||
|
last_verified TIMESTAMPTZ,
|
||||||
|
helpful_count INTEGER DEFAULT 0,
|
||||||
|
tags TEXT[] DEFAULT '{}',
|
||||||
|
search_vector TSVECTOR,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- FACTORIES
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS factories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
vendor_id UUID REFERENCES vendors(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
country TEXT NOT NULL,
|
||||||
|
city TEXT,
|
||||||
|
factory_type TEXT CHECK (factory_type IN ('manufacturing','assembly','r_and_d','headquarters')),
|
||||||
|
products TEXT[] DEFAULT '{}',
|
||||||
|
capacity_units_month INTEGER,
|
||||||
|
employee_count INTEGER,
|
||||||
|
certifications TEXT[] DEFAULT '{}',
|
||||||
|
expansion_planned BOOLEAN DEFAULT FALSE,
|
||||||
|
expansion_details TEXT,
|
||||||
|
source_url TEXT,
|
||||||
|
last_verified TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- NEWS ARTICLES
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS news_articles (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
source TEXT NOT NULL,
|
||||||
|
source_url TEXT NOT NULL UNIQUE,
|
||||||
|
published_at TIMESTAMPTZ,
|
||||||
|
author TEXT,
|
||||||
|
summary TEXT,
|
||||||
|
full_text TEXT,
|
||||||
|
category TEXT CHECK (category IN ('product_launch','market_report','standard','m_and_a','factory','event')),
|
||||||
|
event TEXT,
|
||||||
|
mentioned_vendors TEXT[] DEFAULT '{}',
|
||||||
|
mentioned_products TEXT[] DEFAULT '{}',
|
||||||
|
mentioned_standards TEXT[] DEFAULT '{}',
|
||||||
|
sentiment_score NUMERIC,
|
||||||
|
relevance_score NUMERIC,
|
||||||
|
content_hash TEXT,
|
||||||
|
tags TEXT[] DEFAULT '{}',
|
||||||
|
search_vector TSVECTOR,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- BLOG DRAFTS
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS blog_drafts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
topic TEXT CHECK (topic IN ('hype_cycle','price_trend','new_product','comparison','tutorial')),
|
||||||
|
target_audience TEXT CHECK (target_audience IN ('sales','technical','customer','seo')),
|
||||||
|
outline JSONB,
|
||||||
|
draft_content TEXT,
|
||||||
|
data_sources JSONB,
|
||||||
|
status TEXT DEFAULT 'draft' CHECK (status IN ('draft','review','approved','published')),
|
||||||
|
generated_by TEXT,
|
||||||
|
word_count INTEGER,
|
||||||
|
seo_keywords TEXT[] DEFAULT '{}',
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- TRIGGERS: Auto-update search_vector
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Transceiver search vector
|
||||||
|
CREATE OR REPLACE FUNCTION transceivers_search_vector_update() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.standard_name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.form_factor, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.speed, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.use_case, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.category, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.wavelengths, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.modulation, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.generation, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(array_to_string(NEW.tags, ' '), '')), 'D');
|
||||||
|
NEW.updated_at := NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER transceivers_search_update
|
||||||
|
BEFORE INSERT OR UPDATE ON transceivers
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION transceivers_search_vector_update();
|
||||||
|
|
||||||
|
-- Switch search vector
|
||||||
|
CREATE OR REPLACE FUNCTION switches_search_vector_update() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.model, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.series, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.category, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.asic_vendor, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.asic_model, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(array_to_string(NEW.tags, ' '), '')), 'D');
|
||||||
|
NEW.updated_at := NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER switches_search_update
|
||||||
|
BEFORE INSERT OR UPDATE ON switches
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION switches_search_vector_update();
|
||||||
|
|
||||||
|
-- Knowledge base search vector
|
||||||
|
CREATE OR REPLACE FUNCTION kb_search_vector_update() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.question, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.answer, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.category, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(array_to_string(NEW.tags, ' '), '')), 'D');
|
||||||
|
NEW.updated_at := NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER kb_search_update
|
||||||
|
BEFORE INSERT OR UPDATE ON knowledge_base
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION kb_search_vector_update();
|
||||||
|
|
||||||
|
-- News search vector
|
||||||
|
CREATE OR REPLACE FUNCTION news_search_vector_update() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.summary, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(NEW.source, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('english', COALESCE(array_to_string(NEW.tags, ' '), '')), 'D');
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER news_search_update
|
||||||
|
BEFORE INSERT OR UPDATE ON news_articles
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION news_search_vector_update();
|
||||||
122
sql/003-timeseries.sql
Normal file
122
sql/003-timeseries.sql
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
-- TIP: Transceiver Intelligence Platform
|
||||||
|
-- Migration 003: TimescaleDB Hypertables
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- PRICE OBSERVATIONS (Real-time competitor pricing)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS price_observations (
|
||||||
|
time TIMESTAMPTZ NOT NULL,
|
||||||
|
transceiver_id UUID NOT NULL,
|
||||||
|
source_vendor_id UUID NOT NULL,
|
||||||
|
price NUMERIC NOT NULL,
|
||||||
|
currency TEXT DEFAULT 'USD',
|
||||||
|
stock_level TEXT CHECK (stock_level IN ('in_stock','low_stock','out_of_stock','on_request','discontinued')),
|
||||||
|
quantity_available INTEGER,
|
||||||
|
lead_time_days INTEGER,
|
||||||
|
min_order_qty INTEGER,
|
||||||
|
url TEXT,
|
||||||
|
content_hash TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT create_hypertable('price_observations', by_range('time'),
|
||||||
|
if_not_exists => TRUE);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- STOCK OBSERVATIONS (Separate stock tracking)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS stock_observations (
|
||||||
|
time TIMESTAMPTZ NOT NULL,
|
||||||
|
transceiver_id UUID NOT NULL,
|
||||||
|
source_vendor_id UUID NOT NULL,
|
||||||
|
in_stock BOOLEAN NOT NULL,
|
||||||
|
quantity_available INTEGER,
|
||||||
|
lead_time_days INTEGER,
|
||||||
|
content_hash TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT create_hypertable('stock_observations', by_range('time'),
|
||||||
|
if_not_exists => TRUE);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- MARKET METRICS (Hype Cycle input data)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS market_metrics (
|
||||||
|
time TIMESTAMPTZ NOT NULL,
|
||||||
|
technology TEXT NOT NULL,
|
||||||
|
metric_type TEXT NOT NULL CHECK (metric_type IN (
|
||||||
|
'vendor_count','shipment_share','asp_decline_rate',
|
||||||
|
'media_hype_index','patent_filings','port_shipments',
|
||||||
|
'revenue_usd','asp_usd'
|
||||||
|
)),
|
||||||
|
value NUMERIC NOT NULL,
|
||||||
|
source TEXT,
|
||||||
|
notes TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT create_hypertable('market_metrics', by_range('time'),
|
||||||
|
if_not_exists => TRUE);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- CONTINUOUS AGGREGATES
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Daily price aggregates
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS price_daily
|
||||||
|
WITH (timescaledb.continuous) AS
|
||||||
|
SELECT
|
||||||
|
time_bucket('1 day', time) AS bucket,
|
||||||
|
transceiver_id,
|
||||||
|
source_vendor_id,
|
||||||
|
AVG(price) AS avg_price,
|
||||||
|
MIN(price) AS min_price,
|
||||||
|
MAX(price) AS max_price,
|
||||||
|
last(stock_level, time) AS latest_stock,
|
||||||
|
COUNT(*) AS observation_count
|
||||||
|
FROM price_observations
|
||||||
|
GROUP BY bucket, transceiver_id, source_vendor_id
|
||||||
|
WITH NO DATA;
|
||||||
|
|
||||||
|
-- Weekly price aggregates
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS price_weekly
|
||||||
|
WITH (timescaledb.continuous) AS
|
||||||
|
SELECT
|
||||||
|
time_bucket('7 days', time) AS bucket,
|
||||||
|
transceiver_id,
|
||||||
|
source_vendor_id,
|
||||||
|
AVG(price) AS avg_price,
|
||||||
|
MIN(price) AS min_price,
|
||||||
|
MAX(price) AS max_price,
|
||||||
|
COUNT(*) AS observation_count
|
||||||
|
FROM price_observations
|
||||||
|
GROUP BY bucket, transceiver_id, source_vendor_id
|
||||||
|
WITH NO DATA;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- RETENTION POLICIES
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Raw price data: keep 90 days
|
||||||
|
SELECT add_retention_policy('price_observations', INTERVAL '90 days',
|
||||||
|
if_not_exists => TRUE);
|
||||||
|
|
||||||
|
-- Raw stock data: keep 90 days
|
||||||
|
SELECT add_retention_policy('stock_observations', INTERVAL '90 days',
|
||||||
|
if_not_exists => TRUE);
|
||||||
|
|
||||||
|
-- Market metrics: keep 10 years (small volume)
|
||||||
|
-- No retention policy needed
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- REFRESH POLICIES for continuous aggregates
|
||||||
|
-- ============================================================
|
||||||
|
SELECT add_continuous_aggregate_policy('price_daily',
|
||||||
|
start_offset => INTERVAL '3 days',
|
||||||
|
end_offset => INTERVAL '1 hour',
|
||||||
|
schedule_interval => INTERVAL '1 hour',
|
||||||
|
if_not_exists => TRUE);
|
||||||
|
|
||||||
|
SELECT add_continuous_aggregate_policy('price_weekly',
|
||||||
|
start_offset => INTERVAL '30 days',
|
||||||
|
end_offset => INTERVAL '7 days',
|
||||||
|
schedule_interval => INTERVAL '1 day',
|
||||||
|
if_not_exists => TRUE);
|
||||||
75
sql/004-indexes.sql
Normal file
75
sql/004-indexes.sql
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
-- TIP: Transceiver Intelligence Platform
|
||||||
|
-- Migration 004: Indexes
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- FULL-TEXT SEARCH INDEXES (GIN on tsvector)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_search ON transceivers USING GIN(search_vector);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_switches_search ON switches USING GIN(search_vector);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kb_search ON knowledge_base USING GIN(search_vector);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_news_search ON news_articles USING GIN(search_vector);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- ARRAY INDEXES (GIN on text[])
|
||||||
|
-- ============================================================
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_tags ON transceivers USING GIN(tags);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_switches_tags ON switches USING GIN(tags);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendors_specialties ON vendors USING GIN(specialties);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_standards_form_factors ON standards USING GIN(form_factors);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kb_form_factors ON knowledge_base USING GIN(applies_to_form_factors);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kb_speeds ON knowledge_base USING GIN(applies_to_speeds);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_news_vendors ON news_articles USING GIN(mentioned_vendors);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- JSONB INDEXES
|
||||||
|
-- ============================================================
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_vendor_compat ON transceivers USING GIN(vendor_compat);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_switches_ports ON switches USING GIN(ports_config);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- B-TREE INDEXES (lookups, sorting)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_form_factor ON transceivers(form_factor);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_speed_gbps ON transceivers(speed_gbps);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_category ON transceivers(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_market_status ON transceivers(market_status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_reach ON transceivers(reach_meters);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_fiber_type ON transceivers(fiber_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_wdm_type ON transceivers(wdm_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transceivers_coherent ON transceivers(coherent);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_switches_vendor ON switches(vendor_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_switches_category ON switches(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_switches_lifecycle ON switches(lifecycle_status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_switches_max_speed ON switches(max_speed_gbps);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_compatibility_switch ON compatibility(switch_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_compatibility_transceiver ON compatibility(transceiver_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_documents_entity ON documents(entity_type, entity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_documents_ocr_status ON documents(ocr_status);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kb_category ON knowledge_base(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kb_severity ON knowledge_base(severity);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_news_published ON news_articles(published_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_news_category ON news_articles(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_news_event ON news_articles(event);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendors_type ON vendors(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendors_competitor ON vendors(is_competitor) WHERE is_competitor = TRUE;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- TRIGRAM INDEXES (fuzzy search on names)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vendors_name_trgm ON vendors USING GIN(name gin_trgm_ops);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_switches_model_trgm ON switches USING GIN(model gin_trgm_ops);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_standards_name_trgm ON standards USING GIN(name gin_trgm_ops);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- TIMESERIES INDEXES
|
||||||
|
-- ============================================================
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_price_transceiver ON price_observations(transceiver_id, time DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_price_source ON price_observations(source_vendor_id, time DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_transceiver ON stock_observations(transceiver_id, time DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_metrics_technology ON market_metrics(technology, metric_type, time DESC);
|
||||||
Loading…
x
Reference in New Issue
Block a user