Initial TIP foundation: schema, seed data, crawlers, API, MCP server
- PostgreSQL 17 + TimescaleDB schema with 12 tables - 48 standards (IEEE, SFF, ITU-T, OIF, MSA) - 33 form factors (SFP through OSFP-XD/CPO) - 85+ vendors (OEM, compatible, manufacturers, marketplaces) - 80+ seed transceivers (1G-1.6T, CWDM, BiDi, DAC, AOC, FC, PON) - 60+ network devices (Cisco, Juniper, Arista, HPE, Dell, etc.) - Crawler framework with fs.com and eBay crawlers - REST API (15 endpoints) on port 3200 - MCP server (12 tools) on port 3201 - PM2 ecosystem for production deployment on Erik (.82)
This commit is contained in:
commit
de5bdb24ca
30
.env.example
Normal file
30
.env.example
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# PostgreSQL (Erik .82)
|
||||||
|
DATABASE_URL=postgresql://tip:password@192.168.178.82:5432/transceiver_db
|
||||||
|
|
||||||
|
# Qdrant (Erik .82)
|
||||||
|
QDRANT_URL=http://192.168.178.82:6333
|
||||||
|
QDRANT_COLLECTION=transceivers
|
||||||
|
|
||||||
|
# Ollama (.213)
|
||||||
|
OLLAMA_URL=http://192.168.178.213:11434
|
||||||
|
|
||||||
|
# Cloudflare R2
|
||||||
|
R2_ACCOUNT_ID=
|
||||||
|
R2_ACCESS_KEY_ID=
|
||||||
|
R2_SECRET_ACCESS_KEY=
|
||||||
|
R2_BUCKET=tip-documents
|
||||||
|
|
||||||
|
# eBay API
|
||||||
|
EBAY_APP_ID=
|
||||||
|
EBAY_CERT_ID=
|
||||||
|
|
||||||
|
# Amazon PA-API
|
||||||
|
AMAZON_ACCESS_KEY=
|
||||||
|
AMAZON_SECRET_KEY=
|
||||||
|
AMAZON_PARTNER_TAG=
|
||||||
|
|
||||||
|
# API Server
|
||||||
|
PORT=3200
|
||||||
|
HOST=0.0.0.0
|
||||||
|
NODE_ENV=development
|
||||||
|
LOG_LEVEL=info
|
||||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.dev.vars
|
||||||
|
wrangler.toml
|
||||||
|
*.local
|
||||||
|
storage/
|
||||||
|
crawl-data/
|
||||||
|
.crawlee/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
11
CHANGELOG_PENDING.md
Normal file
11
CHANGELOG_PENDING.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Changelog (Pending)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"d":"2026-03-31","t":"FEAT","m":"TIP project foundation: schema, 48 standards, 33 form factors, 85+ vendors, 80+ transceivers, 60+ network devices"}
|
||||||
|
{"d":"2026-03-31","t":"FEAT","m":"Crawler framework with fs.com (Playwright) and eBay (Cheerio) crawlers"}
|
||||||
|
{"d":"2026-03-31","t":"FEAT","m":"REST API with 15 endpoints: search, pricing, compatibility, stats, form factors, vendors, devices"}
|
||||||
|
{"d":"2026-03-31","t":"FEAT","m":"MCP server with 12 tools for AI/LLM integration via SSE protocol"}
|
||||||
|
{"d":"2026-03-31","t":"DATA","m":"Seed data: Cisco, Juniper, Arista, HPE, Dell + 60 compatible vendors, all from 1G to 1.6T"}
|
||||||
|
{"d":"2026-03-31","t":"DATA","m":"Complete CWDM spectrum (1270-1610nm), BiDi, DAC, AOC, Fibre Channel, GPON/XGS-PON"}
|
||||||
|
{"d":"2026-03-31","t":"DATA","m":"Network devices: Nexus 9000, Catalyst 9300/9500, QFX5120/5130, DCS-7050/7060, Spectrum-4"}
|
||||||
|
```
|
||||||
32
ecosystem.config.cjs
Normal file
32
ecosystem.config.cjs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'tip-api',
|
||||||
|
script: 'dist/api/server.js',
|
||||||
|
cwd: '/opt/transceiver-db',
|
||||||
|
instances: 1,
|
||||||
|
exec_mode: 'fork',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
PORT: 3200,
|
||||||
|
},
|
||||||
|
env_file: '.env',
|
||||||
|
max_memory_restart: '512M',
|
||||||
|
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tip-mcp',
|
||||||
|
script: 'dist/mcp/server.js',
|
||||||
|
cwd: '/opt/transceiver-db',
|
||||||
|
instances: 1,
|
||||||
|
exec_mode: 'fork',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
MCP_PORT: 3201,
|
||||||
|
},
|
||||||
|
env_file: '.env',
|
||||||
|
max_memory_restart: '256M',
|
||||||
|
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
454
migrations/001_foundation.sql
Normal file
454
migrations/001_foundation.sql
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
-- TIP Foundation Schema
|
||||||
|
-- PostgreSQL 17 + TimescaleDB
|
||||||
|
|
||||||
|
-- Enable extensions
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- fuzzy text search
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "btree_gin"; -- GIN index support
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- ENUMS
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TYPE transceiver_status AS ENUM ('active', 'eol', 'pre_release', 'nrnd', 'unknown');
|
||||||
|
CREATE TYPE data_rate_unit AS ENUM ('Mbps', 'Gbps', 'Tbps');
|
||||||
|
CREATE TYPE reach_unit AS ENUM ('m', 'km');
|
||||||
|
CREATE TYPE temperature_range AS ENUM ('commercial', 'extended', 'industrial');
|
||||||
|
CREATE TYPE dom_type AS ENUM ('none', 'ddm', 'ddmi', 'cmis', 'sff8472', 'sff8636');
|
||||||
|
CREATE TYPE connector_type AS ENUM (
|
||||||
|
'LC', 'SC', 'MPO-12', 'MPO-16', 'MPO-24', 'CS', 'SN',
|
||||||
|
'FC', 'ST', 'MTRJ', 'E2000', 'copper_rj45', 'cx4',
|
||||||
|
'dac_passive', 'dac_active', 'aoc', 'none', 'other'
|
||||||
|
);
|
||||||
|
CREATE TYPE fiber_type AS ENUM (
|
||||||
|
'smf', 'mmf_om1', 'mmf_om2', 'mmf_om3', 'mmf_om4', 'mmf_om5',
|
||||||
|
'copper', 'dac', 'aoc', 'free_space', 'other'
|
||||||
|
);
|
||||||
|
CREATE TYPE wavelength_band AS ENUM (
|
||||||
|
'O', 'E', 'S', 'C', 'L', 'U', 'visible', 'cwdm', 'dwdm', 'lwdm', 'swdm', 'other'
|
||||||
|
);
|
||||||
|
CREATE TYPE vendor_type AS ENUM (
|
||||||
|
'oem', 'compatible', 'distributor', 'manufacturer', 'marketplace', 'refurbished'
|
||||||
|
);
|
||||||
|
CREATE TYPE price_currency AS ENUM (
|
||||||
|
'USD', 'EUR', 'GBP', 'CNY', 'JPY', 'KRW', 'TWD', 'THB', 'INR', 'CAD', 'AUD'
|
||||||
|
);
|
||||||
|
CREATE TYPE hype_phase AS ENUM (
|
||||||
|
'innovation_trigger', 'peak_inflated', 'trough_disillusionment',
|
||||||
|
'slope_enlightenment', 'plateau_productivity', 'decline'
|
||||||
|
);
|
||||||
|
CREATE TYPE crawl_status AS ENUM ('pending', 'running', 'success', 'failed', 'rate_limited');
|
||||||
|
CREATE TYPE media_type AS ENUM ('image', 'datasheet', 'manual', 'diagram', 'video', 'certificate');
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- CORE TABLES
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Standards (IEEE, SFF, ITU-T, OIF, etc.)
|
||||||
|
CREATE TABLE standards (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
body VARCHAR(50) NOT NULL, -- IEEE, SNIA/SFF, ITU-T, OIF, MSA
|
||||||
|
version VARCHAR(50),
|
||||||
|
year INT,
|
||||||
|
url TEXT,
|
||||||
|
description TEXT,
|
||||||
|
superseded_by INT REFERENCES standards(id),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Form Factors
|
||||||
|
CREATE TABLE form_factors (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
full_name VARCHAR(200),
|
||||||
|
standard_id INT REFERENCES standards(id),
|
||||||
|
lanes INT, -- electrical lanes
|
||||||
|
max_data_rate DECIMAL(10,2),
|
||||||
|
data_rate_unit data_rate_unit DEFAULT 'Gbps',
|
||||||
|
width_mm DECIMAL(6,2),
|
||||||
|
height_mm DECIMAL(6,2),
|
||||||
|
depth_mm DECIMAL(6,2),
|
||||||
|
power_max_w DECIMAL(6,2),
|
||||||
|
generation INT, -- for hype cycle
|
||||||
|
release_year INT,
|
||||||
|
eol_year INT,
|
||||||
|
description TEXT,
|
||||||
|
image_url TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Vendors / Manufacturers / Sellers
|
||||||
|
CREATE TABLE vendors (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(200) NOT NULL,
|
||||||
|
slug VARCHAR(200) NOT NULL UNIQUE,
|
||||||
|
vendor_type vendor_type NOT NULL DEFAULT 'compatible',
|
||||||
|
website TEXT,
|
||||||
|
logo_url TEXT,
|
||||||
|
country VARCHAR(3), -- ISO 3166-1 alpha-3
|
||||||
|
founded_year INT,
|
||||||
|
description TEXT,
|
||||||
|
is_oem BOOLEAN DEFAULT FALSE, -- Cisco, Juniper, Arista, etc.
|
||||||
|
is_factory BOOLEAN DEFAULT FALSE, -- Hisense, Innolight, etc.
|
||||||
|
aliases TEXT[], -- alternative names
|
||||||
|
scrape_url TEXT, -- catalog base URL
|
||||||
|
scrape_enabled BOOLEAN DEFAULT FALSE,
|
||||||
|
scrape_interval INT DEFAULT 86400, -- seconds
|
||||||
|
last_scraped_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Core Transceiver Table
|
||||||
|
CREATE TABLE transceivers (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
part_number VARCHAR(200) NOT NULL,
|
||||||
|
vendor_id INT NOT NULL REFERENCES vendors(id),
|
||||||
|
form_factor_id INT REFERENCES form_factors(id),
|
||||||
|
|
||||||
|
-- Classification
|
||||||
|
name VARCHAR(500),
|
||||||
|
description TEXT,
|
||||||
|
category VARCHAR(100), -- SFP, SFP+, QSFP28, QSFP-DD, OSFP, etc.
|
||||||
|
subcategory VARCHAR(100), -- SR, LR, ER, ZR, BiDi, CWDM, DWDM, DAC, AOC
|
||||||
|
|
||||||
|
-- Performance
|
||||||
|
data_rate DECIMAL(10,2),
|
||||||
|
data_rate_unit data_rate_unit DEFAULT 'Gbps',
|
||||||
|
max_reach DECIMAL(10,2),
|
||||||
|
reach_unit reach_unit DEFAULT 'km',
|
||||||
|
|
||||||
|
-- Optical
|
||||||
|
wavelength_nm DECIMAL(8,2), -- TX wavelength
|
||||||
|
wavelength_rx DECIMAL(8,2), -- RX wavelength (BiDi)
|
||||||
|
wavelengths DECIMAL(8,2)[], -- CWDM/DWDM channels
|
||||||
|
wavelength_band wavelength_band,
|
||||||
|
tx_power_min DECIMAL(6,2), -- dBm
|
||||||
|
tx_power_max DECIMAL(6,2),
|
||||||
|
rx_sensitivity DECIMAL(6,2), -- dBm
|
||||||
|
link_budget_db DECIMAL(6,2),
|
||||||
|
|
||||||
|
-- Physical
|
||||||
|
connector connector_type,
|
||||||
|
fiber_type fiber_type,
|
||||||
|
duplex BOOLEAN DEFAULT TRUE,
|
||||||
|
breakout VARCHAR(50), -- e.g. "4x25G", "8x50G"
|
||||||
|
|
||||||
|
-- Environmental
|
||||||
|
temp_range temperature_range DEFAULT 'commercial',
|
||||||
|
temp_min_c DECIMAL(5,1),
|
||||||
|
temp_max_c DECIMAL(5,1),
|
||||||
|
power_consumption_w DECIMAL(6,2),
|
||||||
|
|
||||||
|
-- Monitoring
|
||||||
|
dom_support dom_type DEFAULT 'none',
|
||||||
|
|
||||||
|
-- OEM Cross-Reference
|
||||||
|
oem_part_number VARCHAR(200), -- original OEM part number
|
||||||
|
oem_vendor_id INT REFERENCES vendors(id),
|
||||||
|
|
||||||
|
-- Status
|
||||||
|
status transceiver_status DEFAULT 'active',
|
||||||
|
release_date DATE,
|
||||||
|
eol_date DATE,
|
||||||
|
|
||||||
|
-- Media
|
||||||
|
image_url TEXT,
|
||||||
|
datasheet_url TEXT,
|
||||||
|
product_url TEXT,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
tags TEXT[],
|
||||||
|
raw_specs JSONB, -- original scraped data
|
||||||
|
source VARCHAR(100), -- where this data came from
|
||||||
|
source_url TEXT,
|
||||||
|
last_verified TIMESTAMPTZ,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
UNIQUE(part_number, vendor_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- PRICING (TimescaleDB hypertable)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE prices (
|
||||||
|
time TIMESTAMPTZ NOT NULL,
|
||||||
|
transceiver_id INT NOT NULL REFERENCES transceivers(id),
|
||||||
|
vendor_id INT NOT NULL REFERENCES vendors(id),
|
||||||
|
|
||||||
|
price DECIMAL(12,4) NOT NULL,
|
||||||
|
currency price_currency DEFAULT 'USD',
|
||||||
|
price_usd DECIMAL(12,4), -- normalized to USD
|
||||||
|
|
||||||
|
quantity_min INT DEFAULT 1,
|
||||||
|
quantity_max INT,
|
||||||
|
in_stock BOOLEAN,
|
||||||
|
stock_quantity INT,
|
||||||
|
lead_time_days INT,
|
||||||
|
|
||||||
|
condition VARCHAR(20) DEFAULT 'new', -- new, refurbished, used
|
||||||
|
url TEXT,
|
||||||
|
source VARCHAR(100),
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Make prices a TimescaleDB hypertable
|
||||||
|
SELECT create_hypertable('prices', 'time', if_not_exists => TRUE);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- COMPATIBILITY
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Switch/Router models
|
||||||
|
CREATE TABLE network_devices (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
vendor_id INT NOT NULL REFERENCES vendors(id),
|
||||||
|
model VARCHAR(200) NOT NULL,
|
||||||
|
series VARCHAR(100), -- Catalyst 9300, EX4400, etc.
|
||||||
|
device_type VARCHAR(50), -- switch, router, firewall, olt, media_converter
|
||||||
|
|
||||||
|
ports_sfp INT DEFAULT 0,
|
||||||
|
ports_sfp_plus INT DEFAULT 0,
|
||||||
|
ports_sfp28 INT DEFAULT 0,
|
||||||
|
ports_qsfp_plus INT DEFAULT 0,
|
||||||
|
ports_qsfp28 INT DEFAULT 0,
|
||||||
|
ports_qsfp_dd INT DEFAULT 0,
|
||||||
|
ports_osfp INT DEFAULT 0,
|
||||||
|
ports_cfp INT DEFAULT 0,
|
||||||
|
ports_rj45 INT DEFAULT 0,
|
||||||
|
|
||||||
|
max_throughput VARCHAR(50),
|
||||||
|
release_year INT,
|
||||||
|
eol_date DATE,
|
||||||
|
status VARCHAR(20) DEFAULT 'active',
|
||||||
|
|
||||||
|
image_url TEXT,
|
||||||
|
product_url TEXT,
|
||||||
|
manual_url TEXT,
|
||||||
|
|
||||||
|
raw_specs JSONB,
|
||||||
|
source VARCHAR(100),
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
UNIQUE(vendor_id, model)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Compatibility matrix
|
||||||
|
CREATE TABLE compatibility (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
transceiver_id INT NOT NULL REFERENCES transceivers(id),
|
||||||
|
device_id INT NOT NULL REFERENCES network_devices(id),
|
||||||
|
|
||||||
|
verified BOOLEAN DEFAULT FALSE, -- vendor-verified or community-tested
|
||||||
|
verified_by VARCHAR(100), -- vendor, community, lab
|
||||||
|
firmware_min VARCHAR(50),
|
||||||
|
firmware_max VARCHAR(50),
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
source VARCHAR(100), -- cisco_tmg, juniper_hct, community, etc.
|
||||||
|
source_url TEXT,
|
||||||
|
verified_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
UNIQUE(transceiver_id, device_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- KNOWLEDGE BASE
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE faq_articles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(500) NOT NULL,
|
||||||
|
slug VARCHAR(500) NOT NULL UNIQUE,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
summary TEXT,
|
||||||
|
category VARCHAR(100),
|
||||||
|
tags TEXT[],
|
||||||
|
|
||||||
|
related_transceivers INT[],
|
||||||
|
related_devices INT[],
|
||||||
|
|
||||||
|
view_count INT DEFAULT 0,
|
||||||
|
helpful_count INT DEFAULT 0,
|
||||||
|
|
||||||
|
source VARCHAR(100),
|
||||||
|
source_url TEXT,
|
||||||
|
embedding_id VARCHAR(100), -- Qdrant point ID
|
||||||
|
|
||||||
|
published BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- HYPE CYCLE ENGINE
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE hype_cycles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
technology VARCHAR(200) NOT NULL, -- e.g. "QSFP-DD 400G", "Silicon Photonics"
|
||||||
|
form_factor_id INT REFERENCES form_factors(id),
|
||||||
|
|
||||||
|
-- Bass Model Parameters
|
||||||
|
bass_p DECIMAL(10,6), -- innovation coefficient
|
||||||
|
bass_q DECIMAL(10,6), -- imitation coefficient
|
||||||
|
bass_m BIGINT, -- market potential
|
||||||
|
|
||||||
|
current_phase hype_phase,
|
||||||
|
phase_started DATE,
|
||||||
|
predicted_peak DATE,
|
||||||
|
predicted_trough DATE,
|
||||||
|
predicted_plateau DATE,
|
||||||
|
|
||||||
|
-- Signals
|
||||||
|
adoption_units BIGINT,
|
||||||
|
market_size_usd BIGINT,
|
||||||
|
search_trend DECIMAL(5,2), -- Google Trends 0-100
|
||||||
|
patent_count INT,
|
||||||
|
paper_count INT,
|
||||||
|
news_sentiment DECIMAL(5,2), -- -1.0 to 1.0
|
||||||
|
|
||||||
|
confidence DECIMAL(5,2), -- model confidence 0-1
|
||||||
|
|
||||||
|
data_points JSONB, -- time series data
|
||||||
|
model_output JSONB, -- full model results
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- MEDIA / DOCUMENTS
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE media (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
transceiver_id INT REFERENCES transceivers(id),
|
||||||
|
device_id INT REFERENCES network_devices(id),
|
||||||
|
vendor_id INT REFERENCES vendors(id),
|
||||||
|
|
||||||
|
media_type media_type NOT NULL,
|
||||||
|
title VARCHAR(500),
|
||||||
|
url TEXT NOT NULL, -- original URL
|
||||||
|
r2_key VARCHAR(500), -- Cloudflare R2 key
|
||||||
|
r2_url TEXT, -- R2 public URL
|
||||||
|
|
||||||
|
mime_type VARCHAR(100),
|
||||||
|
file_size_bytes BIGINT,
|
||||||
|
width_px INT,
|
||||||
|
height_px INT,
|
||||||
|
|
||||||
|
ocr_text TEXT, -- extracted text (Docling)
|
||||||
|
embedding_id VARCHAR(100), -- Qdrant point ID
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- CRAWL TRACKING
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE crawl_jobs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
crawler VARCHAR(100) NOT NULL, -- fscom, cisco_tmg, ebay, etc.
|
||||||
|
status crawl_status DEFAULT 'pending',
|
||||||
|
|
||||||
|
urls_total INT DEFAULT 0,
|
||||||
|
urls_processed INT DEFAULT 0,
|
||||||
|
urls_failed INT DEFAULT 0,
|
||||||
|
items_found INT DEFAULT 0,
|
||||||
|
items_new INT DEFAULT 0,
|
||||||
|
items_updated INT DEFAULT 0,
|
||||||
|
|
||||||
|
error_message TEXT,
|
||||||
|
duration_ms INT,
|
||||||
|
|
||||||
|
started_at TIMESTAMPTZ,
|
||||||
|
finished_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE crawl_errors (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
job_id INT REFERENCES crawl_jobs(id),
|
||||||
|
url TEXT,
|
||||||
|
error_code VARCHAR(20),
|
||||||
|
error_message TEXT,
|
||||||
|
retry_count INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- NEWS / BLOG
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE news_articles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(500) NOT NULL,
|
||||||
|
url TEXT NOT NULL UNIQUE,
|
||||||
|
source VARCHAR(100),
|
||||||
|
author VARCHAR(200),
|
||||||
|
|
||||||
|
content TEXT,
|
||||||
|
summary TEXT,
|
||||||
|
|
||||||
|
tags TEXT[],
|
||||||
|
mentioned_technologies TEXT[],
|
||||||
|
sentiment DECIMAL(5,2),
|
||||||
|
|
||||||
|
published_at TIMESTAMPTZ,
|
||||||
|
scraped_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- INDEXES
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Transceivers
|
||||||
|
CREATE INDEX idx_transceivers_vendor ON transceivers(vendor_id);
|
||||||
|
CREATE INDEX idx_transceivers_form_factor ON transceivers(form_factor_id);
|
||||||
|
CREATE INDEX idx_transceivers_category ON transceivers(category);
|
||||||
|
CREATE INDEX idx_transceivers_data_rate ON transceivers(data_rate);
|
||||||
|
CREATE INDEX idx_transceivers_wavelength ON transceivers(wavelength_nm);
|
||||||
|
CREATE INDEX idx_transceivers_status ON transceivers(status);
|
||||||
|
CREATE INDEX idx_transceivers_part_number_gin ON transceivers USING gin(part_number gin_trgm_ops);
|
||||||
|
CREATE INDEX idx_transceivers_name_gin ON transceivers USING gin(name gin_trgm_ops);
|
||||||
|
CREATE INDEX idx_transceivers_tags ON transceivers USING gin(tags);
|
||||||
|
CREATE INDEX idx_transceivers_oem ON transceivers(oem_part_number) WHERE oem_part_number IS NOT NULL;
|
||||||
|
|
||||||
|
-- Prices
|
||||||
|
CREATE INDEX idx_prices_transceiver ON prices(transceiver_id, time DESC);
|
||||||
|
CREATE INDEX idx_prices_vendor ON prices(vendor_id, time DESC);
|
||||||
|
|
||||||
|
-- Compatibility
|
||||||
|
CREATE INDEX idx_compat_transceiver ON compatibility(transceiver_id);
|
||||||
|
CREATE INDEX idx_compat_device ON compatibility(device_id);
|
||||||
|
|
||||||
|
-- Devices
|
||||||
|
CREATE INDEX idx_devices_vendor ON network_devices(vendor_id);
|
||||||
|
CREATE INDEX idx_devices_model_gin ON network_devices USING gin(model gin_trgm_ops);
|
||||||
|
|
||||||
|
-- FAQ
|
||||||
|
CREATE INDEX idx_faq_tags ON faq_articles USING gin(tags);
|
||||||
|
CREATE INDEX idx_faq_content_gin ON faq_articles USING gin(content gin_trgm_ops);
|
||||||
|
|
||||||
|
-- Media
|
||||||
|
CREATE INDEX idx_media_transceiver ON media(transceiver_id);
|
||||||
|
CREATE INDEX idx_media_type ON media(media_type);
|
||||||
|
|
||||||
|
-- Crawl
|
||||||
|
CREATE INDEX idx_crawl_jobs_status ON crawl_jobs(status);
|
||||||
|
CREATE INDEX idx_crawl_jobs_crawler ON crawl_jobs(crawler);
|
||||||
9049
package-lock.json
generated
Normal file
9049
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
59
package.json
Normal file
59
package.json
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "transceiver-intelligence-platform",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "The Octopart for Optical Transceivers — real-time pricing, compatibility, hype cycle engine",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/api/server.ts",
|
||||||
|
"start": "node dist/api/server.js",
|
||||||
|
"build": "tsup src/api/server.ts src/mcp/server.ts --format esm --dts",
|
||||||
|
"migrate": "node --import tsx scripts/migrate.ts",
|
||||||
|
"seed": "node --import tsx scripts/seed.ts",
|
||||||
|
"crawl": "node --import tsx scripts/crawl.ts",
|
||||||
|
"crawl:fs": "node --import tsx src/crawlers/fscom.ts",
|
||||||
|
"crawl:10gtek": "node --import tsx src/crawlers/tengtek.ts",
|
||||||
|
"crawl:ebay": "node --import tsx src/crawlers/ebay.ts",
|
||||||
|
"crawl:cisco-tmg": "node --import tsx src/crawlers/cisco-tmg.ts",
|
||||||
|
"crawl:axiom": "node --import tsx src/crawlers/axiom.ts",
|
||||||
|
"crawl:prolabs": "node --import tsx src/crawlers/prolabs.ts",
|
||||||
|
"mcp": "node --import tsx src/mcp/server.ts",
|
||||||
|
"test": "vitest",
|
||||||
|
"lint": "eslint src/",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pg": "^8.13.0",
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"helmet": "^8.0.0",
|
||||||
|
"compression": "^1.7.5",
|
||||||
|
"crawlee": "^3.12.0",
|
||||||
|
"playwright": "^1.48.0",
|
||||||
|
"cheerio": "^1.0.0",
|
||||||
|
"pg-boss": "^10.1.0",
|
||||||
|
"pino": "^9.5.0",
|
||||||
|
"pino-pretty": "^11.3.0",
|
||||||
|
"zod": "^3.23.0",
|
||||||
|
"dotenv": "^16.4.0",
|
||||||
|
"p-queue": "^8.0.1",
|
||||||
|
"p-retry": "^6.2.0",
|
||||||
|
"cron": "^3.1.0",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"undici": "^7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/pg": "^8.11.0",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"tsup": "^8.3.0",
|
||||||
|
"vitest": "^2.1.0",
|
||||||
|
"eslint": "^9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
40
scripts/crawl.ts
Normal file
40
scripts/crawl.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { pino } from 'pino';
|
||||||
|
import { FsComCrawler } from '../src/crawlers/fscom.js';
|
||||||
|
import { EbayCrawler } from '../src/crawlers/ebay.js';
|
||||||
|
|
||||||
|
const log = pino({ name: 'crawl-orchestrator' });
|
||||||
|
|
||||||
|
const CRAWLERS: Record<string, () => { run: () => Promise<void> }> = {
|
||||||
|
fscom: () => new FsComCrawler(),
|
||||||
|
ebay: () => new EbayCrawler(),
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const target = process.argv[2];
|
||||||
|
|
||||||
|
if (target && CRAWLERS[target]) {
|
||||||
|
log.info({ crawler: target }, 'Running single crawler');
|
||||||
|
const crawler = CRAWLERS[target]!();
|
||||||
|
await crawler.run();
|
||||||
|
} else if (target === 'all' || !target) {
|
||||||
|
log.info('Running all crawlers sequentially');
|
||||||
|
for (const [name, factory] of Object.entries(CRAWLERS)) {
|
||||||
|
log.info({ crawler: name }, 'Starting crawler');
|
||||||
|
try {
|
||||||
|
const crawler = factory();
|
||||||
|
await crawler.run();
|
||||||
|
} catch (err) {
|
||||||
|
log.error({ err, crawler: name }, 'Crawler failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error(`Unknown crawler: ${target}. Available: ${Object.keys(CRAWLERS).join(', ')}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Crawl orchestration complete');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
55
scripts/migrate.ts
Normal file
55
scripts/migrate.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { readFileSync, readdirSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { query, pool } from '../src/utils/db.js';
|
||||||
|
import { pino } from 'pino';
|
||||||
|
|
||||||
|
const log = pino({ name: 'migrate' });
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
log.info('Running migrations...');
|
||||||
|
|
||||||
|
// Create migrations tracking table
|
||||||
|
await query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS _migrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(200) NOT NULL UNIQUE,
|
||||||
|
applied_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get applied migrations
|
||||||
|
const applied = await query('SELECT name FROM _migrations ORDER BY id');
|
||||||
|
const appliedSet = new Set(applied.rows.map(r => r.name));
|
||||||
|
|
||||||
|
// Read migration files
|
||||||
|
const migrationsDir = join(import.meta.dirname ?? '.', '..', 'migrations');
|
||||||
|
const files = readdirSync(migrationsDir)
|
||||||
|
.filter(f => f.endsWith('.sql'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (appliedSet.has(file)) {
|
||||||
|
log.info(`Skip (already applied): ${file}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Applying: ${file}`);
|
||||||
|
const sql = readFileSync(join(migrationsDir, file), 'utf-8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await query(sql);
|
||||||
|
await query('INSERT INTO _migrations (name) VALUES ($1)', [file]);
|
||||||
|
log.info(`Applied: ${file}`);
|
||||||
|
} catch (err) {
|
||||||
|
log.error({ err, file }, 'Migration failed');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('All migrations applied');
|
||||||
|
await pool.end();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
142
scripts/seed.ts
Normal file
142
scripts/seed.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { query, transaction } from '../src/utils/db.js';
|
||||||
|
import { standards } from '../seed/standards.js';
|
||||||
|
import { formFactors } from '../seed/form-factors.js';
|
||||||
|
import { vendors } from '../seed/vendors.js';
|
||||||
|
import { transceivers } from '../seed/transceivers.js';
|
||||||
|
import { networkDevices } from '../seed/network-devices.js';
|
||||||
|
import { pino } from 'pino';
|
||||||
|
|
||||||
|
const log = pino({ name: 'seed' });
|
||||||
|
|
||||||
|
async function seedStandards() {
|
||||||
|
log.info(`Seeding ${standards.length} standards...`);
|
||||||
|
for (const s of standards) {
|
||||||
|
await query(
|
||||||
|
`INSERT INTO standards (name, body, version, year, url, description)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
ON CONFLICT (name) DO UPDATE SET version = $3, year = $4, url = $5, description = $6, updated_at = NOW()`,
|
||||||
|
[s.name, s.body, s.version ?? null, s.year ?? null, s.url ?? null, s.description ?? null]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.info('Standards seeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedFormFactors() {
|
||||||
|
log.info(`Seeding ${formFactors.length} form factors...`);
|
||||||
|
for (const ff of formFactors) {
|
||||||
|
await query(
|
||||||
|
`INSERT INTO form_factors (name, full_name, lanes, max_data_rate, data_rate_unit, width_mm, height_mm, depth_mm, power_max_w, generation, release_year, eol_year, description, image_url)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||||
|
ON CONFLICT (name) DO UPDATE SET full_name = $2, lanes = $3, max_data_rate = $4, power_max_w = $9, updated_at = NOW()`,
|
||||||
|
[ff.name, ff.full_name ?? null, ff.lanes ?? null, ff.max_data_rate ?? null, ff.data_rate_unit ?? 'Gbps',
|
||||||
|
ff.width_mm ?? null, ff.height_mm ?? null, ff.depth_mm ?? null, ff.power_max_w ?? null,
|
||||||
|
ff.generation ?? null, ff.release_year ?? null, ff.eol_year ?? null, ff.description ?? null, ff.image_url ?? null]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.info('Form factors seeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedVendors() {
|
||||||
|
log.info(`Seeding ${vendors.length} vendors...`);
|
||||||
|
for (const v of vendors) {
|
||||||
|
await query(
|
||||||
|
`INSERT INTO vendors (name, slug, vendor_type, website, logo_url, country, founded_year, description, is_oem, is_factory, aliases, scrape_url, scrape_enabled)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||||
|
ON CONFLICT (slug) DO UPDATE SET name = $1, website = $4, description = $8, scrape_url = $12, scrape_enabled = $13, updated_at = NOW()`,
|
||||||
|
[v.name, v.slug, v.vendor_type, v.website ?? null, v.logo_url ?? null,
|
||||||
|
v.country ?? null, v.founded_year ?? null, v.description ?? null,
|
||||||
|
v.is_oem ?? false, v.is_factory ?? false, v.aliases ?? null,
|
||||||
|
v.scrape_url ?? null, v.scrape_enabled ?? false]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.info('Vendors seeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedTransceivers() {
|
||||||
|
log.info(`Seeding ${transceivers.length} transceivers...`);
|
||||||
|
|
||||||
|
// Build lookup maps
|
||||||
|
const vendorRows = await query('SELECT id, slug FROM vendors');
|
||||||
|
const vendorMap = new Map(vendorRows.rows.map(r => [r.slug, r.id]));
|
||||||
|
|
||||||
|
const ffRows = await query('SELECT id, name FROM form_factors');
|
||||||
|
const ffMap = new Map(ffRows.rows.map(r => [r.name, r.id]));
|
||||||
|
|
||||||
|
for (const t of transceivers) {
|
||||||
|
const vendorId = vendorMap.get(t.vendor_slug);
|
||||||
|
if (!vendorId) { log.warn(`Vendor not found: ${t.vendor_slug}`); continue; }
|
||||||
|
|
||||||
|
const ffId = ffMap.get(t.form_factor) ?? null;
|
||||||
|
const oemVendorId = t.oem_vendor_slug ? vendorMap.get(t.oem_vendor_slug) ?? null : null;
|
||||||
|
|
||||||
|
await query(
|
||||||
|
`INSERT INTO transceivers (part_number, vendor_id, form_factor_id, name, category, subcategory,
|
||||||
|
data_rate, data_rate_unit, max_reach, reach_unit, wavelength_nm, wavelength_rx,
|
||||||
|
connector, fiber_type, duplex, temp_range, dom_support,
|
||||||
|
oem_part_number, oem_vendor_id, description, tags, status, source)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, 'seed')
|
||||||
|
ON CONFLICT (part_number, vendor_id) DO UPDATE SET
|
||||||
|
name = $4, category = $5, subcategory = $6, data_rate = $7, max_reach = $9,
|
||||||
|
wavelength_nm = $11, tags = $21, updated_at = NOW()`,
|
||||||
|
[
|
||||||
|
t.part_number, vendorId, ffId, t.name, t.category, t.subcategory,
|
||||||
|
t.data_rate, t.data_rate_unit, t.max_reach, t.reach_unit,
|
||||||
|
t.wavelength_nm, t.wavelength_rx ?? null,
|
||||||
|
t.connector, t.fiber_type, t.duplex, t.temp_range, t.dom_support,
|
||||||
|
t.oem_part_number ?? null, oemVendorId, t.description ?? null,
|
||||||
|
t.tags, t.status
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.info('Transceivers seeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedNetworkDevices() {
|
||||||
|
log.info(`Seeding ${networkDevices.length} network devices...`);
|
||||||
|
|
||||||
|
const vendorRows = await query('SELECT id, slug FROM vendors');
|
||||||
|
const vendorMap = new Map(vendorRows.rows.map(r => [r.slug, r.id]));
|
||||||
|
|
||||||
|
for (const d of networkDevices) {
|
||||||
|
const vendorId = vendorMap.get(d.vendor_slug);
|
||||||
|
if (!vendorId) { log.warn(`Vendor not found: ${d.vendor_slug}`); continue; }
|
||||||
|
|
||||||
|
await query(
|
||||||
|
`INSERT INTO network_devices (vendor_id, model, series, device_type,
|
||||||
|
ports_sfp, ports_sfp_plus, ports_sfp28, ports_qsfp_plus, ports_qsfp28, ports_qsfp_dd, ports_osfp, ports_rj45,
|
||||||
|
max_throughput, release_year, status)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
|
ON CONFLICT (vendor_id, model) DO UPDATE SET
|
||||||
|
series = $3, max_throughput = $13, updated_at = NOW()`,
|
||||||
|
[
|
||||||
|
vendorId, d.model, d.series, d.device_type,
|
||||||
|
d.ports_sfp ?? 0, d.ports_sfp_plus ?? 0, d.ports_sfp28 ?? 0,
|
||||||
|
d.ports_qsfp_plus ?? 0, d.ports_qsfp28 ?? 0, d.ports_qsfp_dd ?? 0,
|
||||||
|
d.ports_osfp ?? 0, d.ports_rj45 ?? 0,
|
||||||
|
d.max_throughput ?? null, d.release_year ?? null, d.status
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.info('Network devices seeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
log.info('Starting TIP seed...');
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
await seedStandards();
|
||||||
|
await seedFormFactors();
|
||||||
|
await seedVendors();
|
||||||
|
await seedTransceivers();
|
||||||
|
await seedNetworkDevices();
|
||||||
|
|
||||||
|
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
||||||
|
log.info(`Seed complete in ${duration}s`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
log.error({ err }, 'Seed failed');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
234
seed/form-factors.ts
Normal file
234
seed/form-factors.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import type { FormFactor } from '../src/types/index.js';
|
||||||
|
|
||||||
|
export const formFactors: FormFactor[] = [
|
||||||
|
// SFP Family
|
||||||
|
{
|
||||||
|
name: 'SFP', full_name: 'Small Form-factor Pluggable',
|
||||||
|
lanes: 1, max_data_rate: 4.25, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 13.4, height_mm: 8.5, depth_mm: 56.5, power_max_w: 1.0,
|
||||||
|
generation: 1, release_year: 2001,
|
||||||
|
description: 'Standard SFP for 100M/1G/2.5G/4G FC applications'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SFP+', full_name: 'Enhanced Small Form-factor Pluggable',
|
||||||
|
lanes: 1, max_data_rate: 16, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 13.4, height_mm: 8.5, depth_mm: 56.5, power_max_w: 1.5,
|
||||||
|
generation: 2, release_year: 2006,
|
||||||
|
description: '10G SFP form factor, backward compatible with SFP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SFP28', full_name: 'SFP28 (28 Gbaud)',
|
||||||
|
lanes: 1, max_data_rate: 25, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 13.4, height_mm: 8.5, depth_mm: 56.5, power_max_w: 1.5,
|
||||||
|
generation: 3, release_year: 2014,
|
||||||
|
description: '25G SFP form factor, same cage as SFP/SFP+'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SFP56', full_name: 'SFP56 (56 Gbaud PAM4)',
|
||||||
|
lanes: 1, max_data_rate: 50, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 13.4, height_mm: 8.5, depth_mm: 56.5, power_max_w: 2.0,
|
||||||
|
generation: 4, release_year: 2019,
|
||||||
|
description: '50G SFP using PAM4 modulation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SFP-DD', full_name: 'SFP Double Density',
|
||||||
|
lanes: 2, max_data_rate: 100, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 13.4, height_mm: 8.5, depth_mm: 67.6, power_max_w: 3.5,
|
||||||
|
generation: 5, release_year: 2019,
|
||||||
|
description: '100G dual-lane SFP, backward compatible with SFP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SFP112', full_name: 'SFP112 (112 Gbaud)',
|
||||||
|
lanes: 1, max_data_rate: 100, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 13.4, height_mm: 8.5, depth_mm: 56.5, power_max_w: 2.5,
|
||||||
|
generation: 6, release_year: 2024,
|
||||||
|
description: '100G single-lane SFP with 112G SerDes'
|
||||||
|
},
|
||||||
|
|
||||||
|
// QSFP Family
|
||||||
|
{
|
||||||
|
name: 'QSFP+', full_name: 'Quad Small Form-factor Pluggable Plus',
|
||||||
|
lanes: 4, max_data_rate: 40, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 18.35, height_mm: 8.5, depth_mm: 72.4, power_max_w: 3.5,
|
||||||
|
generation: 2, release_year: 2009,
|
||||||
|
description: '40G QSFP (4x10G), widely deployed in data centers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'QSFP28', full_name: 'QSFP28 (28 Gbaud)',
|
||||||
|
lanes: 4, max_data_rate: 100, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 18.35, height_mm: 8.5, depth_mm: 72.4, power_max_w: 3.5,
|
||||||
|
generation: 3, release_year: 2014,
|
||||||
|
description: '100G QSFP (4x25G), most popular 100G form factor'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'QSFP56', full_name: 'QSFP56 (56 Gbaud PAM4)',
|
||||||
|
lanes: 4, max_data_rate: 200, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 18.35, height_mm: 8.5, depth_mm: 72.4, power_max_w: 5.0,
|
||||||
|
generation: 4, release_year: 2019,
|
||||||
|
description: '200G QSFP (4x50G PAM4)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'QSFP-DD', full_name: 'QSFP Double Density',
|
||||||
|
lanes: 8, max_data_rate: 400, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 18.35, height_mm: 9.5, depth_mm: 89.4, power_max_w: 12.0,
|
||||||
|
generation: 5, release_year: 2019,
|
||||||
|
description: '400G QSFP-DD (8x50G), backward compatible with QSFP28'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'QSFP-DD800', full_name: 'QSFP-DD800 (112 Gbaud)',
|
||||||
|
lanes: 8, max_data_rate: 800, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 18.35, height_mm: 9.5, depth_mm: 89.4, power_max_w: 18.0,
|
||||||
|
generation: 6, release_year: 2023,
|
||||||
|
description: '800G QSFP-DD (8x100G), next-gen data center interconnect'
|
||||||
|
},
|
||||||
|
|
||||||
|
// OSFP Family
|
||||||
|
{
|
||||||
|
name: 'OSFP', full_name: 'Octal Small Form-factor Pluggable',
|
||||||
|
lanes: 8, max_data_rate: 400, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 22.58, height_mm: 13.0, depth_mm: 107.8, power_max_w: 15.0,
|
||||||
|
generation: 5, release_year: 2019,
|
||||||
|
description: '400G OSFP (8x50G), larger than QSFP-DD for better thermal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OSFP-XD', full_name: 'OSFP Extra Density',
|
||||||
|
lanes: 16, max_data_rate: 1600, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 22.58, height_mm: 13.0, depth_mm: 107.8, power_max_w: 30.0,
|
||||||
|
generation: 7, release_year: 2024,
|
||||||
|
description: '1.6T OSFP (16x100G), designed for AI/ML clusters'
|
||||||
|
},
|
||||||
|
|
||||||
|
// CFP Family
|
||||||
|
{
|
||||||
|
name: 'CFP', full_name: 'C Form-factor Pluggable',
|
||||||
|
lanes: 10, max_data_rate: 100, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 82.0, height_mm: 12.4, depth_mm: 144.75, power_max_w: 24.0,
|
||||||
|
generation: 1, release_year: 2010,
|
||||||
|
description: 'First 100G pluggable, large form factor'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CFP2', full_name: 'CFP2',
|
||||||
|
lanes: 8, max_data_rate: 200, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 41.5, height_mm: 12.4, depth_mm: 108.0, power_max_w: 12.0,
|
||||||
|
generation: 2, release_year: 2013,
|
||||||
|
description: 'Half-width CFP, popular for 100G coherent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CFP2-DCO', full_name: 'CFP2 Digital Coherent Optics',
|
||||||
|
lanes: 4, max_data_rate: 400, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 41.5, height_mm: 12.4, depth_mm: 108.0, power_max_w: 20.0,
|
||||||
|
generation: 3, release_year: 2018,
|
||||||
|
description: 'CFP2 with integrated DSP for coherent applications'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CFP4', full_name: 'CFP4',
|
||||||
|
lanes: 4, max_data_rate: 100, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 21.5, height_mm: 9.5, depth_mm: 92.0, power_max_w: 6.0,
|
||||||
|
generation: 3, release_year: 2014,
|
||||||
|
description: 'Quarter-width CFP, compact 100G'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CFP8', full_name: 'CFP8',
|
||||||
|
lanes: 8, max_data_rate: 400, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 40.0, height_mm: 9.5, depth_mm: 102.0, power_max_w: 12.0,
|
||||||
|
generation: 4, release_year: 2017,
|
||||||
|
description: 'CFP8 for 400G applications'
|
||||||
|
},
|
||||||
|
|
||||||
|
// XFP
|
||||||
|
{
|
||||||
|
name: 'XFP', full_name: '10 Gigabit Small Form Factor Pluggable',
|
||||||
|
lanes: 1, max_data_rate: 11.1, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 18.35, height_mm: 8.5, depth_mm: 78.0, power_max_w: 3.5,
|
||||||
|
generation: 1, release_year: 2002, eol_year: 2020,
|
||||||
|
description: 'First 10G pluggable, superseded by SFP+'
|
||||||
|
},
|
||||||
|
|
||||||
|
// GBIC
|
||||||
|
{
|
||||||
|
name: 'GBIC', full_name: 'Gigabit Interface Converter',
|
||||||
|
lanes: 1, max_data_rate: 1.25, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 30.0, height_mm: 12.7, depth_mm: 65.0, power_max_w: 2.0,
|
||||||
|
generation: 0, release_year: 1995, eol_year: 2010,
|
||||||
|
description: 'Original pluggable transceiver, superseded by SFP'
|
||||||
|
},
|
||||||
|
|
||||||
|
// X2 / XENPAK
|
||||||
|
{
|
||||||
|
name: 'X2', full_name: 'X2 10 Gigabit Transceiver',
|
||||||
|
lanes: 4, max_data_rate: 10, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 35.0, height_mm: 13.0, depth_mm: 64.4, power_max_w: 5.0,
|
||||||
|
generation: 1, release_year: 2004, eol_year: 2015,
|
||||||
|
description: 'Compact 10G, superseded by SFP+'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'XENPAK', full_name: 'XENPAK 10 Gigabit Transceiver',
|
||||||
|
lanes: 4, max_data_rate: 10, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 35.0, height_mm: 13.5, depth_mm: 131.0, power_max_w: 8.0,
|
||||||
|
generation: 0, release_year: 2002, eol_year: 2012,
|
||||||
|
description: 'Early 10G form factor, superseded by XFP/SFP+'
|
||||||
|
},
|
||||||
|
|
||||||
|
// CXP
|
||||||
|
{
|
||||||
|
name: 'CXP', full_name: 'CXP 12x QDR InfiniBand',
|
||||||
|
lanes: 12, max_data_rate: 120, data_rate_unit: 'Gbps',
|
||||||
|
width_mm: 45.0, height_mm: 12.0, depth_mm: 104.0, power_max_w: 6.0,
|
||||||
|
generation: 2, release_year: 2010, eol_year: 2018,
|
||||||
|
description: '120G parallel optics for InfiniBand'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Copper/DAC form factors
|
||||||
|
{
|
||||||
|
name: 'QSFP28-DAC', full_name: 'QSFP28 Direct Attach Copper',
|
||||||
|
lanes: 4, max_data_rate: 100, data_rate_unit: 'Gbps',
|
||||||
|
generation: 3, release_year: 2015,
|
||||||
|
description: '100G passive/active copper cable assembly'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'QSFP-DD-DAC', full_name: 'QSFP-DD Direct Attach Copper',
|
||||||
|
lanes: 8, max_data_rate: 400, data_rate_unit: 'Gbps',
|
||||||
|
generation: 5, release_year: 2020,
|
||||||
|
description: '400G DAC cable assembly'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SFP28-DAC', full_name: 'SFP28 Direct Attach Copper',
|
||||||
|
lanes: 1, max_data_rate: 25, data_rate_unit: 'Gbps',
|
||||||
|
generation: 3, release_year: 2015,
|
||||||
|
description: '25G DAC cable assembly'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SFP+-DAC', full_name: 'SFP+ Direct Attach Copper',
|
||||||
|
lanes: 1, max_data_rate: 10, data_rate_unit: 'Gbps',
|
||||||
|
generation: 2, release_year: 2008,
|
||||||
|
description: '10G DAC cable assembly'
|
||||||
|
},
|
||||||
|
|
||||||
|
// AOC
|
||||||
|
{
|
||||||
|
name: 'QSFP28-AOC', full_name: 'QSFP28 Active Optical Cable',
|
||||||
|
lanes: 4, max_data_rate: 100, data_rate_unit: 'Gbps',
|
||||||
|
generation: 3, release_year: 2015,
|
||||||
|
description: '100G AOC cable assembly'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'QSFP-DD-AOC', full_name: 'QSFP-DD Active Optical Cable',
|
||||||
|
lanes: 8, max_data_rate: 400, data_rate_unit: 'Gbps',
|
||||||
|
generation: 5, release_year: 2020,
|
||||||
|
description: '400G AOC cable assembly'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Emerging
|
||||||
|
{
|
||||||
|
name: 'CPO', full_name: 'Co-Packaged Optics',
|
||||||
|
lanes: 32, max_data_rate: 3200, data_rate_unit: 'Gbps',
|
||||||
|
power_max_w: 50.0, generation: 8, release_year: 2027,
|
||||||
|
description: 'Optics co-packaged with switch ASIC (Broadcom/Intel roadmap)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'LPO', full_name: 'Linear-drive Pluggable Optics',
|
||||||
|
lanes: 8, max_data_rate: 800, data_rate_unit: 'Gbps',
|
||||||
|
power_max_w: 10.0, generation: 7, release_year: 2025,
|
||||||
|
description: 'Linear-drive optics eliminating DSP for lower power'
|
||||||
|
},
|
||||||
|
];
|
||||||
126
seed/network-devices.ts
Normal file
126
seed/network-devices.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// Network Switch/Router Seed Data — Major Series with Port Configurations
|
||||||
|
|
||||||
|
interface SeedDevice {
|
||||||
|
vendor_slug: string;
|
||||||
|
model: string;
|
||||||
|
series: string;
|
||||||
|
device_type: string;
|
||||||
|
ports_sfp?: number;
|
||||||
|
ports_sfp_plus?: number;
|
||||||
|
ports_sfp28?: number;
|
||||||
|
ports_qsfp_plus?: number;
|
||||||
|
ports_qsfp28?: number;
|
||||||
|
ports_qsfp_dd?: number;
|
||||||
|
ports_osfp?: number;
|
||||||
|
ports_rj45?: number;
|
||||||
|
max_throughput?: string;
|
||||||
|
release_year?: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const networkDevices: SeedDevice[] = [
|
||||||
|
// ============================================================
|
||||||
|
// CISCO
|
||||||
|
// ============================================================
|
||||||
|
// Catalyst 9300
|
||||||
|
{ vendor_slug: 'cisco', model: 'C9300-24T', series: 'Catalyst 9300', device_type: 'switch', ports_rj45: 24, ports_sfp_plus: 4, max_throughput: '480 Gbps', release_year: 2017, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'C9300-48P', series: 'Catalyst 9300', device_type: 'switch', ports_rj45: 48, ports_sfp_plus: 4, max_throughput: '480 Gbps', release_year: 2017, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'C9300-24UX', series: 'Catalyst 9300', device_type: 'switch', ports_sfp_plus: 24, ports_qsfp28: 2, max_throughput: '480 Gbps', release_year: 2018, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'C9300L-48T-4X', series: 'Catalyst 9300L', device_type: 'switch', ports_rj45: 48, ports_sfp_plus: 4, max_throughput: '240 Gbps', release_year: 2019, status: 'active' },
|
||||||
|
// Catalyst 9400
|
||||||
|
{ vendor_slug: 'cisco', model: 'C9400-SUP-1', series: 'Catalyst 9400', device_type: 'switch', max_throughput: '9 Tbps', release_year: 2017, status: 'active' },
|
||||||
|
// Catalyst 9500
|
||||||
|
{ vendor_slug: 'cisco', model: 'C9500-24Y4C', series: 'Catalyst 9500', device_type: 'switch', ports_sfp28: 24, ports_qsfp28: 4, max_throughput: '3.6 Tbps', release_year: 2018, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'C9500-48Y4C', series: 'Catalyst 9500', device_type: 'switch', ports_sfp28: 48, ports_qsfp28: 4, max_throughput: '6.4 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'C9500-32C', series: 'Catalyst 9500', device_type: 'switch', ports_qsfp28: 32, max_throughput: '6.4 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
// Nexus 9000
|
||||||
|
{ vendor_slug: 'cisco', model: 'N9K-C9336C-FX2', series: 'Nexus 9300', device_type: 'switch', ports_qsfp28: 36, max_throughput: '7.2 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'N9K-C93180YC-FX', series: 'Nexus 9300', device_type: 'switch', ports_sfp28: 48, ports_qsfp28: 6, max_throughput: '3.6 Tbps', release_year: 2018, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'N9K-C9364D-GX2A', series: 'Nexus 9300', device_type: 'switch', ports_qsfp_dd: 64, max_throughput: '25.6 Tbps', release_year: 2022, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'N9K-C9332D-GX2B', series: 'Nexus 9300', device_type: 'switch', ports_qsfp_dd: 32, max_throughput: '12.8 Tbps', release_year: 2022, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'N9K-C9508', series: 'Nexus 9500', device_type: 'switch', max_throughput: '172.8 Tbps', release_year: 2013, status: 'active' },
|
||||||
|
// NCS 5500
|
||||||
|
{ vendor_slug: 'cisco', model: 'NCS-5501-SE', series: 'NCS 5500', device_type: 'router', ports_sfp28: 40, ports_qsfp28: 4, max_throughput: '4 Tbps', release_year: 2017, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'NCS-5504', series: 'NCS 5500', device_type: 'router', max_throughput: '64 Tbps', release_year: 2016, status: 'active' },
|
||||||
|
// ASR 9000
|
||||||
|
{ vendor_slug: 'cisco', model: 'ASR-9901', series: 'ASR 9000', device_type: 'router', ports_sfp_plus: 20, ports_qsfp_plus: 4, max_throughput: '480 Gbps', release_year: 2015, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: 'ASR-9006', series: 'ASR 9000', device_type: 'router', max_throughput: '19.2 Tbps', release_year: 2010, status: 'active' },
|
||||||
|
// Cisco 8000
|
||||||
|
{ vendor_slug: 'cisco', model: '8201-32FH', series: 'Cisco 8000', device_type: 'router', ports_qsfp_dd: 32, max_throughput: '12.8 Tbps', release_year: 2020, status: 'active' },
|
||||||
|
{ vendor_slug: 'cisco', model: '8111-32EH', series: 'Cisco 8000', device_type: 'router', ports_qsfp_dd: 32, max_throughput: '12.8 Tbps', release_year: 2023, status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// JUNIPER
|
||||||
|
// ============================================================
|
||||||
|
{ vendor_slug: 'juniper', model: 'QFX5120-48T', series: 'QFX5120', device_type: 'switch', ports_rj45: 48, ports_sfp28: 4, ports_qsfp28: 2, max_throughput: '1.76 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
{ vendor_slug: 'juniper', model: 'QFX5120-48Y', series: 'QFX5120', device_type: 'switch', ports_sfp28: 48, ports_qsfp28: 8, max_throughput: '3.6 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
{ vendor_slug: 'juniper', model: 'QFX5130-32CD', series: 'QFX5130', device_type: 'switch', ports_qsfp_dd: 32, max_throughput: '12.8 Tbps', release_year: 2022, status: 'active' },
|
||||||
|
{ vendor_slug: 'juniper', model: 'QFX5220-32CD', series: 'QFX5220', device_type: 'switch', ports_qsfp_dd: 32, max_throughput: '12.8 Tbps', release_year: 2021, status: 'active' },
|
||||||
|
{ vendor_slug: 'juniper', model: 'QFX5700-96S', series: 'QFX5700', device_type: 'switch', ports_sfp28: 96, ports_qsfp28: 8, max_throughput: '6.4 Tbps', release_year: 2020, status: 'active' },
|
||||||
|
{ vendor_slug: 'juniper', model: 'EX4400-48T', series: 'EX4400', device_type: 'switch', ports_rj45: 48, ports_sfp28: 4, max_throughput: '1.04 Tbps', release_year: 2021, status: 'active' },
|
||||||
|
{ vendor_slug: 'juniper', model: 'EX4400-24P', series: 'EX4400', device_type: 'switch', ports_rj45: 24, ports_sfp28: 4, max_throughput: '520 Gbps', release_year: 2021, status: 'active' },
|
||||||
|
{ vendor_slug: 'juniper', model: 'EX4650-48Y-AFO', series: 'EX4650', device_type: 'switch', ports_sfp28: 48, ports_qsfp28: 8, max_throughput: '3.2 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
{ vendor_slug: 'juniper', model: 'MX204', series: 'MX Series', device_type: 'router', ports_sfp28: 4, ports_qsfp28: 8, max_throughput: '400 Gbps', release_year: 2017, status: 'active' },
|
||||||
|
{ vendor_slug: 'juniper', model: 'MX304', series: 'MX Series', device_type: 'router', max_throughput: '4.8 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
{ vendor_slug: 'juniper', model: 'PTX10001-36MR', series: 'PTX10000', device_type: 'router', ports_qsfp_dd: 36, max_throughput: '14.4 Tbps', release_year: 2021, status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ARISTA
|
||||||
|
// ============================================================
|
||||||
|
{ vendor_slug: 'arista', model: 'DCS-7050CX3-32S', series: '7050X3', device_type: 'switch', ports_sfp28: 32, ports_qsfp28: 2, max_throughput: '3.2 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
{ vendor_slug: 'arista', model: 'DCS-7050SX3-48YC12', series: '7050X3', device_type: 'switch', ports_sfp28: 48, ports_qsfp28: 12, max_throughput: '6.4 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
{ vendor_slug: 'arista', model: 'DCS-7060DX5-32', series: '7060X5', device_type: 'switch', ports_qsfp_dd: 32, max_throughput: '25.6 Tbps', release_year: 2023, status: 'active' },
|
||||||
|
{ vendor_slug: 'arista', model: 'DCS-7060DX5-64S', series: '7060X5', device_type: 'switch', ports_qsfp_dd: 64, max_throughput: '51.2 Tbps', release_year: 2023, status: 'active' },
|
||||||
|
{ vendor_slug: 'arista', model: 'DCS-7280CR3-32P4', series: '7280R3', device_type: 'switch', ports_qsfp28: 32, ports_qsfp_dd: 4, max_throughput: '6.4 Tbps', release_year: 2020, status: 'active' },
|
||||||
|
{ vendor_slug: 'arista', model: 'DCS-7280DR3-24', series: '7280R3', device_type: 'switch', ports_qsfp_dd: 24, max_throughput: '9.6 Tbps', release_year: 2021, status: 'active' },
|
||||||
|
{ vendor_slug: 'arista', model: 'DCS-7800R3-36D', series: '7800R3', device_type: 'switch', ports_qsfp_dd: 36, max_throughput: '460.8 Tbps', release_year: 2020, status: 'active' },
|
||||||
|
{ vendor_slug: 'arista', model: 'DCS-7170-64C', series: '7170', device_type: 'switch', ports_qsfp28: 64, max_throughput: '6.4 Tbps', release_year: 2018, status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HPE / ARUBA
|
||||||
|
// ============================================================
|
||||||
|
{ vendor_slug: 'hpe', model: 'JL661A', series: 'Aruba 6300M', device_type: 'switch', ports_rj45: 48, ports_sfp_plus: 4, max_throughput: '296 Gbps', release_year: 2020, status: 'active' },
|
||||||
|
{ vendor_slug: 'hpe', model: 'JL658A', series: 'Aruba 6300F', device_type: 'switch', ports_sfp_plus: 48, ports_sfp28: 4, max_throughput: '960 Gbps', release_year: 2020, status: 'active' },
|
||||||
|
{ vendor_slug: 'hpe', model: 'R8S89A', series: 'Aruba CX 10000', device_type: 'switch', ports_sfp28: 48, ports_qsfp28: 8, max_throughput: '3.6 Tbps', release_year: 2022, status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// DELL
|
||||||
|
// ============================================================
|
||||||
|
{ vendor_slug: 'dell', model: 'S5248F-ON', series: 'PowerSwitch S5200', device_type: 'switch', ports_sfp28: 48, ports_qsfp28: 4, ports_qsfp_dd: 2, max_throughput: '3.6 Tbps', release_year: 2020, status: 'active' },
|
||||||
|
{ vendor_slug: 'dell', model: 'S5296F-ON', series: 'PowerSwitch S5200', device_type: 'switch', ports_sfp28: 96, ports_qsfp28: 8, max_throughput: '6.4 Tbps', release_year: 2020, status: 'active' },
|
||||||
|
{ vendor_slug: 'dell', model: 'Z9664F-ON', series: 'PowerSwitch Z9664', device_type: 'switch', ports_qsfp_dd: 64, max_throughput: '51.2 Tbps', release_year: 2023, status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// EDGECORE (WHITE-BOX)
|
||||||
|
// ============================================================
|
||||||
|
{ vendor_slug: 'edgecore', model: 'AS7726-32X', series: 'AS7726', device_type: 'switch', ports_qsfp28: 32, ports_sfp_plus: 2, max_throughput: '6.4 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
{ vendor_slug: 'edgecore', model: 'AS9516-32D', series: 'AS9516', device_type: 'switch', ports_qsfp_dd: 32, max_throughput: '12.8 Tbps', release_year: 2021, status: 'active' },
|
||||||
|
{ vendor_slug: 'edgecore', model: 'AS7946-74XKSB', series: 'AS7946', device_type: 'switch', ports_osfp: 74, max_throughput: '51.2 Tbps', release_year: 2024, status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MIKROTIK
|
||||||
|
// ============================================================
|
||||||
|
{ vendor_slug: 'mikrotik', model: 'CRS326-24G-2S+IN', series: 'CRS326', device_type: 'switch', ports_rj45: 24, ports_sfp_plus: 2, max_throughput: '68 Gbps', release_year: 2018, status: 'active' },
|
||||||
|
{ vendor_slug: 'mikrotik', model: 'CRS354-48G-4S+2Q+', series: 'CRS354', device_type: 'switch', ports_rj45: 48, ports_sfp_plus: 4, ports_qsfp_plus: 2, max_throughput: '336 Gbps', release_year: 2020, status: 'active' },
|
||||||
|
{ vendor_slug: 'mikrotik', model: 'CCR2216-1G-12XS-2XQ', series: 'CCR2216', device_type: 'router', ports_rj45: 1, ports_sfp28: 12, ports_qsfp28: 2, max_throughput: '600 Gbps', release_year: 2022, status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// FORTINET
|
||||||
|
// ============================================================
|
||||||
|
{ vendor_slug: 'fortinet', model: 'FG-600F', series: 'FortiGate 600F', device_type: 'firewall', ports_rj45: 16, ports_sfp_plus: 8, ports_sfp28: 4, max_throughput: '600 Gbps', release_year: 2022, status: 'active' },
|
||||||
|
{ vendor_slug: 'fortinet', model: 'FG-3700F', series: 'FortiGate 3700F', device_type: 'firewall', ports_rj45: 4, ports_sfp28: 16, ports_qsfp28: 8, max_throughput: '1.2 Tbps', release_year: 2022, status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// PALO ALTO
|
||||||
|
// ============================================================
|
||||||
|
{ vendor_slug: 'paloalto', model: 'PA-5450', series: 'PA-5400', device_type: 'firewall', ports_rj45: 2, ports_sfp_plus: 8, ports_qsfp28: 4, max_throughput: '150 Gbps', release_year: 2022, status: 'active' },
|
||||||
|
{ vendor_slug: 'paloalto', model: 'PA-7500', series: 'PA-7500', device_type: 'firewall', ports_qsfp_dd: 24, max_throughput: '1.5 Tbps', release_year: 2024, status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// NVIDIA / MELLANOX
|
||||||
|
// ============================================================
|
||||||
|
{ vendor_slug: 'mellanox', model: 'SN5600', series: 'Spectrum-4', device_type: 'switch', ports_osfp: 64, max_throughput: '51.2 Tbps', release_year: 2023, status: 'active' },
|
||||||
|
{ vendor_slug: 'mellanox', model: 'SN4700', series: 'Spectrum-3', device_type: 'switch', ports_qsfp_dd: 32, max_throughput: '12.8 Tbps', release_year: 2021, status: 'active' },
|
||||||
|
{ vendor_slug: 'mellanox', model: 'SN3700', series: 'Spectrum-2', device_type: 'switch', ports_qsfp28: 32, max_throughput: '6.4 Tbps', release_year: 2019, status: 'active' },
|
||||||
|
{ vendor_slug: 'mellanox', model: 'QM9700', series: 'Quantum-2', device_type: 'switch', ports_osfp: 64, max_throughput: '51.2 Tbps', release_year: 2023, status: 'active' },
|
||||||
|
];
|
||||||
68
seed/standards.ts
Normal file
68
seed/standards.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import type { Standard } from '../src/types/index.js';
|
||||||
|
|
||||||
|
export const standards: Standard[] = [
|
||||||
|
// IEEE 802.3 Standards
|
||||||
|
{ name: 'IEEE 802.3z', body: 'IEEE', version: '1998', year: 1998, description: 'Gigabit Ethernet over fiber (1000BASE-SX/LX/CX)' },
|
||||||
|
{ name: 'IEEE 802.3ae', body: 'IEEE', version: '2002', year: 2002, description: '10 Gigabit Ethernet (10GBASE-SR/LR/ER/SW/LW/EW)' },
|
||||||
|
{ name: 'IEEE 802.3ak', body: 'IEEE', version: '2004', year: 2004, description: '10GBASE-CX4 copper' },
|
||||||
|
{ name: 'IEEE 802.3an', body: 'IEEE', version: '2006', year: 2006, description: '10GBASE-T copper' },
|
||||||
|
{ name: 'IEEE 802.3aq', body: 'IEEE', version: '2006', year: 2006, description: '10GBASE-LRM (long reach multimode)' },
|
||||||
|
{ name: 'IEEE 802.3ba', body: 'IEEE', version: '2010', year: 2010, description: '40 Gigabit and 100 Gigabit Ethernet' },
|
||||||
|
{ name: 'IEEE 802.3bj', body: 'IEEE', version: '2014', year: 2014, description: '100GBASE-CR4/KR4/SR4' },
|
||||||
|
{ name: 'IEEE 802.3bm', body: 'IEEE', version: '2015', year: 2015, description: '40GBASE-ER4, 100GBASE-SR4 (improved)' },
|
||||||
|
{ name: 'IEEE 802.3by', body: 'IEEE', version: '2016', year: 2016, description: '25 Gigabit Ethernet' },
|
||||||
|
{ name: 'IEEE 802.3bs', body: 'IEEE', version: '2017', year: 2017, description: '200 Gigabit and 400 Gigabit Ethernet' },
|
||||||
|
{ name: 'IEEE 802.3cd', body: 'IEEE', version: '2018', year: 2018, description: '50 Gbps per lane, 50/100/200 GbE' },
|
||||||
|
{ name: 'IEEE 802.3cm', body: 'IEEE', version: '2020', year: 2020, description: '400GBASE-SR4.2 (multimode)' },
|
||||||
|
{ name: 'IEEE 802.3cn', body: 'IEEE', version: '2019', year: 2019, description: '50/100/200GBASE-ER (extended reach)' },
|
||||||
|
{ name: 'IEEE 802.3ct', body: 'IEEE', version: '2021', year: 2021, description: '100GBASE-ZR (coherent, 80km)' },
|
||||||
|
{ name: 'IEEE 802.3cu', body: 'IEEE', version: '2021', year: 2021, description: '100/400GBASE-FR/LR single-lambda' },
|
||||||
|
{ name: 'IEEE 802.3cw', body: 'IEEE', version: '2023', year: 2023, description: '400GBASE-ZR (coherent, 80km)' },
|
||||||
|
{ name: 'IEEE 802.3db', body: 'IEEE', version: '2022', year: 2022, description: '100G/200G/400G short reach over multimode' },
|
||||||
|
{ name: 'IEEE 802.3df', body: 'IEEE', version: '2024', year: 2024, description: '800GbE and 1.6TbE (200G per lane)' },
|
||||||
|
|
||||||
|
// SFF / SNIA Standards
|
||||||
|
{ name: 'SFF-8024', body: 'SNIA/SFF', version: 'Rev 4.11', year: 2023, description: 'SFF Cross-Reference to Industry Products (transceiver type codes)', url: 'https://www.snia.org/technology-communities/sff/specifications' },
|
||||||
|
{ name: 'SFF-8431', body: 'SNIA/SFF', version: 'Rev 4.1', year: 2009, description: 'SFP+ electrical specification' },
|
||||||
|
{ name: 'SFF-8432', body: 'SNIA/SFF', version: 'Rev 5.0', year: 2007, description: 'SFP+ mechanical specification' },
|
||||||
|
{ name: 'SFF-8436', body: 'SNIA/SFF', version: 'Rev 4.1', year: 2013, description: 'QSFP+ specification' },
|
||||||
|
{ name: 'SFF-8472', body: 'SNIA/SFF', version: 'Rev 12.4', year: 2021, description: 'SFP/SFP+ Digital Diagnostic Monitoring (DDM)' },
|
||||||
|
{ name: 'SFF-8636', body: 'SNIA/SFF', version: 'Rev 2.11', year: 2019, description: 'QSFP+/QSFP28 management interface' },
|
||||||
|
{ name: 'SFF-8665', body: 'SNIA/SFF', version: 'Rev 2.0', year: 2016, description: 'QSFP+ 28 Gbps 4X specification' },
|
||||||
|
{ name: 'INF-8074i', body: 'SNIA/SFF', version: 'Rev 1.0', year: 2001, description: 'SFP Transceiver specification' },
|
||||||
|
|
||||||
|
// CMIS
|
||||||
|
{ name: 'CMIS 4.0', body: 'OSFP-MSA', version: '4.0', year: 2019, description: 'Common Management Interface Specification (QSFP-DD/OSFP)' },
|
||||||
|
{ name: 'CMIS 5.0', body: 'OSFP-MSA', version: '5.0', year: 2021, description: 'CMIS with CDB, VDM, CMIS 5.0' },
|
||||||
|
{ name: 'CMIS 5.2', body: 'OSFP-MSA', version: '5.2', year: 2023, description: 'Latest CMIS with improved thermal management' },
|
||||||
|
|
||||||
|
// MSA Standards
|
||||||
|
{ name: 'QSFP-DD MSA', body: 'QSFP-DD MSA', version: '6.01', year: 2022, description: 'QSFP-DD hardware specification', url: 'https://www.qsfp-dd.com' },
|
||||||
|
{ name: 'OSFP MSA', body: 'OSFP-MSA', version: '4.0', year: 2022, description: 'Octal SFP hardware specification', url: 'https://osfpmsa.org' },
|
||||||
|
{ name: 'QSFP28 MSA', body: 'QSFP-MSA', version: '2.0', year: 2015, description: 'QSFP28 100G hardware specification' },
|
||||||
|
{ name: 'CFP MSA', body: 'CFP-MSA', version: '2.0', year: 2010, description: 'C Form-factor Pluggable', url: 'https://www.cfp-msa.org' },
|
||||||
|
{ name: 'CFP2 MSA', body: 'CFP-MSA', version: '1.0', year: 2013, description: 'CFP2 form factor specification' },
|
||||||
|
{ name: 'CFP4 MSA', body: 'CFP-MSA', version: '1.0', year: 2014, description: 'CFP4 form factor specification' },
|
||||||
|
{ name: 'SFP-DD MSA', body: 'SFP-DD MSA', version: '1.0', year: 2019, description: 'SFP-DD dual-density specification' },
|
||||||
|
|
||||||
|
// ITU-T
|
||||||
|
{ name: 'ITU-T G.694.1', body: 'ITU-T', version: '2020', year: 2020, description: 'DWDM frequency grid (spectral grids for WDM)', url: 'https://www.itu.int/rec/T-REC-G.694.1' },
|
||||||
|
{ name: 'ITU-T G.694.2', body: 'ITU-T', version: '2003', year: 2003, description: 'CWDM wavelength grid' },
|
||||||
|
{ name: 'ITU-T G.698.2', body: 'ITU-T', version: '2018', year: 2018, description: 'Amplified multichannel DWDM applications' },
|
||||||
|
{ name: 'ITU-T G.709', body: 'ITU-T', version: '2020', year: 2020, description: 'OTN (Optical Transport Network) interfaces' },
|
||||||
|
{ name: 'ITU-T G.652', body: 'ITU-T', version: '2016', year: 2016, description: 'Standard single-mode fiber' },
|
||||||
|
{ name: 'ITU-T G.655', body: 'ITU-T', version: '2009', year: 2009, description: 'Non-zero dispersion-shifted fiber' },
|
||||||
|
{ name: 'ITU-T G.657', body: 'ITU-T', version: '2016', year: 2016, description: 'Bend-insensitive single-mode fiber' },
|
||||||
|
|
||||||
|
// OIF
|
||||||
|
{ name: 'OIF-400ZR', body: 'OIF', version: '1.0', year: 2020, description: '400G ZR coherent DWDM pluggable (DP-16QAM)', url: 'https://www.oiforum.com/technical-work/hot-topics/400zr/' },
|
||||||
|
{ name: 'OIF-800ZR', body: 'OIF', version: '0.6', year: 2024, description: '800G ZR coherent (draft, expected 2025)' },
|
||||||
|
{ name: 'OIF-CEI-112G', body: 'OIF', version: '1.0', year: 2023, description: '112 Gbps per lane electrical interface' },
|
||||||
|
|
||||||
|
// OpenZR+
|
||||||
|
{ name: 'OpenZR+', body: 'OpenZR+ MSA', version: '2.0', year: 2022, description: 'Extended reach coherent (400ZR+ to 1000km+)', url: 'https://openzrplus.org' },
|
||||||
|
|
||||||
|
// CWDM/SWDM
|
||||||
|
{ name: 'CWDM4 MSA', body: 'CWDM4 MSA', version: '2.0', year: 2015, description: '100G CWDM4 (4x25G, 2km SMF)' },
|
||||||
|
{ name: 'SWDM Alliance', body: 'SWDM', version: '1.0', year: 2017, description: 'Short Wavelength Division Multiplexing (MMF)' },
|
||||||
|
];
|
||||||
243
seed/transceivers.ts
Normal file
243
seed/transceivers.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
// Comprehensive Transceiver Seed Data
|
||||||
|
// Covers all major categories: SFP, SFP+, SFP28, QSFP+, QSFP28, QSFP-DD, OSFP, CWDM, DWDM, DAC, AOC, BiDi, Coherent
|
||||||
|
// Vendor references use slugs (resolved at seed time)
|
||||||
|
|
||||||
|
interface SeedTransceiver {
|
||||||
|
part_number: string;
|
||||||
|
vendor_slug: string;
|
||||||
|
form_factor: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
subcategory: string;
|
||||||
|
data_rate: number;
|
||||||
|
data_rate_unit: 'Mbps' | 'Gbps' | 'Tbps';
|
||||||
|
max_reach: number;
|
||||||
|
reach_unit: 'm' | 'km';
|
||||||
|
wavelength_nm: number;
|
||||||
|
wavelength_rx?: number;
|
||||||
|
connector: string;
|
||||||
|
fiber_type: string;
|
||||||
|
duplex: boolean;
|
||||||
|
temp_range: string;
|
||||||
|
dom_support: string;
|
||||||
|
oem_part_number?: string;
|
||||||
|
oem_vendor_slug?: string;
|
||||||
|
description?: string;
|
||||||
|
tags: string[];
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const transceivers: SeedTransceiver[] = [
|
||||||
|
// ============================================================
|
||||||
|
// 1G SFP — COPPER
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'GLC-T', vendor_slug: 'cisco', form_factor: 'SFP', name: 'Cisco 1000BASE-T SFP', category: 'SFP', subcategory: 'T', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 0, connector: 'copper_rj45', fiber_type: 'copper', duplex: true, temp_range: 'commercial', dom_support: 'none', description: '1G copper SFP for Cat5e/6', tags: ['1g', 'copper', 'rj45'], status: 'active' },
|
||||||
|
{ part_number: 'GLC-TE', vendor_slug: 'cisco', form_factor: 'SFP', name: 'Cisco 1000BASE-T SFP (extended temp)', category: 'SFP', subcategory: 'T', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 0, connector: 'copper_rj45', fiber_type: 'copper', duplex: true, temp_range: 'extended', dom_support: 'none', tags: ['1g', 'copper', 'rj45', 'extended-temp'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-GE-T', vendor_slug: 'juniper', form_factor: 'SFP', name: 'Juniper 1000BASE-T SFP', category: 'SFP', subcategory: 'T', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 0, connector: 'copper_rj45', fiber_type: 'copper', duplex: true, temp_range: 'commercial', dom_support: 'none', tags: ['1g', 'copper'], status: 'active' },
|
||||||
|
{ part_number: 'J8177C', vendor_slug: 'hpe', form_factor: 'SFP', name: 'HPE 1000BASE-T SFP', category: 'SFP', subcategory: 'T', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 0, connector: 'copper_rj45', fiber_type: 'copper', duplex: true, temp_range: 'commercial', dom_support: 'none', tags: ['1g', 'copper'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 1G SFP — FIBER
|
||||||
|
// ============================================================
|
||||||
|
// SX (850nm, MMF)
|
||||||
|
{ part_number: 'GLC-SX-MMD', vendor_slug: 'cisco', form_factor: 'SFP', name: 'Cisco 1000BASE-SX SFP', category: 'SFP', subcategory: 'SX', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 550, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om3', duplex: true, temp_range: 'commercial', dom_support: 'ddm', tags: ['1g', 'sx', 'multimode'], status: 'active' },
|
||||||
|
{ part_number: 'EX-SFP-1GE-SX', vendor_slug: 'juniper', form_factor: 'SFP', name: 'Juniper 1000BASE-SX SFP', category: 'SFP', subcategory: 'SX', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 550, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om3', duplex: true, temp_range: 'commercial', dom_support: 'ddm', tags: ['1g', 'sx', 'multimode'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-1G-SX', vendor_slug: 'arista', form_factor: 'SFP', name: 'Arista 1000BASE-SX SFP', category: 'SFP', subcategory: 'SX', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 550, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om3', duplex: true, temp_range: 'commercial', dom_support: 'ddm', tags: ['1g', 'sx', 'multimode'], status: 'active' },
|
||||||
|
|
||||||
|
// LX (1310nm, SMF)
|
||||||
|
{ part_number: 'GLC-LH-SMD', vendor_slug: 'cisco', form_factor: 'SFP', name: 'Cisco 1000BASE-LX/LH SFP', category: 'SFP', subcategory: 'LX', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'ddm', tags: ['1g', 'lx', 'singlemode'], status: 'active' },
|
||||||
|
{ part_number: 'EX-SFP-1GE-LX', vendor_slug: 'juniper', form_factor: 'SFP', name: 'Juniper 1000BASE-LX SFP', category: 'SFP', subcategory: 'LX', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'ddm', tags: ['1g', 'lx', 'singlemode'], status: 'active' },
|
||||||
|
|
||||||
|
// ZX (1550nm, SMF, 80km)
|
||||||
|
{ part_number: 'GLC-ZX-SMD', vendor_slug: 'cisco', form_factor: 'SFP', name: 'Cisco 1000BASE-ZX SFP', category: 'SFP', subcategory: 'ZX', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1550, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'ddm', tags: ['1g', 'zx', 'long-reach'], status: 'active' },
|
||||||
|
{ part_number: 'EX-SFP-1GE-LX40K', vendor_slug: 'juniper', form_factor: 'SFP', name: 'Juniper 1000BASE-LX40K SFP', category: 'SFP', subcategory: 'EX', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 40, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'ddm', tags: ['1g', 'extended-reach'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 10G SFP+
|
||||||
|
// ============================================================
|
||||||
|
// SR (850nm, MMF)
|
||||||
|
{ part_number: 'SFP-10G-SR', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10GBASE-SR SFP+', category: 'SFP+', subcategory: 'SR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 400, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om4', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'sr', 'multimode', 'datacenter'], status: 'active' },
|
||||||
|
{ part_number: 'EX-SFP-10GE-SR', vendor_slug: 'juniper', form_factor: 'SFP+', name: 'Juniper 10GBASE-SR SFP+', category: 'SFP+', subcategory: 'SR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 400, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om4', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'sr', 'multimode'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-10G-SR', vendor_slug: 'arista', form_factor: 'SFP+', name: 'Arista 10GBASE-SR SFP+', category: 'SFP+', subcategory: 'SR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 400, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om4', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'sr', 'multimode'], status: 'active' },
|
||||||
|
|
||||||
|
// LR (1310nm, SMF, 10km)
|
||||||
|
{ part_number: 'SFP-10G-LR', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10GBASE-LR SFP+', category: 'SFP+', subcategory: 'LR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'lr', 'singlemode'], status: 'active' },
|
||||||
|
{ part_number: 'EX-SFP-10GE-LR', vendor_slug: 'juniper', form_factor: 'SFP+', name: 'Juniper 10GBASE-LR SFP+', category: 'SFP+', subcategory: 'LR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'lr', 'singlemode'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-10G-LR', vendor_slug: 'arista', form_factor: 'SFP+', name: 'Arista 10GBASE-LR SFP+', category: 'SFP+', subcategory: 'LR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'lr', 'singlemode'], status: 'active' },
|
||||||
|
|
||||||
|
// ER (1550nm, SMF, 40km)
|
||||||
|
{ part_number: 'SFP-10G-ER', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10GBASE-ER SFP+', category: 'SFP+', subcategory: 'ER', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 40, reach_unit: 'km', wavelength_nm: 1550, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'er', 'extended-reach'], status: 'active' },
|
||||||
|
|
||||||
|
// ZR (1550nm, SMF, 80km)
|
||||||
|
{ part_number: 'SFP-10G-ZR', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10GBASE-ZR SFP+', category: 'SFP+', subcategory: 'ZR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1550, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'zr', 'long-reach'], status: 'active' },
|
||||||
|
|
||||||
|
// 10G Copper
|
||||||
|
{ part_number: 'SFP-10G-T', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10GBASE-T SFP+', category: 'SFP+', subcategory: 'T', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 30, reach_unit: 'm', wavelength_nm: 0, connector: 'copper_rj45', fiber_type: 'copper', duplex: true, temp_range: 'commercial', dom_support: 'none', tags: ['10g', 'copper', 'rj45'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 25G SFP28
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'SFP-25G-SR-S', vendor_slug: 'cisco', form_factor: 'SFP28', name: 'Cisco 25GBASE-SR SFP28', category: 'SFP28', subcategory: 'SR', data_rate: 25, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om4', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['25g', 'sr', 'multimode', 'datacenter'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-25G-LR-S', vendor_slug: 'cisco', form_factor: 'SFP28', name: 'Cisco 25GBASE-LR SFP28', category: 'SFP28', subcategory: 'LR', data_rate: 25, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['25g', 'lr', 'singlemode'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-25G-SR', vendor_slug: 'arista', form_factor: 'SFP28', name: 'Arista 25GBASE-SR SFP28', category: 'SFP28', subcategory: 'SR', data_rate: 25, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om4', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['25g', 'sr'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 40G QSFP+
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'QSFP-40G-SR4', vendor_slug: 'cisco', form_factor: 'QSFP+', name: 'Cisco 40GBASE-SR4 QSFP+', category: 'QSFP+', subcategory: 'SR4', data_rate: 40, data_rate_unit: 'Gbps', max_reach: 150, reach_unit: 'm', wavelength_nm: 850, connector: 'MPO-12', fiber_type: 'mmf_om4', duplex: false, temp_range: 'commercial', dom_support: 'sff8636', description: '4x10G SR, MPO-12 connector', tags: ['40g', 'sr4', 'parallel-optics', 'mpo'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP-40G-LR4', vendor_slug: 'cisco', form_factor: 'QSFP+', name: 'Cisco 40GBASE-LR4 QSFP+', category: 'QSFP+', subcategory: 'LR4', data_rate: 40, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8636', description: '4x10G LAN-WDM over SMF', tags: ['40g', 'lr4', 'wdm', 'singlemode'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP-40G-SR-BD', vendor_slug: 'cisco', form_factor: 'QSFP+', name: 'Cisco 40GBASE-SR-BD QSFP+ (BiDi)', category: 'QSFP+', subcategory: 'BiDi', data_rate: 40, data_rate_unit: 'Gbps', max_reach: 150, reach_unit: 'm', wavelength_nm: 850, wavelength_rx: 900, connector: 'LC', fiber_type: 'mmf_om4', duplex: true, temp_range: 'commercial', dom_support: 'sff8636', description: '40G BiDi over LC duplex MMF', tags: ['40g', 'bidi', 'multimode', 'lc'], status: 'active' },
|
||||||
|
{ part_number: 'JNP-QSFP-40G-LX4', vendor_slug: 'juniper', form_factor: 'QSFP+', name: 'Juniper 40GBASE-LX4 QSFP+', category: 'QSFP+', subcategory: 'LX4', data_rate: 40, data_rate_unit: 'Gbps', max_reach: 2, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8636', tags: ['40g', 'lx4', 'wdm'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 100G QSFP28
|
||||||
|
// ============================================================
|
||||||
|
// SR4
|
||||||
|
{ part_number: 'QSFP-100G-SR4-S', vendor_slug: 'cisco', form_factor: 'QSFP28', name: 'Cisco 100GBASE-SR4 QSFP28', category: 'QSFP28', subcategory: 'SR4', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'MPO-12', fiber_type: 'mmf_om4', duplex: false, temp_range: 'commercial', dom_support: 'cmis', description: '4x25G SR over MPO-12 (OM4)', tags: ['100g', 'sr4', 'parallel-optics', 'datacenter'], status: 'active' },
|
||||||
|
{ part_number: 'JNP-QSFP-100G-SR4', vendor_slug: 'juniper', form_factor: 'QSFP28', name: 'Juniper 100GBASE-SR4 QSFP28', category: 'QSFP28', subcategory: 'SR4', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'MPO-12', fiber_type: 'mmf_om4', duplex: false, temp_range: 'commercial', dom_support: 'cmis', tags: ['100g', 'sr4'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP-100G-SR4', vendor_slug: 'arista', form_factor: 'QSFP28', name: 'Arista 100GBASE-SR4 QSFP28', category: 'QSFP28', subcategory: 'SR4', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'MPO-12', fiber_type: 'mmf_om4', duplex: false, temp_range: 'commercial', dom_support: 'cmis', tags: ['100g', 'sr4'], status: 'active' },
|
||||||
|
|
||||||
|
// LR4
|
||||||
|
{ part_number: 'QSFP-100G-LR4-S', vendor_slug: 'cisco', form_factor: 'QSFP28', name: 'Cisco 100GBASE-LR4 QSFP28', category: 'QSFP28', subcategory: 'LR4', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', description: '4x25G LAN-WDM over SMF', tags: ['100g', 'lr4', 'wdm', 'singlemode'], status: 'active' },
|
||||||
|
{ part_number: 'JNP-QSFP-100G-LR4', vendor_slug: 'juniper', form_factor: 'QSFP28', name: 'Juniper 100GBASE-LR4 QSFP28', category: 'QSFP28', subcategory: 'LR4', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', tags: ['100g', 'lr4'], status: 'active' },
|
||||||
|
|
||||||
|
// CWDM4
|
||||||
|
{ part_number: 'QSFP-100G-CWDM4-S', vendor_slug: 'cisco', form_factor: 'QSFP28', name: 'Cisco 100G CWDM4 QSFP28', category: 'QSFP28', subcategory: 'CWDM4', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 2, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', description: '100G CWDM4 (4x25G, 2km)', tags: ['100g', 'cwdm4', 'wdm'], status: 'active' },
|
||||||
|
|
||||||
|
// PSM4
|
||||||
|
{ part_number: 'QSFP-100G-PSM4-S', vendor_slug: 'cisco', form_factor: 'QSFP28', name: 'Cisco 100G PSM4 QSFP28', category: 'QSFP28', subcategory: 'PSM4', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'MPO-12', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'cmis', description: '100G PSM4 parallel SMF', tags: ['100g', 'psm4', 'parallel-optics'], status: 'active' },
|
||||||
|
|
||||||
|
// DR (single lambda 100G)
|
||||||
|
{ part_number: 'QSFP-100G-DR-S', vendor_slug: 'cisco', form_factor: 'QSFP28', name: 'Cisco 100GBASE-DR QSFP28', category: 'QSFP28', subcategory: 'DR', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', description: 'Single lambda 100G (IEEE 802.3cu)', tags: ['100g', 'dr', 'single-lambda'], status: 'active' },
|
||||||
|
|
||||||
|
// FR (single lambda 100G, 2km)
|
||||||
|
{ part_number: 'QSFP-100G-FR-S', vendor_slug: 'cisco', form_factor: 'QSFP28', name: 'Cisco 100GBASE-FR QSFP28', category: 'QSFP28', subcategory: 'FR', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 2, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', tags: ['100g', 'fr', 'single-lambda'], status: 'active' },
|
||||||
|
|
||||||
|
// ZR (coherent, 80km)
|
||||||
|
{ part_number: 'QDD-100G-ZR-S', vendor_slug: 'cisco', form_factor: 'QSFP28', name: 'Cisco 100GBASE-ZR QSFP28', category: 'QSFP28', subcategory: 'ZR', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1550, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', description: '100G coherent ZR (IEEE 802.3ct)', tags: ['100g', 'zr', 'coherent', 'dwdm'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 200G QSFP56
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'QDD-200G-FR4-S', vendor_slug: 'cisco', form_factor: 'QSFP56', name: 'Cisco 200G FR4 QSFP56', category: 'QSFP56', subcategory: 'FR4', data_rate: 200, data_rate_unit: 'Gbps', max_reach: 2, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', tags: ['200g', 'fr4', 'wdm'], status: 'active' },
|
||||||
|
{ part_number: 'QDD-200G-SR4', vendor_slug: 'cisco', form_factor: 'QSFP56', name: 'Cisco 200G SR4 QSFP56', category: 'QSFP56', subcategory: 'SR4', data_rate: 200, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'MPO-12', fiber_type: 'mmf_om4', duplex: false, temp_range: 'commercial', dom_support: 'cmis', tags: ['200g', 'sr4', 'parallel-optics'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 400G QSFP-DD
|
||||||
|
// ============================================================
|
||||||
|
// DR4
|
||||||
|
{ part_number: 'QDD-400G-DR4-S', vendor_slug: 'cisco', form_factor: 'QSFP-DD', name: 'Cisco 400GBASE-DR4 QSFP-DD', category: 'QSFP-DD', subcategory: 'DR4', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'MPO-12', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'cmis', description: '4x100G DR over parallel SMF', tags: ['400g', 'dr4', 'parallel-optics', 'datacenter'], status: 'active' },
|
||||||
|
{ part_number: 'JNP-QSFP-DD-400G-DR4', vendor_slug: 'juniper', form_factor: 'QSFP-DD', name: 'Juniper 400GBASE-DR4 QSFP-DD', category: 'QSFP-DD', subcategory: 'DR4', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'MPO-12', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'cmis', tags: ['400g', 'dr4'], status: 'active' },
|
||||||
|
{ part_number: 'QDD-400G-DR4', vendor_slug: 'arista', form_factor: 'QSFP-DD', name: 'Arista 400GBASE-DR4 QSFP-DD', category: 'QSFP-DD', subcategory: 'DR4', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'MPO-12', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'cmis', tags: ['400g', 'dr4'], status: 'active' },
|
||||||
|
|
||||||
|
// FR4
|
||||||
|
{ part_number: 'QDD-400G-FR4-S', vendor_slug: 'cisco', form_factor: 'QSFP-DD', name: 'Cisco 400GBASE-FR4 QSFP-DD', category: 'QSFP-DD', subcategory: 'FR4', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 2, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', description: '4x100G FR, 2km SMF', tags: ['400g', 'fr4', 'wdm'], status: 'active' },
|
||||||
|
|
||||||
|
// LR4
|
||||||
|
{ part_number: 'QDD-400G-LR4-S', vendor_slug: 'cisco', form_factor: 'QSFP-DD', name: 'Cisco 400GBASE-LR4 QSFP-DD', category: 'QSFP-DD', subcategory: 'LR4', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', tags: ['400g', 'lr4', 'wdm'], status: 'active' },
|
||||||
|
|
||||||
|
// SR4.2
|
||||||
|
{ part_number: 'QDD-400G-SR4.2', vendor_slug: 'cisco', form_factor: 'QSFP-DD', name: 'Cisco 400GBASE-SR4.2 QSFP-DD', category: 'QSFP-DD', subcategory: 'SR4.2', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'MPO-12', fiber_type: 'mmf_om4', duplex: false, temp_range: 'commercial', dom_support: 'cmis', description: '400G over multimode (IEEE 802.3cm)', tags: ['400g', 'sr4.2', 'multimode'], status: 'active' },
|
||||||
|
|
||||||
|
// ZR (coherent, OIF 400ZR)
|
||||||
|
{ part_number: 'QDD-400G-ZR-S', vendor_slug: 'cisco', form_factor: 'QSFP-DD', name: 'Cisco 400G ZR QSFP-DD', category: 'QSFP-DD', subcategory: 'ZR', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1550, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', description: '400G coherent ZR (OIF 400ZR, DP-16QAM)', tags: ['400g', 'zr', 'coherent', 'dwdm', 'dci'], status: 'active' },
|
||||||
|
|
||||||
|
// ZR+ (OpenZR+)
|
||||||
|
{ part_number: 'QDD-400G-ZRP-S', vendor_slug: 'cisco', form_factor: 'QSFP-DD', name: 'Cisco 400G ZR+ QSFP-DD', category: 'QSFP-DD', subcategory: 'ZR+', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 1000, reach_unit: 'km', wavelength_nm: 1550, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', description: '400G OpenZR+ coherent (1000km+)', tags: ['400g', 'zr+', 'coherent', 'dwdm', 'long-haul'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 400G OSFP
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'OSFP-400G-DR4', vendor_slug: 'arista', form_factor: 'OSFP', name: 'Arista 400G DR4 OSFP', category: 'OSFP', subcategory: 'DR4', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'MPO-12', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'cmis', tags: ['400g', 'dr4', 'osfp'], status: 'active' },
|
||||||
|
{ part_number: 'OSFP-400G-FR4', vendor_slug: 'arista', form_factor: 'OSFP', name: 'Arista 400G FR4 OSFP', category: 'OSFP', subcategory: 'FR4', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 2, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', tags: ['400g', 'fr4', 'osfp'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 800G QSFP-DD800 / OSFP
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'QDD-800G-DR8', vendor_slug: 'cisco', form_factor: 'QSFP-DD800', name: 'Cisco 800G DR8 QSFP-DD800', category: 'QSFP-DD800', subcategory: 'DR8', data_rate: 800, data_rate_unit: 'Gbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'MPO-16', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'cmis', description: '8x100G DR parallel SMF', tags: ['800g', 'dr8', 'ai', 'next-gen'], status: 'active' },
|
||||||
|
{ part_number: 'QDD-800G-DR4', vendor_slug: 'cisco', form_factor: 'QSFP-DD800', name: 'Cisco 800G DR4 QSFP-DD800', category: 'QSFP-DD800', subcategory: 'DR4', data_rate: 800, data_rate_unit: 'Gbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'MPO-12', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'cmis', description: '4x200G DR (200G per lane)', tags: ['800g', 'dr4', 'ai'], status: 'pre_release' },
|
||||||
|
{ part_number: 'OSFP-800G-DR8', vendor_slug: 'arista', form_factor: 'OSFP', name: 'Arista 800G DR8 OSFP', category: 'OSFP', subcategory: 'DR8', data_rate: 800, data_rate_unit: 'Gbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'MPO-16', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'cmis', tags: ['800g', 'dr8', 'osfp', 'ai'], status: 'active' },
|
||||||
|
{ part_number: 'QDD-800G-ZR', vendor_slug: 'cisco', form_factor: 'QSFP-DD800', name: 'Cisco 800G ZR QSFP-DD800', category: 'QSFP-DD800', subcategory: 'ZR', data_rate: 800, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1550, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', description: '800G coherent ZR (OIF 800ZR)', tags: ['800g', 'zr', 'coherent', 'dci'], status: 'pre_release' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 1.6T OSFP-XD
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'OSFP-XD-1.6T-DR8', vendor_slug: 'cisco', form_factor: 'OSFP-XD', name: 'Cisco 1.6T DR8 OSFP-XD', category: 'OSFP-XD', subcategory: 'DR8', data_rate: 1.6, data_rate_unit: 'Tbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'MPO-16', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'cmis', description: '8x200G DR (IEEE 802.3df)', tags: ['1.6t', 'dr8', 'ai', 'next-gen'], status: 'pre_release' },
|
||||||
|
{ part_number: 'OSFP-XD-1.6T-FR8', vendor_slug: 'arista', form_factor: 'OSFP-XD', name: 'Arista 1.6T FR8 OSFP-XD', category: 'OSFP-XD', subcategory: 'FR8', data_rate: 1.6, data_rate_unit: 'Tbps', max_reach: 2, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'cmis', tags: ['1.6t', 'fr8', 'ai'], status: 'pre_release' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// CWDM SFP/SFP+ (1270-1610nm)
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'CWDM-SFP10G-1270', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1270nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 40, reach_unit: 'km', wavelength_nm: 1270, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1270nm'], status: 'active' },
|
||||||
|
{ part_number: 'CWDM-SFP10G-1290', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1290nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 40, reach_unit: 'km', wavelength_nm: 1290, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1290nm'], status: 'active' },
|
||||||
|
{ part_number: 'CWDM-SFP10G-1310', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1310nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 40, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1310nm'], status: 'active' },
|
||||||
|
{ part_number: 'CWDM-SFP10G-1330', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1330nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 40, reach_unit: 'km', wavelength_nm: 1330, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1330nm'], status: 'active' },
|
||||||
|
{ part_number: 'CWDM-SFP10G-1470', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1470nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1470, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1470nm'], status: 'active' },
|
||||||
|
{ part_number: 'CWDM-SFP10G-1490', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1490nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1490, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1490nm'], status: 'active' },
|
||||||
|
{ part_number: 'CWDM-SFP10G-1510', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1510nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1510, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1510nm'], status: 'active' },
|
||||||
|
{ part_number: 'CWDM-SFP10G-1550', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1550nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1550, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1550nm'], status: 'active' },
|
||||||
|
{ part_number: 'CWDM-SFP10G-1570', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1570nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1570, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1570nm'], status: 'active' },
|
||||||
|
{ part_number: 'CWDM-SFP10G-1590', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1590nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1590, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1590nm'], status: 'active' },
|
||||||
|
{ part_number: 'CWDM-SFP10G-1610', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G CWDM SFP+ 1610nm', category: 'SFP+', subcategory: 'CWDM', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 80, reach_unit: 'km', wavelength_nm: 1610, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'cwdm', '1610nm'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// BiDi SFP / SFP+
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'GLC-BX-D', vendor_slug: 'cisco', form_factor: 'SFP', name: 'Cisco 1G BiDi SFP (downstream)', category: 'SFP', subcategory: 'BiDi', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1490, wavelength_rx: 1310, connector: 'LC', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'ddm', description: 'TX:1490nm / RX:1310nm, single fiber', tags: ['1g', 'bidi', 'single-fiber'], status: 'active' },
|
||||||
|
{ part_number: 'GLC-BX-U', vendor_slug: 'cisco', form_factor: 'SFP', name: 'Cisco 1G BiDi SFP (upstream)', category: 'SFP', subcategory: 'BiDi', data_rate: 1, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, wavelength_rx: 1490, connector: 'LC', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'ddm', description: 'TX:1310nm / RX:1490nm, single fiber', tags: ['1g', 'bidi', 'single-fiber'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-10G-BXD-I', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G BiDi SFP+ (downstream)', category: 'SFP+', subcategory: 'BiDi', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1330, wavelength_rx: 1270, connector: 'LC', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'bidi', 'single-fiber'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-10G-BXU-I', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 10G BiDi SFP+ (upstream)', category: 'SFP+', subcategory: 'BiDi', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1270, wavelength_rx: 1330, connector: 'LC', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'bidi', 'single-fiber'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// COMPATIBLE VENDOR TRANSCEIVERS (Flexoptix, 10Gtek, FS.com)
|
||||||
|
// ============================================================
|
||||||
|
// Flexoptix
|
||||||
|
{ part_number: 'FL-SFP+SR', vendor_slug: 'flexoptix', form_factor: 'SFP+', name: 'Flexoptix 10G SR SFP+', category: 'SFP+', subcategory: 'SR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 400, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om4', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', oem_part_number: 'SFP-10G-SR', oem_vendor_slug: 'cisco', description: 'Flexoptix compatible, FlexBox codable', tags: ['10g', 'sr', 'compatible', 'flexbox'], status: 'active' },
|
||||||
|
{ part_number: 'FL-SFP+LR', vendor_slug: 'flexoptix', form_factor: 'SFP+', name: 'Flexoptix 10G LR SFP+', category: 'SFP+', subcategory: 'LR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', oem_part_number: 'SFP-10G-LR', oem_vendor_slug: 'cisco', tags: ['10g', 'lr', 'compatible', 'flexbox'], status: 'active' },
|
||||||
|
{ part_number: 'FL-QSFP28-SR4', vendor_slug: 'flexoptix', form_factor: 'QSFP28', name: 'Flexoptix 100G SR4 QSFP28', category: 'QSFP28', subcategory: 'SR4', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'MPO-12', fiber_type: 'mmf_om4', duplex: false, temp_range: 'commercial', dom_support: 'cmis', oem_part_number: 'QSFP-100G-SR4-S', oem_vendor_slug: 'cisco', tags: ['100g', 'sr4', 'compatible', 'flexbox'], status: 'active' },
|
||||||
|
|
||||||
|
// FS.com
|
||||||
|
{ part_number: 'SFP-10GSR-85', vendor_slug: 'fs-com', form_factor: 'SFP+', name: 'FS.com 10G SR SFP+', category: 'SFP+', subcategory: 'SR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 300, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om3', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'sr', 'compatible', 'budget'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-10GLR-31', vendor_slug: 'fs-com', form_factor: 'SFP+', name: 'FS.com 10G LR SFP+', category: 'SFP+', subcategory: 'LR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'km', wavelength_nm: 1310, connector: 'LC', fiber_type: 'smf', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'lr', 'compatible', 'budget'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP28-SR4-100G', vendor_slug: 'fs-com', form_factor: 'QSFP28', name: 'FS.com 100G SR4 QSFP28', category: 'QSFP28', subcategory: 'SR4', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'MPO-12', fiber_type: 'mmf_om4', duplex: false, temp_range: 'commercial', dom_support: 'cmis', tags: ['100g', 'sr4', 'compatible', 'budget'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP-DD-400G-DR4', vendor_slug: 'fs-com', form_factor: 'QSFP-DD', name: 'FS.com 400G DR4 QSFP-DD', category: 'QSFP-DD', subcategory: 'DR4', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 500, reach_unit: 'm', wavelength_nm: 1310, connector: 'MPO-12', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'cmis', tags: ['400g', 'dr4', 'compatible', 'budget'], status: 'active' },
|
||||||
|
|
||||||
|
// 10Gtek
|
||||||
|
{ part_number: 'SFP-10G-SR-TK', vendor_slug: '10gtek', form_factor: 'SFP+', name: '10Gtek 10G SR SFP+', category: 'SFP+', subcategory: 'SR', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 300, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om3', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['10g', 'sr', 'compatible', 'budget'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP28-100G-SR4-TK', vendor_slug: '10gtek', form_factor: 'QSFP28', name: '10Gtek 100G SR4 QSFP28', category: 'QSFP28', subcategory: 'SR4', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'MPO-12', fiber_type: 'mmf_om4', duplex: false, temp_range: 'commercial', dom_support: 'cmis', tags: ['100g', 'sr4', 'compatible', 'budget'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// DAC (Direct Attach Copper)
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'SFP-H10GB-CU1M', vendor_slug: 'cisco', form_factor: 'SFP+-DAC', name: 'Cisco 10G SFP+ DAC 1m', category: 'SFP+-DAC', subcategory: 'DAC', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 1, reach_unit: 'm', wavelength_nm: 0, connector: 'dac_passive', fiber_type: 'dac', duplex: true, temp_range: 'commercial', dom_support: 'none', tags: ['10g', 'dac', 'passive', '1m'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-H10GB-CU3M', vendor_slug: 'cisco', form_factor: 'SFP+-DAC', name: 'Cisco 10G SFP+ DAC 3m', category: 'SFP+-DAC', subcategory: 'DAC', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 3, reach_unit: 'm', wavelength_nm: 0, connector: 'dac_passive', fiber_type: 'dac', duplex: true, temp_range: 'commercial', dom_support: 'none', tags: ['10g', 'dac', 'passive', '3m'], status: 'active' },
|
||||||
|
{ part_number: 'SFP-H10GB-CU5M', vendor_slug: 'cisco', form_factor: 'SFP+-DAC', name: 'Cisco 10G SFP+ DAC 5m', category: 'SFP+-DAC', subcategory: 'DAC', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 5, reach_unit: 'm', wavelength_nm: 0, connector: 'dac_passive', fiber_type: 'dac', duplex: true, temp_range: 'commercial', dom_support: 'none', tags: ['10g', 'dac', 'passive', '5m'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP-H40G-CU1M', vendor_slug: 'cisco', form_factor: 'QSFP+', name: 'Cisco 40G QSFP+ DAC 1m', category: 'QSFP+', subcategory: 'DAC', data_rate: 40, data_rate_unit: 'Gbps', max_reach: 1, reach_unit: 'm', wavelength_nm: 0, connector: 'dac_passive', fiber_type: 'dac', duplex: true, temp_range: 'commercial', dom_support: 'none', tags: ['40g', 'dac', 'passive'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP-100G-CU1M', vendor_slug: 'cisco', form_factor: 'QSFP28-DAC', name: 'Cisco 100G QSFP28 DAC 1m', category: 'QSFP28-DAC', subcategory: 'DAC', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 1, reach_unit: 'm', wavelength_nm: 0, connector: 'dac_passive', fiber_type: 'dac', duplex: true, temp_range: 'commercial', dom_support: 'none', tags: ['100g', 'dac', 'passive'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP-100G-CU3M', vendor_slug: 'cisco', form_factor: 'QSFP28-DAC', name: 'Cisco 100G QSFP28 DAC 3m', category: 'QSFP28-DAC', subcategory: 'DAC', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 3, reach_unit: 'm', wavelength_nm: 0, connector: 'dac_passive', fiber_type: 'dac', duplex: true, temp_range: 'commercial', dom_support: 'none', tags: ['100g', 'dac', 'passive'], status: 'active' },
|
||||||
|
{ part_number: 'QDD-400-CU1M', vendor_slug: 'cisco', form_factor: 'QSFP-DD-DAC', name: 'Cisco 400G QSFP-DD DAC 1m', category: 'QSFP-DD-DAC', subcategory: 'DAC', data_rate: 400, data_rate_unit: 'Gbps', max_reach: 1, reach_unit: 'm', wavelength_nm: 0, connector: 'dac_passive', fiber_type: 'dac', duplex: true, temp_range: 'commercial', dom_support: 'none', tags: ['400g', 'dac', 'passive'], status: 'active' },
|
||||||
|
|
||||||
|
// Breakout DACs
|
||||||
|
{ part_number: 'QSFP-4SFP10G-CU1M', vendor_slug: 'cisco', form_factor: 'QSFP+', name: 'Cisco 40G→4x10G Breakout DAC 1m', category: 'QSFP+', subcategory: 'Breakout-DAC', data_rate: 40, data_rate_unit: 'Gbps', max_reach: 1, reach_unit: 'm', wavelength_nm: 0, connector: 'dac_passive', fiber_type: 'dac', duplex: true, temp_range: 'commercial', dom_support: 'none', description: 'QSFP+ to 4x SFP+ breakout', tags: ['40g', 'breakout', '4x10g', 'dac'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP-4SFP25G-CU1M', vendor_slug: 'cisco', form_factor: 'QSFP28', name: 'Cisco 100G→4x25G Breakout DAC 1m', category: 'QSFP28', subcategory: 'Breakout-DAC', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 1, reach_unit: 'm', wavelength_nm: 0, connector: 'dac_passive', fiber_type: 'dac', duplex: true, temp_range: 'commercial', dom_support: 'none', description: 'QSFP28 to 4x SFP28 breakout', tags: ['100g', 'breakout', '4x25g', 'dac'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// AOC (Active Optical Cable)
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'QSFP-H40G-AOC10M', vendor_slug: 'cisco', form_factor: 'QSFP+', name: 'Cisco 40G QSFP+ AOC 10m', category: 'QSFP+', subcategory: 'AOC', data_rate: 40, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'm', wavelength_nm: 850, connector: 'aoc', fiber_type: 'aoc', duplex: true, temp_range: 'commercial', dom_support: 'sff8636', tags: ['40g', 'aoc', '10m'], status: 'active' },
|
||||||
|
{ part_number: 'QSFP-100G-AOC10M', vendor_slug: 'cisco', form_factor: 'QSFP28-AOC', name: 'Cisco 100G QSFP28 AOC 10m', category: 'QSFP28-AOC', subcategory: 'AOC', data_rate: 100, data_rate_unit: 'Gbps', max_reach: 10, reach_unit: 'm', wavelength_nm: 850, connector: 'aoc', fiber_type: 'aoc', duplex: true, temp_range: 'commercial', dom_support: 'cmis', tags: ['100g', 'aoc', '10m'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// FIBRE CHANNEL
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'DS-SFP-FC8G-SW', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 8G FC SFP+ Shortwave', category: 'SFP+', subcategory: 'FC-SW', data_rate: 8, data_rate_unit: 'Gbps', max_reach: 190, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om3', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['8g-fc', 'fibre-channel', 'san'], status: 'active' },
|
||||||
|
{ part_number: 'DS-SFP-FC16G-SW', vendor_slug: 'cisco', form_factor: 'SFP+', name: 'Cisco 16G FC SFP+ Shortwave', category: 'SFP+', subcategory: 'FC-SW', data_rate: 16, data_rate_unit: 'Gbps', max_reach: 125, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om3', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['16g-fc', 'fibre-channel', 'san'], status: 'active' },
|
||||||
|
{ part_number: 'DS-SFP-FC32G-SW', vendor_slug: 'cisco', form_factor: 'SFP28', name: 'Cisco 32G FC SFP28 Shortwave', category: 'SFP28', subcategory: 'FC-SW', data_rate: 32, data_rate_unit: 'Gbps', max_reach: 100, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om4', duplex: true, temp_range: 'commercial', dom_support: 'sff8472', tags: ['32g-fc', 'fibre-channel', 'san'], status: 'active' },
|
||||||
|
{ part_number: 'DS-SFP-FC64G-SW', vendor_slug: 'cisco', form_factor: 'SFP56', name: 'Cisco 64G FC SFP56 Shortwave', category: 'SFP56', subcategory: 'FC-SW', data_rate: 64, data_rate_unit: 'Gbps', max_reach: 50, reach_unit: 'm', wavelength_nm: 850, connector: 'LC', fiber_type: 'mmf_om4', duplex: true, temp_range: 'commercial', dom_support: 'cmis', tags: ['64g-fc', 'fibre-channel', 'san'], status: 'active' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// PON / ACCESS
|
||||||
|
// ============================================================
|
||||||
|
{ part_number: 'SFP-GE-LX-SM1310', vendor_slug: 'huawei', form_factor: 'SFP', name: 'Huawei GPON OLT SFP', category: 'SFP', subcategory: 'GPON', data_rate: 2.5, data_rate_unit: 'Gbps', max_reach: 20, reach_unit: 'km', wavelength_nm: 1490, wavelength_rx: 1310, connector: 'SC', fiber_type: 'smf', duplex: false, temp_range: 'commercial', dom_support: 'ddm', tags: ['gpon', 'olt', 'pon', 'ftth'], status: 'active' },
|
||||||
|
{ part_number: 'XGS-PON-OLT-C+', vendor_slug: 'huawei', form_factor: 'SFP+', name: 'Huawei XGS-PON OLT SFP+ Class C+', category: 'SFP+', subcategory: 'XGS-PON', data_rate: 10, data_rate_unit: 'Gbps', max_reach: 20, reach_unit: 'km', wavelength_nm: 1577, wavelength_rx: 1270, connector: 'SC', fiber_type: 'smf', duplex: false, temp_range: 'industrial', dom_support: 'ddm', tags: ['xgs-pon', 'olt', 'pon', 'ftth', '10g'], status: 'active' },
|
||||||
|
];
|
||||||
96
seed/vendors.ts
Normal file
96
seed/vendors.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import type { Vendor } from '../src/types/index.js';
|
||||||
|
|
||||||
|
export const vendors: Vendor[] = [
|
||||||
|
// ============================================================
|
||||||
|
// OEM VENDORS (Original Equipment Manufacturers)
|
||||||
|
// ============================================================
|
||||||
|
{ name: 'Cisco', slug: 'cisco', vendor_type: 'oem', website: 'https://www.cisco.com', country: 'USA', founded_year: 1984, is_oem: true, description: 'Largest networking equipment vendor globally', scrape_url: 'https://tmgmatrix.cisco.com' },
|
||||||
|
{ name: 'Juniper Networks', slug: 'juniper', vendor_type: 'oem', website: 'https://www.juniper.net', country: 'USA', founded_year: 1996, is_oem: true, description: 'Enterprise and service provider networking', scrape_url: 'https://apps.juniper.net/hct/' },
|
||||||
|
{ name: 'Arista Networks', slug: 'arista', vendor_type: 'oem', website: 'https://www.arista.com', country: 'USA', founded_year: 2004, is_oem: true, description: 'Cloud networking and data center switches', aliases: ['Arista'] },
|
||||||
|
{ name: 'Hewlett Packard Enterprise', slug: 'hpe', vendor_type: 'oem', website: 'https://www.hpe.com', country: 'USA', founded_year: 2015, is_oem: true, description: 'Aruba networking, ProCurve switches', aliases: ['HPE', 'HP', 'HP Networking', 'Aruba', 'ProCurve'] },
|
||||||
|
{ name: 'Dell Technologies', slug: 'dell', vendor_type: 'oem', website: 'https://www.dell.com', country: 'USA', founded_year: 1984, is_oem: true, description: 'PowerSwitch/Force10 networking', aliases: ['Dell', 'Force10', 'Dell EMC'] },
|
||||||
|
{ name: 'Huawei', slug: 'huawei', vendor_type: 'oem', website: 'https://www.huawei.com', country: 'CHN', founded_year: 1987, is_oem: true, description: 'Largest telecom equipment vendor globally' },
|
||||||
|
{ name: 'Nokia', slug: 'nokia', vendor_type: 'oem', website: 'https://www.nokia.com', country: 'FIN', founded_year: 1865, is_oem: true, description: 'Telecom infrastructure, IP/MPLS routers', aliases: ['Nokia Networks', 'Alcatel-Lucent', 'ALU'] },
|
||||||
|
{ name: 'Ciena', slug: 'ciena', vendor_type: 'oem', website: 'https://www.ciena.com', country: 'USA', founded_year: 1992, is_oem: true, description: 'Optical networking and WaveServer coherent' },
|
||||||
|
{ name: 'ADTRAN', slug: 'adtran', vendor_type: 'oem', website: 'https://www.adtran.com', country: 'USA', founded_year: 1985, is_oem: true, description: 'Access networking equipment', aliases: ['ADVA', 'ADVA Optical'] },
|
||||||
|
{ name: 'Extreme Networks', slug: 'extreme', vendor_type: 'oem', website: 'https://www.extremenetworks.com', country: 'USA', founded_year: 1996, is_oem: true, description: 'Campus and data center networking', aliases: ['Extreme', 'Brocade', 'Enterasys'] },
|
||||||
|
{ name: 'MikroTik', slug: 'mikrotik', vendor_type: 'oem', website: 'https://mikrotik.com', country: 'LVA', founded_year: 1996, is_oem: true, description: 'Affordable routers and switches' },
|
||||||
|
{ name: 'Ubiquiti', slug: 'ubiquiti', vendor_type: 'oem', website: 'https://www.ui.com', country: 'USA', founded_year: 2005, is_oem: true, description: 'UniFi networking equipment', aliases: ['UBNT', 'UniFi'] },
|
||||||
|
{ name: 'FS.com', slug: 'fs-com', vendor_type: 'oem', website: 'https://www.fs.com', country: 'CHN', founded_year: 2009, is_oem: true, description: 'Direct-to-customer networking equipment and optics', scrape_url: 'https://www.fs.com/c/transceivers-702', scrape_enabled: true },
|
||||||
|
{ name: 'Mellanox', slug: 'mellanox', vendor_type: 'oem', website: 'https://www.nvidia.com/networking', country: 'USA', founded_year: 1999, is_oem: true, description: 'InfiniBand and Ethernet NICs (now NVIDIA)', aliases: ['NVIDIA Networking', 'NVIDIA Mellanox'] },
|
||||||
|
{ name: 'Broadcom', slug: 'broadcom', vendor_type: 'oem', website: 'https://www.broadcom.com', country: 'USA', founded_year: 1961, is_oem: true, description: 'Switch ASICs (Memory), NICs, optics', aliases: ['Avago', 'Brocade FC'] },
|
||||||
|
{ name: 'Intel', slug: 'intel', vendor_type: 'oem', website: 'https://www.intel.com', country: 'USA', founded_year: 1968, is_oem: true, description: 'Silicon Photonics, Ethernet NICs' },
|
||||||
|
{ name: 'Palo Alto Networks', slug: 'paloalto', vendor_type: 'oem', website: 'https://www.paloaltonetworks.com', country: 'USA', founded_year: 2005, is_oem: true, description: 'Next-gen firewalls requiring SFP+/QSFP28' },
|
||||||
|
{ name: 'Fortinet', slug: 'fortinet', vendor_type: 'oem', website: 'https://www.fortinet.com', country: 'USA', founded_year: 2000, is_oem: true, description: 'FortiGate firewalls with SFP/SFP+ ports' },
|
||||||
|
{ name: 'Check Point', slug: 'checkpoint', vendor_type: 'oem', website: 'https://www.checkpoint.com', country: 'ISR', founded_year: 1993, is_oem: true, description: 'Security gateways with optical ports' },
|
||||||
|
{ name: 'Calix', slug: 'calix', vendor_type: 'oem', website: 'https://www.calix.com', country: 'USA', founded_year: 1999, is_oem: true, description: 'Broadband access (GPON/XGS-PON OLT)' },
|
||||||
|
{ name: 'Edgecore Networks', slug: 'edgecore', vendor_type: 'oem', website: 'https://www.edge-core.com', country: 'TWN', founded_year: 2014, is_oem: true, description: 'Open networking (white-box) switches', aliases: ['Accton'] },
|
||||||
|
{ name: 'Celestica', slug: 'celestica', vendor_type: 'oem', website: 'https://www.celestica.com', country: 'CAN', founded_year: 1994, is_oem: true, description: 'White-box switches (DS3000/DS4000)' },
|
||||||
|
{ name: 'Supermicro', slug: 'supermicro', vendor_type: 'oem', website: 'https://www.supermicro.com', country: 'USA', founded_year: 1993, is_oem: true, description: 'Server and networking hardware' },
|
||||||
|
{ name: 'ZTE', slug: 'zte', vendor_type: 'oem', website: 'https://www.zte.com.cn', country: 'CHN', founded_year: 1985, is_oem: true, description: 'Telecom equipment, OLT/ONU' },
|
||||||
|
{ name: 'FiberHome', slug: 'fiberhome', vendor_type: 'oem', website: 'https://www.fiberhome.com', country: 'CHN', founded_year: 1999, is_oem: true, description: 'Chinese optical networking equipment' },
|
||||||
|
{ name: 'Allied Telesis', slug: 'allied-telesis', vendor_type: 'oem', website: 'https://www.alliedtelesis.com', country: 'JPN', founded_year: 1987, is_oem: true, description: 'Enterprise networking equipment' },
|
||||||
|
{ name: 'Netgear', slug: 'netgear', vendor_type: 'oem', website: 'https://www.netgear.com', country: 'USA', founded_year: 1996, is_oem: true, description: 'SMB networking equipment' },
|
||||||
|
{ name: 'D-Link', slug: 'dlink', vendor_type: 'oem', website: 'https://www.dlink.com', country: 'TWN', founded_year: 1986, is_oem: true, description: 'Consumer and SMB networking' },
|
||||||
|
{ name: 'TP-Link', slug: 'tplink', vendor_type: 'oem', website: 'https://www.tp-link.com', country: 'CHN', founded_year: 1996, is_oem: true, description: 'Consumer and SMB networking' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// TRANSCEIVER MANUFACTURERS (Factories)
|
||||||
|
// ============================================================
|
||||||
|
{ name: 'II-VI / Coherent', slug: 'coherent', vendor_type: 'manufacturer', website: 'https://www.coherent.com', country: 'USA', founded_year: 1971, is_factory: true, description: 'Coherent optics, VCSELs, WDM filters', aliases: ['II-VI', 'Finisar', 'II-VI Finisar'] },
|
||||||
|
{ name: 'Lumentum', slug: 'lumentum', vendor_type: 'manufacturer', website: 'https://www.lumentum.com', country: 'USA', founded_year: 2015, is_factory: true, description: 'Lasers, ROADMs, coherent receivers', aliases: ['JDSU', 'JDS Uniphase'] },
|
||||||
|
{ name: 'InnoLight Technology', slug: 'innolight', vendor_type: 'manufacturer', website: 'https://www.innolight.com', country: 'CHN', founded_year: 2008, is_factory: true, description: 'Top 3 global transceiver manufacturer, AI/hyperscaler focus' },
|
||||||
|
{ name: 'Hisense Broadband', slug: 'hisense-broadband', vendor_type: 'manufacturer', website: 'https://www.hisense-broadband.com', country: 'CHN', founded_year: 2003, is_factory: true, description: 'High-volume transceiver factory', aliases: ['Hisense'] },
|
||||||
|
{ name: 'Source Photonics', slug: 'source-photonics', vendor_type: 'manufacturer', website: 'https://www.sourcephotonics.com', country: 'USA', founded_year: 2000, is_factory: true, description: 'Access and data center transceivers' },
|
||||||
|
{ name: 'Eoptolink Technology', slug: 'eoptolink', vendor_type: 'manufacturer', website: 'https://en.eoptolink.com', country: 'CHN', founded_year: 2002, is_factory: true, description: 'SFP/QSFP manufacturer in Chengdu' },
|
||||||
|
{ name: 'Zhongji Innolight', slug: 'zhongji', vendor_type: 'manufacturer', website: 'https://www.innolight.com', country: 'CHN', founded_year: 2008, is_factory: true, description: 'InnoLight parent entity' },
|
||||||
|
{ name: 'Accelink Technologies', slug: 'accelink', vendor_type: 'manufacturer', website: 'https://www.accelink.com', country: 'CHN', founded_year: 2001, is_factory: true, description: 'Optical components and modules (Wuhan)' },
|
||||||
|
{ name: 'Centera Photonics', slug: 'centera', vendor_type: 'manufacturer', website: 'https://www.centeraphotonics.com', country: 'TWN', founded_year: 2016, is_factory: true, description: 'Silicon Photonics transceivers' },
|
||||||
|
{ name: 'Marvell', slug: 'marvell', vendor_type: 'manufacturer', website: 'https://www.marvell.com', country: 'USA', founded_year: 1995, is_factory: true, description: 'DSP/retimer chips for coherent optics', aliases: ['Inphi'] },
|
||||||
|
{ name: 'Luxtera', slug: 'luxtera', vendor_type: 'manufacturer', website: 'https://www.cisco.com', country: 'USA', founded_year: 2001, is_factory: true, description: 'Silicon Photonics pioneer (acquired by Cisco 2019)' },
|
||||||
|
{ name: 'Sicoya', slug: 'sicoya', vendor_type: 'manufacturer', website: 'https://www.sicoya.com', country: 'DEU', founded_year: 2015, is_factory: true, description: 'German Silicon Photonics startup (Juniper acquired)' },
|
||||||
|
{ name: 'O-Net Technologies', slug: 'onet', vendor_type: 'manufacturer', website: 'https://www.o-netcom.com', country: 'CHN', founded_year: 2000, is_factory: true, description: 'Optical subsystems and modules' },
|
||||||
|
{ name: 'Cloud Light Technology', slug: 'cloud-light', vendor_type: 'manufacturer', website: 'https://www.cloud-light.com', country: 'CHN', founded_year: 2014, is_factory: true, description: 'High-speed optical transceivers' },
|
||||||
|
{ name: 'Broadex Technologies', slug: 'broadex', vendor_type: 'manufacturer', website: 'https://www.broadex-tech.com', country: 'CHN', founded_year: 2014, is_factory: true, description: 'Silicon Photonics transceivers' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// COMPATIBLE TRANSCEIVER VENDORS
|
||||||
|
// ============================================================
|
||||||
|
{ name: 'Flexoptix', slug: 'flexoptix', vendor_type: 'compatible', website: 'https://www.flexoptix.net', country: 'DEU', founded_year: 2009, description: 'German compatible transceiver vendor with FlexBox coding', aliases: ['FLEXOPTIX GmbH'], scrape_url: 'https://www.flexoptix.net/en/', scrape_enabled: true },
|
||||||
|
{ name: 'Axiom Memory Solutions', slug: 'axiom', vendor_type: 'compatible', website: 'https://www.axiommemory.com', country: 'USA', founded_year: 1990, description: 'Compatible transceivers with OEM cross-reference', scrape_url: 'https://www.axiommemory.com/category/transceivers', scrape_enabled: true },
|
||||||
|
{ name: 'ProLabs', slug: 'prolabs', vendor_type: 'compatible', website: 'https://www.prolabs.com', country: 'GBR', founded_year: 2005, description: 'UK compatible transceiver vendor with compatibility finder', scrape_url: 'https://www.prolabs.com/compatibility/', scrape_enabled: true },
|
||||||
|
{ name: 'AddOn Networks', slug: 'addon', vendor_type: 'compatible', website: 'https://www.addonnetworks.com', country: 'USA', founded_year: 2008, description: 'Compatible transceivers and cables' },
|
||||||
|
{ name: 'FluxLight', slug: 'fluxlight', vendor_type: 'compatible', website: 'https://www.fluxlight.com', country: 'USA', founded_year: 2014, description: 'Compatible transceivers with OEM mapping', scrape_url: 'https://www.fluxlight.com/transceivers/', scrape_enabled: true },
|
||||||
|
{ name: 'Approved Networks', slug: 'approved-networks', vendor_type: 'compatible', website: 'https://www.approvednetworks.com', country: 'USA', founded_year: 2008, description: 'Compatible optics with JSON-LD structured data' },
|
||||||
|
{ name: '10Gtek', slug: '10gtek', vendor_type: 'compatible', website: 'https://www.10gtek.com', country: 'CHN', founded_year: 2010, description: 'Chinese compatible transceiver vendor', scrape_url: 'https://www.10gtek.com', scrape_enabled: true },
|
||||||
|
{ name: 'Champion ONE', slug: 'champion-one', vendor_type: 'compatible', website: 'https://www.championone.com', country: 'USA', founded_year: 2006, description: 'Compatible transceivers and active optical cables' },
|
||||||
|
{ name: 'Proline (Legrand)', slug: 'proline', vendor_type: 'compatible', website: 'https://www.legrand.us', country: 'USA', description: 'Legrand/C2G compatible transceivers', aliases: ['C2G', 'Legrand'] },
|
||||||
|
{ name: 'Atgbics', slug: 'atgbics', vendor_type: 'compatible', website: 'https://www.atgbics.com', country: 'GBR', founded_year: 2009, description: 'UK compatible optics vendor' },
|
||||||
|
{ name: 'Fiberstore', slug: 'fiberstore', vendor_type: 'compatible', website: 'https://www.fs.com', country: 'CHN', description: 'FS.com compatible transceiver line' },
|
||||||
|
{ name: 'Starview', slug: 'starview', vendor_type: 'compatible', website: 'https://www.starview.com', country: 'USA', description: 'Compatible transceivers for enterprise' },
|
||||||
|
{ name: 'Optec', slug: 'optec', vendor_type: 'compatible', website: 'https://www.optec.de', country: 'DEU', description: 'German compatible optics vendor' },
|
||||||
|
{ name: 'Curvature', slug: 'curvature', vendor_type: 'compatible', website: 'https://www.curvature.com', country: 'USA', description: 'Third-party maintenance and compatible optics', aliases: ['Network Hardware Resale'] },
|
||||||
|
{ name: 'Transition Networks', slug: 'transition-networks', vendor_type: 'compatible', website: 'https://www.transition.com', country: 'USA', founded_year: 1987, description: 'Media converters and compatible transceivers' },
|
||||||
|
{ name: 'Opticin', slug: 'opticin', vendor_type: 'compatible', website: 'https://opticin.com', country: 'RUS', description: 'Russian compatible transceiver vendor' },
|
||||||
|
{ name: 'Fibertrade', slug: 'fibertrade', vendor_type: 'compatible', website: 'https://fibertrade.ru', country: 'RUS', description: 'Russian compatible transceivers' },
|
||||||
|
{ name: 'Linkway Communication', slug: 'linkway', vendor_type: 'compatible', website: 'https://www.linkwaycommunication.com', country: 'CHN', description: 'Chinese compatible optics vendor' },
|
||||||
|
{ name: 'Gigalight', slug: 'gigalight', vendor_type: 'compatible', website: 'https://www.gigalight.com', country: 'CHN', founded_year: 2006, description: 'Shenzhen-based optics manufacturer and compatible vendor' },
|
||||||
|
{ name: 'HTFuture', slug: 'htfuture', vendor_type: 'compatible', website: 'https://www.htfuture.com', country: 'CHN', description: 'Chinese compatible transceiver vendor' },
|
||||||
|
{ name: 'LODFIBER', slug: 'lodfiber', vendor_type: 'compatible', website: 'https://www.lodfiber.com', country: 'CHN', description: 'Shenzhen compatible transceiver vendor' },
|
||||||
|
{ name: 'Finisar', slug: 'finisar', vendor_type: 'compatible', website: 'https://www.coherent.com', country: 'USA', founded_year: 1988, description: 'Legacy brand (now Coherent), still referenced in part numbers' },
|
||||||
|
{ name: 'JDSU', slug: 'jdsu', vendor_type: 'compatible', website: 'https://www.lumentum.com', country: 'USA', description: 'Legacy brand (now Lumentum/Viavi)' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// DISTRIBUTORS / MARKETPLACES
|
||||||
|
// ============================================================
|
||||||
|
{ name: 'Amazon', slug: 'amazon', vendor_type: 'marketplace', website: 'https://www.amazon.com', country: 'USA', founded_year: 1994, description: 'Online marketplace with transceiver listings' },
|
||||||
|
{ name: 'eBay', slug: 'ebay', vendor_type: 'marketplace', website: 'https://www.ebay.com', country: 'USA', founded_year: 1995, description: 'Auction/fixed-price marketplace for new and used optics', scrape_url: 'https://www.ebay.com/sch/i.html?_nkw=transceiver', scrape_enabled: true },
|
||||||
|
{ name: 'AliExpress', slug: 'aliexpress', vendor_type: 'marketplace', website: 'https://www.aliexpress.com', country: 'CHN', founded_year: 2010, description: 'Chinese marketplace with direct factory pricing' },
|
||||||
|
{ name: 'Mouser Electronics', slug: 'mouser', vendor_type: 'distributor', website: 'https://www.mouser.com', country: 'USA', founded_year: 1964, description: 'Electronic component distributor' },
|
||||||
|
{ name: 'Digi-Key', slug: 'digikey', vendor_type: 'distributor', website: 'https://www.digikey.com', country: 'USA', founded_year: 1972, description: 'Electronic component distributor' },
|
||||||
|
{ name: 'Arrow Electronics', slug: 'arrow', vendor_type: 'distributor', website: 'https://www.arrow.com', country: 'USA', founded_year: 1935, description: 'Electronic component distributor' },
|
||||||
|
{ name: 'Ingram Micro', slug: 'ingram', vendor_type: 'distributor', website: 'https://www.ingrammicro.com', country: 'USA', founded_year: 1979, description: 'IT distribution including networking' },
|
||||||
|
{ name: 'Hummingbird Networks', slug: 'hummingbird', vendor_type: 'distributor', website: 'https://www.hummingbirdnetworks.com', country: 'USA', description: 'Used and refurbished networking equipment' },
|
||||||
|
{ name: 'Park Place Technologies', slug: 'park-place', vendor_type: 'refurbished', website: 'https://www.parkplacetechnologies.com', country: 'USA', description: 'Third-party maintenance and refurbished optics' },
|
||||||
|
{ name: 'ITRenew', slug: 'itrenew', vendor_type: 'refurbished', website: 'https://www.itrenew.com', country: 'USA', description: 'Refurbished IT equipment including optics' },
|
||||||
|
];
|
||||||
271
src/api/server.ts
Normal file
271
src/api/server.ts
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import compression from 'compression';
|
||||||
|
import { pino } from 'pino';
|
||||||
|
import { query } from '../utils/db.js';
|
||||||
|
|
||||||
|
const log = pino({ name: 'api' });
|
||||||
|
const app = express();
|
||||||
|
const PORT = parseInt(process.env.PORT ?? '3200');
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(compression());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/health', (_, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() }));
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// TRANSCEIVERS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Search transceivers
|
||||||
|
app.get('/api/v1/transceivers', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { q, category, vendor, data_rate, wavelength, connector, status, limit = '50', offset = '0' } = req.query;
|
||||||
|
|
||||||
|
let sql = `
|
||||||
|
SELECT t.*, v.name AS vendor_name, v.slug AS vendor_slug, ff.name AS form_factor_name
|
||||||
|
FROM transceivers t
|
||||||
|
JOIN vendors v ON t.vendor_id = v.id
|
||||||
|
LEFT JOIN form_factors ff ON t.form_factor_id = ff.id
|
||||||
|
WHERE 1=1
|
||||||
|
`;
|
||||||
|
const params: unknown[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
if (q) {
|
||||||
|
sql += ` AND (t.part_number ILIKE $${idx} OR t.name ILIKE $${idx} OR t.oem_part_number ILIKE $${idx})`;
|
||||||
|
params.push(`%${q}%`);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (category) { sql += ` AND t.category = $${idx}`; params.push(category); idx++; }
|
||||||
|
if (vendor) { sql += ` AND v.slug = $${idx}`; params.push(vendor); idx++; }
|
||||||
|
if (data_rate) { sql += ` AND t.data_rate = $${idx}`; params.push(parseFloat(data_rate as string)); idx++; }
|
||||||
|
if (wavelength) { sql += ` AND t.wavelength_nm = $${idx}`; params.push(parseFloat(wavelength as string)); idx++; }
|
||||||
|
if (connector) { sql += ` AND t.connector = $${idx}`; params.push(connector); idx++; }
|
||||||
|
if (status) { sql += ` AND t.status = $${idx}`; params.push(status); idx++; }
|
||||||
|
|
||||||
|
sql += ` ORDER BY t.data_rate DESC, t.name ASC LIMIT $${idx} OFFSET $${idx + 1}`;
|
||||||
|
params.push(parseInt(limit as string), parseInt(offset as string));
|
||||||
|
|
||||||
|
const result = await query(sql, params);
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
let countSql = sql.replace(/SELECT .+ FROM/, 'SELECT COUNT(*) FROM').replace(/ORDER BY.+/, '');
|
||||||
|
const countResult = await query(countSql, params.slice(0, -2));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data: result.rows,
|
||||||
|
total: parseInt(countResult.rows[0]?.count ?? '0'),
|
||||||
|
limit: parseInt(limit as string),
|
||||||
|
offset: parseInt(offset as string),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
log.error({ err }, 'Search failed');
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get single transceiver
|
||||||
|
app.get('/api/v1/transceivers/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await query(
|
||||||
|
`SELECT t.*, v.name AS vendor_name, v.slug AS vendor_slug, ff.name AS form_factor_name,
|
||||||
|
ff.full_name AS form_factor_full_name, ff.lanes, ff.power_max_w
|
||||||
|
FROM transceivers t
|
||||||
|
JOIN vendors v ON t.vendor_id = v.id
|
||||||
|
LEFT JOIN form_factors ff ON t.form_factor_id = ff.id
|
||||||
|
WHERE t.id = $1`,
|
||||||
|
[req.params.id]
|
||||||
|
);
|
||||||
|
if (!result.rows[0]) return res.status(404).json({ error: 'Not found' });
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get prices for a transceiver
|
||||||
|
app.get('/api/v1/transceivers/:id/prices', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { days = '30' } = req.query;
|
||||||
|
const result = await query(
|
||||||
|
`SELECT p.*, v.name AS vendor_name
|
||||||
|
FROM prices p JOIN vendors v ON p.vendor_id = v.id
|
||||||
|
WHERE p.transceiver_id = $1 AND p.time > NOW() - INTERVAL '1 day' * $2
|
||||||
|
ORDER BY p.time DESC LIMIT 500`,
|
||||||
|
[req.params.id, parseInt(days as string)]
|
||||||
|
);
|
||||||
|
res.json({ data: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get compatibility for a transceiver
|
||||||
|
app.get('/api/v1/transceivers/:id/compatibility', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await query(
|
||||||
|
`SELECT c.*, d.model, d.series, d.device_type, v.name AS device_vendor
|
||||||
|
FROM compatibility c
|
||||||
|
JOIN network_devices d ON c.device_id = d.id
|
||||||
|
JOIN vendors v ON d.vendor_id = v.id
|
||||||
|
WHERE c.transceiver_id = $1
|
||||||
|
ORDER BY v.name, d.model`,
|
||||||
|
[req.params.id]
|
||||||
|
);
|
||||||
|
res.json({ data: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// VENDORS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
app.get('/api/v1/vendors', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { type } = req.query;
|
||||||
|
let sql = `SELECT v.*, (SELECT COUNT(*) FROM transceivers t WHERE t.vendor_id = v.id) AS transceiver_count FROM vendors v`;
|
||||||
|
const params: unknown[] = [];
|
||||||
|
if (type) { sql += ` WHERE v.vendor_type = $1`; params.push(type); }
|
||||||
|
sql += ' ORDER BY v.name';
|
||||||
|
const result = await query(sql, params);
|
||||||
|
res.json({ data: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// NETWORK DEVICES
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
app.get('/api/v1/devices', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { vendor, series, q, limit = '50', offset = '0' } = req.query;
|
||||||
|
let sql = `SELECT d.*, v.name AS vendor_name FROM network_devices d JOIN vendors v ON d.vendor_id = v.id WHERE 1=1`;
|
||||||
|
const params: unknown[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
if (vendor) { sql += ` AND v.slug = $${idx}`; params.push(vendor); idx++; }
|
||||||
|
if (series) { sql += ` AND d.series ILIKE $${idx}`; params.push(`%${series}%`); idx++; }
|
||||||
|
if (q) { sql += ` AND d.model ILIKE $${idx}`; params.push(`%${q}%`); idx++; }
|
||||||
|
|
||||||
|
sql += ` ORDER BY v.name, d.model LIMIT $${idx} OFFSET $${idx + 1}`;
|
||||||
|
params.push(parseInt(limit as string), parseInt(offset as string));
|
||||||
|
|
||||||
|
const result = await query(sql, params);
|
||||||
|
res.json({ data: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// FORM FACTORS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
app.get('/api/v1/form-factors', async (_, res) => {
|
||||||
|
try {
|
||||||
|
const result = await query(
|
||||||
|
`SELECT ff.*, (SELECT COUNT(*) FROM transceivers t WHERE t.form_factor_id = ff.id) AS transceiver_count
|
||||||
|
FROM form_factors ff ORDER BY ff.generation, ff.release_year`
|
||||||
|
);
|
||||||
|
res.json({ data: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// STANDARDS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
app.get('/api/v1/standards', async (_, res) => {
|
||||||
|
try {
|
||||||
|
const result = await query('SELECT * FROM standards ORDER BY body, year DESC');
|
||||||
|
res.json({ data: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HYPE CYCLES
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
app.get('/api/v1/hype-cycles', async (_, res) => {
|
||||||
|
try {
|
||||||
|
const result = await query('SELECT * FROM hype_cycles ORDER BY current_phase, technology');
|
||||||
|
res.json({ data: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// STATS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
app.get('/api/v1/stats', async (_, res) => {
|
||||||
|
try {
|
||||||
|
const [transceivers, vendors, devices, prices, compatibility] = await Promise.all([
|
||||||
|
query('SELECT COUNT(*) FROM transceivers'),
|
||||||
|
query('SELECT COUNT(*) FROM vendors'),
|
||||||
|
query('SELECT COUNT(*) FROM network_devices'),
|
||||||
|
query('SELECT COUNT(*) FROM prices'),
|
||||||
|
query('SELECT COUNT(*) FROM compatibility'),
|
||||||
|
]);
|
||||||
|
res.json({
|
||||||
|
transceivers: parseInt(transceivers.rows[0]?.count ?? '0'),
|
||||||
|
vendors: parseInt(vendors.rows[0]?.count ?? '0'),
|
||||||
|
network_devices: parseInt(devices.rows[0]?.count ?? '0'),
|
||||||
|
price_points: parseInt(prices.rows[0]?.count ?? '0'),
|
||||||
|
compatibility_entries: parseInt(compatibility.rows[0]?.count ?? '0'),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// PRICE COMPARISON
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
app.get('/api/v1/compare-prices', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { part_number } = req.query;
|
||||||
|
if (!part_number) return res.status(400).json({ error: 'part_number required' });
|
||||||
|
|
||||||
|
const result = await query(
|
||||||
|
`SELECT DISTINCT ON (v.slug)
|
||||||
|
t.part_number, t.name, v.name AS vendor_name, v.slug AS vendor_slug, v.vendor_type,
|
||||||
|
p.price, p.currency, p.price_usd, p.in_stock, p.condition, p.url, p.time
|
||||||
|
FROM prices p
|
||||||
|
JOIN transceivers t ON p.transceiver_id = t.id
|
||||||
|
JOIN vendors v ON p.vendor_id = v.id
|
||||||
|
WHERE t.part_number ILIKE $1
|
||||||
|
ORDER BY v.slug, p.time DESC`,
|
||||||
|
[`%${part_number}%`]
|
||||||
|
);
|
||||||
|
res.json({ data: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// START
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
log.info({ port: PORT }, 'TIP API server started');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
162
src/crawlers/base.ts
Normal file
162
src/crawlers/base.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { query } from '../utils/db.js';
|
||||||
|
import { pino } from 'pino';
|
||||||
|
import type { CrawlerConfig } from '../types/index.js';
|
||||||
|
|
||||||
|
export abstract class BaseCrawler {
|
||||||
|
protected log;
|
||||||
|
protected config: CrawlerConfig;
|
||||||
|
protected jobId: number | null = null;
|
||||||
|
protected stats = { total: 0, processed: 0, failed: 0, newItems: 0, updated: 0 };
|
||||||
|
|
||||||
|
constructor(config: CrawlerConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.log = pino({ name: `crawler:${config.name}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
async startJob(): Promise<number> {
|
||||||
|
const result = await query(
|
||||||
|
`INSERT INTO crawl_jobs (crawler, status, started_at) VALUES ($1, 'running', NOW()) RETURNING id`,
|
||||||
|
[this.config.name]
|
||||||
|
);
|
||||||
|
this.jobId = result.rows[0]!.id;
|
||||||
|
this.log.info({ jobId: this.jobId }, 'Crawl job started');
|
||||||
|
return this.jobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async finishJob(status: 'success' | 'failed', error?: string): Promise<void> {
|
||||||
|
if (!this.jobId) return;
|
||||||
|
await query(
|
||||||
|
`UPDATE crawl_jobs SET status = $1, urls_total = $2, urls_processed = $3, urls_failed = $4,
|
||||||
|
items_new = $5, items_updated = $6, items_found = $7, error_message = $8,
|
||||||
|
finished_at = NOW(), duration_ms = EXTRACT(EPOCH FROM (NOW() - started_at)) * 1000
|
||||||
|
WHERE id = $9`,
|
||||||
|
[status, this.stats.total, this.stats.processed, this.stats.failed,
|
||||||
|
this.stats.newItems, this.stats.updated, this.stats.newItems + this.stats.updated,
|
||||||
|
error ?? null, this.jobId]
|
||||||
|
);
|
||||||
|
this.log.info({ jobId: this.jobId, status, stats: this.stats }, 'Crawl job finished');
|
||||||
|
}
|
||||||
|
|
||||||
|
async logError(url: string, code: string, message: string): Promise<void> {
|
||||||
|
if (!this.jobId) return;
|
||||||
|
await query(
|
||||||
|
`INSERT INTO crawl_errors (job_id, url, error_code, error_message) VALUES ($1, $2, $3, $4)`,
|
||||||
|
[this.jobId, url, code, message]
|
||||||
|
);
|
||||||
|
this.stats.failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getVendorId(slug: string): Promise<number | null> {
|
||||||
|
const result = await query('SELECT id FROM vendors WHERE slug = $1', [slug]);
|
||||||
|
return result.rows[0]?.id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getFormFactorId(name: string): Promise<number | null> {
|
||||||
|
const result = await query('SELECT id FROM form_factors WHERE name = $1', [name]);
|
||||||
|
return result.rows[0]?.id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async upsertTransceiver(data: {
|
||||||
|
part_number: string;
|
||||||
|
vendor_id: number;
|
||||||
|
form_factor_id?: number | null;
|
||||||
|
name?: string;
|
||||||
|
category?: string;
|
||||||
|
subcategory?: string;
|
||||||
|
data_rate?: number;
|
||||||
|
data_rate_unit?: string;
|
||||||
|
max_reach?: number;
|
||||||
|
reach_unit?: string;
|
||||||
|
wavelength_nm?: number;
|
||||||
|
connector?: string;
|
||||||
|
fiber_type?: string;
|
||||||
|
duplex?: boolean;
|
||||||
|
temp_range?: string;
|
||||||
|
dom_support?: string;
|
||||||
|
oem_part_number?: string;
|
||||||
|
status?: string;
|
||||||
|
image_url?: string;
|
||||||
|
datasheet_url?: string;
|
||||||
|
product_url?: string;
|
||||||
|
tags?: string[];
|
||||||
|
raw_specs?: Record<string, unknown>;
|
||||||
|
source: string;
|
||||||
|
source_url?: string;
|
||||||
|
}): Promise<{ id: number; isNew: boolean }> {
|
||||||
|
const result = await query(
|
||||||
|
`INSERT INTO transceivers (
|
||||||
|
part_number, vendor_id, form_factor_id, name, category, subcategory,
|
||||||
|
data_rate, data_rate_unit, max_reach, reach_unit, wavelength_nm,
|
||||||
|
connector, fiber_type, duplex, temp_range, dom_support,
|
||||||
|
oem_part_number, status, image_url, datasheet_url, product_url,
|
||||||
|
tags, raw_specs, source, source_url, last_verified
|
||||||
|
) 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,NOW())
|
||||||
|
ON CONFLICT (part_number, vendor_id) DO UPDATE SET
|
||||||
|
name = COALESCE($4, transceivers.name),
|
||||||
|
image_url = COALESCE($19, transceivers.image_url),
|
||||||
|
datasheet_url = COALESCE($20, transceivers.datasheet_url),
|
||||||
|
product_url = COALESCE($21, transceivers.product_url),
|
||||||
|
raw_specs = COALESCE($23, transceivers.raw_specs),
|
||||||
|
last_verified = NOW(),
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING id, (xmax = 0) AS is_new`,
|
||||||
|
[
|
||||||
|
data.part_number, data.vendor_id, data.form_factor_id ?? null,
|
||||||
|
data.name ?? null, data.category ?? null, data.subcategory ?? null,
|
||||||
|
data.data_rate ?? null, data.data_rate_unit ?? 'Gbps',
|
||||||
|
data.max_reach ?? null, data.reach_unit ?? 'km',
|
||||||
|
data.wavelength_nm ?? null, data.connector ?? null,
|
||||||
|
data.fiber_type ?? null, data.duplex ?? true,
|
||||||
|
data.temp_range ?? 'commercial', data.dom_support ?? 'none',
|
||||||
|
data.oem_part_number ?? null, data.status ?? 'active',
|
||||||
|
data.image_url ?? null, data.datasheet_url ?? null, data.product_url ?? null,
|
||||||
|
data.tags ?? null, data.raw_specs ? JSON.stringify(data.raw_specs) : null,
|
||||||
|
data.source, data.source_url ?? null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const row = result.rows[0]!;
|
||||||
|
if (row.is_new) this.stats.newItems++;
|
||||||
|
else this.stats.updated++;
|
||||||
|
return { id: row.id, isNew: row.is_new };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async upsertPrice(data: {
|
||||||
|
transceiver_id: number;
|
||||||
|
vendor_id: number;
|
||||||
|
price: number;
|
||||||
|
currency?: string;
|
||||||
|
price_usd?: number;
|
||||||
|
in_stock?: boolean;
|
||||||
|
stock_quantity?: number;
|
||||||
|
condition?: string;
|
||||||
|
url?: string;
|
||||||
|
source: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
await query(
|
||||||
|
`INSERT INTO prices (time, transceiver_id, vendor_id, price, currency, price_usd,
|
||||||
|
in_stock, stock_quantity, condition, url, source)
|
||||||
|
VALUES (NOW(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||||
|
[
|
||||||
|
data.transceiver_id, data.vendor_id, data.price,
|
||||||
|
data.currency ?? 'USD', data.price_usd ?? data.price,
|
||||||
|
data.in_stock ?? null, data.stock_quantity ?? null,
|
||||||
|
data.condition ?? 'new', data.url ?? null, data.source,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract crawl(): Promise<void>;
|
||||||
|
|
||||||
|
async run(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.startJob();
|
||||||
|
await this.crawl();
|
||||||
|
await this.finishJob('success');
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
this.log.error({ err }, 'Crawl failed');
|
||||||
|
await this.finishJob('failed', msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
183
src/crawlers/ebay.ts
Normal file
183
src/crawlers/ebay.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { CheerioCrawler } from 'crawlee';
|
||||||
|
import { BaseCrawler } from './base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* eBay Transceiver Price Crawler
|
||||||
|
* Scrapes current Buy-It-Now listings and sold prices for transceivers
|
||||||
|
* Gives us real market pricing data
|
||||||
|
*/
|
||||||
|
export class EbayCrawler extends BaseCrawler {
|
||||||
|
// Search queries for different transceiver categories
|
||||||
|
private static QUERIES = [
|
||||||
|
{ q: 'SFP 1G transceiver -used -lot', category: 'SFP' },
|
||||||
|
{ q: 'SFP+ 10G transceiver', category: 'SFP+' },
|
||||||
|
{ q: 'SFP28 25G transceiver', category: 'SFP28' },
|
||||||
|
{ q: 'QSFP+ 40G transceiver', category: 'QSFP+' },
|
||||||
|
{ q: 'QSFP28 100G transceiver', category: 'QSFP28' },
|
||||||
|
{ q: 'QSFP-DD 400G transceiver', category: 'QSFP-DD' },
|
||||||
|
{ q: 'OSFP 400G 800G transceiver', category: 'OSFP' },
|
||||||
|
{ q: 'DAC cable 10G SFP+ direct attach', category: 'DAC' },
|
||||||
|
{ q: 'DAC cable 100G QSFP28', category: 'DAC' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
name: 'ebay',
|
||||||
|
baseUrl: 'https://www.ebay.com',
|
||||||
|
interval: 43200, // twice daily
|
||||||
|
maxConcurrency: 3,
|
||||||
|
maxRequestsPerMinute: 15,
|
||||||
|
usePlaywright: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async crawl(): Promise<void> {
|
||||||
|
const ebayVendorId = await this.getVendorId('ebay');
|
||||||
|
if (!ebayVendorId) throw new Error('Vendor ebay not found');
|
||||||
|
|
||||||
|
const crawler = new CheerioCrawler({
|
||||||
|
maxConcurrency: this.config.maxConcurrency,
|
||||||
|
maxRequestsPerMinute: this.config.maxRequestsPerMinute,
|
||||||
|
|
||||||
|
async requestHandler({ request, $, enqueueLinks }) {
|
||||||
|
const url = request.url;
|
||||||
|
const category = request.userData?.category as string;
|
||||||
|
|
||||||
|
this.log.info({ url, category }, 'Processing eBay search page');
|
||||||
|
|
||||||
|
// Extract listings
|
||||||
|
const listings = $('li.s-item, div.s-item__wrapper').toArray();
|
||||||
|
|
||||||
|
for (const el of listings) {
|
||||||
|
try {
|
||||||
|
const $item = $(el);
|
||||||
|
const title = $item.find('.s-item__title span, .s-item__title').first().text().trim();
|
||||||
|
const priceText = $item.find('.s-item__price').first().text().trim();
|
||||||
|
const itemUrl = $item.find('.s-item__link').attr('href') ?? '';
|
||||||
|
const condition = $item.find('.SECONDARY_INFO').text().trim();
|
||||||
|
const imageUrl = $item.find('.s-item__image-img').attr('src') ?? '';
|
||||||
|
|
||||||
|
// Skip non-transceiver results
|
||||||
|
if (!title || title === 'Shop on eBay') continue;
|
||||||
|
|
||||||
|
// Parse price
|
||||||
|
const priceMatch = priceText.match(/[\$€£]([\d,]+\.?\d*)/);
|
||||||
|
if (!priceMatch) continue;
|
||||||
|
const price = parseFloat(priceMatch[1]!.replace(',', ''));
|
||||||
|
if (price <= 0 || price > 50000) continue;
|
||||||
|
|
||||||
|
// Detect currency
|
||||||
|
let currency = 'USD';
|
||||||
|
if (priceText.includes('€')) currency = 'EUR';
|
||||||
|
else if (priceText.includes('£')) currency = 'GBP';
|
||||||
|
|
||||||
|
// Try to extract part number from title
|
||||||
|
const partNumber = this.extractPartNumber(title);
|
||||||
|
if (!partNumber) continue;
|
||||||
|
|
||||||
|
// Detect vendor from title
|
||||||
|
const vendorSlug = this.detectVendor(title);
|
||||||
|
const actualVendorId = vendorSlug ? (await this.getVendorId(vendorSlug)) ?? ebayVendorId : ebayVendorId;
|
||||||
|
|
||||||
|
// Upsert transceiver
|
||||||
|
const result = await this.upsertTransceiver({
|
||||||
|
part_number: partNumber,
|
||||||
|
vendor_id: actualVendorId,
|
||||||
|
name: title.slice(0, 500),
|
||||||
|
category,
|
||||||
|
image_url: imageUrl || undefined,
|
||||||
|
product_url: itemUrl,
|
||||||
|
tags: ['ebay', category.toLowerCase()],
|
||||||
|
source: 'ebay',
|
||||||
|
source_url: itemUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Always insert price (time-series)
|
||||||
|
await this.upsertPrice({
|
||||||
|
transceiver_id: result.id,
|
||||||
|
vendor_id: ebayVendorId,
|
||||||
|
price,
|
||||||
|
currency: currency as any,
|
||||||
|
in_stock: true,
|
||||||
|
condition: condition.toLowerCase().includes('new') ? 'new' : condition.toLowerCase().includes('refurb') ? 'refurbished' : 'used',
|
||||||
|
url: itemUrl,
|
||||||
|
source: 'ebay',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Skip individual item errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow pagination (first 5 pages)
|
||||||
|
const pageNum = parseInt(new URL(url).searchParams.get('_pgn') ?? '1');
|
||||||
|
if (pageNum < 5) {
|
||||||
|
const nextUrl = new URL(url);
|
||||||
|
nextUrl.searchParams.set('_pgn', String(pageNum + 1));
|
||||||
|
await enqueueLinks({
|
||||||
|
urls: [nextUrl.href],
|
||||||
|
userData: { category },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stats.total++;
|
||||||
|
this.stats.processed++;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build initial search URLs
|
||||||
|
const requests = EbayCrawler.QUERIES.map(q => ({
|
||||||
|
url: `${this.config.baseUrl}/sch/i.html?_nkw=${encodeURIComponent(q.q)}&_sacat=0&LH_BIN=1&_sop=15&_ipg=120`,
|
||||||
|
userData: { category: q.category },
|
||||||
|
}));
|
||||||
|
|
||||||
|
await crawler.run(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractPartNumber(title: string): string | null {
|
||||||
|
// Common OEM part number patterns
|
||||||
|
const patterns = [
|
||||||
|
/\b(SFP-\d+G-[A-Z0-9-]+)\b/i,
|
||||||
|
/\b(GLC-[A-Z0-9-]+)\b/i,
|
||||||
|
/\b(QSFP-[A-Z0-9-]+)\b/i,
|
||||||
|
/\b(QDD-[A-Z0-9-]+)\b/i,
|
||||||
|
/\b(OSFP-[A-Z0-9-]+)\b/i,
|
||||||
|
/\b(EX-SFP-[A-Z0-9-]+)\b/i,
|
||||||
|
/\b(JNP-[A-Z0-9-]+)\b/i,
|
||||||
|
/\b(CWDM-[A-Z0-9-]+)\b/i,
|
||||||
|
/\b(DS-SFP-[A-Z0-9-]+)\b/i,
|
||||||
|
/\b([A-Z]{2,4}-\d+[A-Z]+-[A-Z0-9]+)\b/, // Generic pattern
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = title.match(pattern);
|
||||||
|
if (match) return match[1]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectVendor(title: string): string | null {
|
||||||
|
const tl = title.toLowerCase();
|
||||||
|
if (tl.includes('cisco')) return 'cisco';
|
||||||
|
if (tl.includes('juniper')) return 'juniper';
|
||||||
|
if (tl.includes('arista')) return 'arista';
|
||||||
|
if (tl.includes('hpe') || tl.includes('hewlett')) return 'hpe';
|
||||||
|
if (tl.includes('dell')) return 'dell';
|
||||||
|
if (tl.includes('fs.com') || tl.includes('fiberstore')) return 'fs-com';
|
||||||
|
if (tl.includes('10gtek')) return '10gtek';
|
||||||
|
if (tl.includes('flexoptix')) return 'flexoptix';
|
||||||
|
if (tl.includes('mellanox') || tl.includes('nvidia')) return 'mellanox';
|
||||||
|
if (tl.includes('mikrotik')) return 'mikrotik';
|
||||||
|
if (tl.includes('ubiquiti') || tl.includes('unifi')) return 'ubiquiti';
|
||||||
|
if (tl.includes('intel')) return 'intel';
|
||||||
|
if (tl.includes('huawei')) return 'huawei';
|
||||||
|
if (tl.includes('fortinet')) return 'fortinet';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
const crawler = new EbayCrawler();
|
||||||
|
crawler.run();
|
||||||
|
}
|
||||||
249
src/crawlers/fscom.ts
Normal file
249
src/crawlers/fscom.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { PlaywrightCrawler, Dataset } from 'crawlee';
|
||||||
|
import { BaseCrawler } from './base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FS.com Crawler
|
||||||
|
* Scrapes transceiver catalog from fs.com (~4000 SKUs)
|
||||||
|
* Uses Playwright because fs.com has Cloudflare protection
|
||||||
|
*
|
||||||
|
* Categories:
|
||||||
|
* - /c/sfp-702 (1G SFP)
|
||||||
|
* - /c/10g-sfp-756 (10G SFP+)
|
||||||
|
* - /c/25g-sfp28-702 (25G SFP28)
|
||||||
|
* - /c/40g-qsfp-702 (40G QSFP+)
|
||||||
|
* - /c/100g-qsfp28-702 (100G QSFP28)
|
||||||
|
* - /c/200g-qsfp56-702 (200G QSFP56)
|
||||||
|
* - /c/400g-qsfp-dd-702 (400G QSFP-DD)
|
||||||
|
* - /c/800g-osfp-702 (800G OSFP/QSFP-DD800)
|
||||||
|
* - /c/dac-702 (DAC cables)
|
||||||
|
* - /c/aoc-702 (AOC cables)
|
||||||
|
*/
|
||||||
|
export class FsComCrawler extends BaseCrawler {
|
||||||
|
private static CATEGORIES = [
|
||||||
|
{ url: '/c/sfp-702', category: 'SFP', formFactor: 'SFP' },
|
||||||
|
{ url: '/c/10g-sfp-756', category: 'SFP+', formFactor: 'SFP+' },
|
||||||
|
{ url: '/c/25g-sfp28-702', category: 'SFP28', formFactor: 'SFP28' },
|
||||||
|
{ url: '/c/40g-qsfp-702', category: 'QSFP+', formFactor: 'QSFP+' },
|
||||||
|
{ url: '/c/100g-qsfp28-702', category: 'QSFP28', formFactor: 'QSFP28' },
|
||||||
|
{ url: '/c/200g-qsfp56-702', category: 'QSFP56', formFactor: 'QSFP56' },
|
||||||
|
{ url: '/c/400g-qsfp-dd-702', category: 'QSFP-DD', formFactor: 'QSFP-DD' },
|
||||||
|
{ url: '/c/800g-osfp-702', category: 'OSFP', formFactor: 'OSFP' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
name: 'fscom',
|
||||||
|
baseUrl: 'https://www.fs.com',
|
||||||
|
interval: 86400, // daily
|
||||||
|
maxConcurrency: 2,
|
||||||
|
maxRequestsPerMinute: 10,
|
||||||
|
usePlaywright: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async crawl(): Promise<void> {
|
||||||
|
const vendorId = await this.getVendorId('fs-com');
|
||||||
|
if (!vendorId) throw new Error('Vendor fs-com not found');
|
||||||
|
|
||||||
|
const crawler = new PlaywrightCrawler({
|
||||||
|
maxConcurrency: this.config.maxConcurrency,
|
||||||
|
maxRequestsPerMinute: this.config.maxRequestsPerMinute,
|
||||||
|
requestHandlerTimeoutSecs: 60,
|
||||||
|
|
||||||
|
async requestHandler({ request, page, enqueueLinks }) {
|
||||||
|
const url = request.url;
|
||||||
|
|
||||||
|
// Category listing page — enqueue product links
|
||||||
|
if (request.label === 'LIST') {
|
||||||
|
this.log.info({ url }, 'Processing category page');
|
||||||
|
|
||||||
|
// Wait for product grid to load
|
||||||
|
await page.waitForSelector('.prod-list-item, .product-item', { timeout: 15000 }).catch(() => {});
|
||||||
|
|
||||||
|
// Enqueue product detail pages
|
||||||
|
await enqueueLinks({
|
||||||
|
selector: 'a.prod-list-item__title, a.product-item__title',
|
||||||
|
label: 'DETAIL',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Follow pagination
|
||||||
|
await enqueueLinks({
|
||||||
|
selector: 'a.page-next, a.pagination__next',
|
||||||
|
label: 'LIST',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stats.total++;
|
||||||
|
this.stats.processed++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product detail page
|
||||||
|
if (request.label === 'DETAIL') {
|
||||||
|
this.log.info({ url }, 'Processing product page');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.waitForSelector('.product-detail, .prod-detail', { timeout: 15000 }).catch(() => {});
|
||||||
|
|
||||||
|
// Extract product data from the page
|
||||||
|
const data = await page.evaluate(() => {
|
||||||
|
const getText = (sel: string) => document.querySelector(sel)?.textContent?.trim() ?? '';
|
||||||
|
const getAttr = (sel: string, attr: string) => document.querySelector(sel)?.getAttribute(attr) ?? '';
|
||||||
|
|
||||||
|
// Spec table extraction
|
||||||
|
const specs: Record<string, string> = {};
|
||||||
|
document.querySelectorAll('.spec-table tr, .prod-spec tr, table.spec tr').forEach(row => {
|
||||||
|
const cells = row.querySelectorAll('td, th');
|
||||||
|
if (cells.length >= 2) {
|
||||||
|
const key = cells[0]?.textContent?.trim() ?? '';
|
||||||
|
const val = cells[1]?.textContent?.trim() ?? '';
|
||||||
|
if (key && val) specs[key] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: getText('h1') || getText('.product-title'),
|
||||||
|
partNumber: getText('.product-sku, .prod-sku, [itemprop="sku"]'),
|
||||||
|
price: getText('.product-price .price, .prod-price, [itemprop="price"]'),
|
||||||
|
currency: getAttr('[itemprop="priceCurrency"]', 'content') || 'USD',
|
||||||
|
inStock: !getText('.out-of-stock'),
|
||||||
|
image: getAttr('.product-image img, .prod-img img', 'src'),
|
||||||
|
datasheet: getAttr('a[href*="datasheet"], a[href*=".pdf"]', 'href'),
|
||||||
|
specs,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data.partNumber) {
|
||||||
|
this.log.warn({ url }, 'No part number found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine category from URL/specs
|
||||||
|
const category = this.detectCategory(data.specs, url);
|
||||||
|
const ffId = await this.getFormFactorId(category.formFactor);
|
||||||
|
|
||||||
|
// Parse specs
|
||||||
|
const parsed = this.parseSpecs(data.specs);
|
||||||
|
|
||||||
|
const result = await this.upsertTransceiver({
|
||||||
|
part_number: data.partNumber,
|
||||||
|
vendor_id: vendorId,
|
||||||
|
form_factor_id: ffId,
|
||||||
|
name: data.title,
|
||||||
|
category: category.category,
|
||||||
|
subcategory: parsed.subcategory,
|
||||||
|
data_rate: parsed.dataRate,
|
||||||
|
max_reach: parsed.maxReach,
|
||||||
|
reach_unit: parsed.reachUnit as any,
|
||||||
|
wavelength_nm: parsed.wavelength,
|
||||||
|
connector: parsed.connector as any,
|
||||||
|
fiber_type: parsed.fiberType as any,
|
||||||
|
image_url: data.image ? new URL(data.image, this.config.baseUrl).href : undefined,
|
||||||
|
datasheet_url: data.datasheet ? new URL(data.datasheet, this.config.baseUrl).href : undefined,
|
||||||
|
product_url: url,
|
||||||
|
tags: [category.category.toLowerCase(), 'fs.com'],
|
||||||
|
raw_specs: data.specs,
|
||||||
|
source: 'fscom',
|
||||||
|
source_url: url,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert price if available
|
||||||
|
const price = parseFloat(data.price.replace(/[^0-9.]/g, ''));
|
||||||
|
if (price > 0) {
|
||||||
|
await this.upsertPrice({
|
||||||
|
transceiver_id: result.id,
|
||||||
|
vendor_id: vendorId,
|
||||||
|
price,
|
||||||
|
currency: data.currency as any,
|
||||||
|
in_stock: data.inStock,
|
||||||
|
url,
|
||||||
|
source: 'fscom',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
await this.logError(url, 'PARSE', msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stats.total++;
|
||||||
|
this.stats.processed++;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start crawling from category pages
|
||||||
|
const requests = FsComCrawler.CATEGORIES.map(cat => ({
|
||||||
|
url: `${this.config.baseUrl}${cat.url}`,
|
||||||
|
label: 'LIST',
|
||||||
|
userData: cat,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await crawler.run(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectCategory(specs: Record<string, string>, url: string): { category: string; formFactor: string } {
|
||||||
|
const urlLower = url.toLowerCase();
|
||||||
|
if (urlLower.includes('800g')) return { category: 'OSFP', formFactor: 'OSFP' };
|
||||||
|
if (urlLower.includes('400g')) return { category: 'QSFP-DD', formFactor: 'QSFP-DD' };
|
||||||
|
if (urlLower.includes('200g')) return { category: 'QSFP56', formFactor: 'QSFP56' };
|
||||||
|
if (urlLower.includes('100g') || urlLower.includes('qsfp28')) return { category: 'QSFP28', formFactor: 'QSFP28' };
|
||||||
|
if (urlLower.includes('40g') || urlLower.includes('qsfp')) return { category: 'QSFP+', formFactor: 'QSFP+' };
|
||||||
|
if (urlLower.includes('25g') || urlLower.includes('sfp28')) return { category: 'SFP28', formFactor: 'SFP28' };
|
||||||
|
if (urlLower.includes('10g') || urlLower.includes('sfp-plus')) return { category: 'SFP+', formFactor: 'SFP+' };
|
||||||
|
return { category: 'SFP', formFactor: 'SFP' };
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseSpecs(specs: Record<string, string>): {
|
||||||
|
subcategory?: string; dataRate?: number; maxReach?: number; reachUnit?: string;
|
||||||
|
wavelength?: number; connector?: string; fiberType?: string;
|
||||||
|
} {
|
||||||
|
const result: any = {};
|
||||||
|
for (const [key, val] of Object.entries(specs)) {
|
||||||
|
const kl = key.toLowerCase();
|
||||||
|
if (kl.includes('data rate') || kl.includes('speed')) {
|
||||||
|
const match = val.match(/([\d.]+)\s*(Gbps|Mbps|Tbps)/i);
|
||||||
|
if (match) result.dataRate = parseFloat(match[1]!);
|
||||||
|
}
|
||||||
|
if (kl.includes('wavelength')) {
|
||||||
|
const match = val.match(/([\d.]+)\s*nm/i);
|
||||||
|
if (match) result.wavelength = parseFloat(match[1]!);
|
||||||
|
}
|
||||||
|
if (kl.includes('max') && (kl.includes('distance') || kl.includes('reach') || kl.includes('cable'))) {
|
||||||
|
const match = val.match(/([\d.]+)\s*(km|m)/i);
|
||||||
|
if (match) {
|
||||||
|
result.maxReach = parseFloat(match[1]!);
|
||||||
|
result.reachUnit = match[2]!.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kl.includes('connector')) {
|
||||||
|
if (val.includes('LC')) result.connector = 'LC';
|
||||||
|
else if (val.includes('MPO-12')) result.connector = 'MPO-12';
|
||||||
|
else if (val.includes('MPO')) result.connector = 'MPO-12';
|
||||||
|
else if (val.includes('SC')) result.connector = 'SC';
|
||||||
|
else if (val.includes('RJ45')) result.connector = 'copper_rj45';
|
||||||
|
}
|
||||||
|
if (kl.includes('fiber') || kl.includes('cable')) {
|
||||||
|
if (val.includes('SMF') || val.includes('Single')) result.fiberType = 'smf';
|
||||||
|
else if (val.includes('OM4')) result.fiberType = 'mmf_om4';
|
||||||
|
else if (val.includes('OM3')) result.fiberType = 'mmf_om3';
|
||||||
|
}
|
||||||
|
if (kl.includes('type') || kl.includes('application')) {
|
||||||
|
if (val.includes('SR')) result.subcategory = 'SR';
|
||||||
|
else if (val.includes('LR')) result.subcategory = 'LR';
|
||||||
|
else if (val.includes('ER')) result.subcategory = 'ER';
|
||||||
|
else if (val.includes('ZR')) result.subcategory = 'ZR';
|
||||||
|
else if (val.includes('DR')) result.subcategory = 'DR';
|
||||||
|
else if (val.includes('FR')) result.subcategory = 'FR';
|
||||||
|
else if (val.includes('CWDM')) result.subcategory = 'CWDM';
|
||||||
|
else if (val.includes('BiDi')) result.subcategory = 'BiDi';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run standalone
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
const crawler = new FsComCrawler();
|
||||||
|
crawler.run();
|
||||||
|
}
|
||||||
370
src/mcp/server.ts
Normal file
370
src/mcp/server.ts
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { query } from '../utils/db.js';
|
||||||
|
import { pino } from 'pino';
|
||||||
|
import { createServer } from 'http';
|
||||||
|
|
||||||
|
const log = pino({ name: 'mcp' });
|
||||||
|
const PORT = parseInt(process.env.MCP_PORT ?? '3201');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TIP MCP Server — 12 Tools for AI/LLM Integration
|
||||||
|
* Protocol: JSON-RPC 2.0 over SSE (Server-Sent Events)
|
||||||
|
*
|
||||||
|
* Tools:
|
||||||
|
* 1. search_transceivers — Find transceivers by specs, part number, vendor
|
||||||
|
* 2. get_transceiver — Get full details for a transceiver
|
||||||
|
* 3. compare_prices — Compare prices across vendors for a given part
|
||||||
|
* 4. check_compatibility — Check if transceiver works with a device
|
||||||
|
* 5. find_compatible_transceivers — Find all transceivers for a device
|
||||||
|
* 6. search_devices — Search switches/routers by vendor, model, ports
|
||||||
|
* 7. get_form_factor_info — Get details about a form factor
|
||||||
|
* 8. get_hype_cycle — Get technology hype cycle status
|
||||||
|
* 9. search_faq — Search knowledge base articles
|
||||||
|
* 10. get_vendor_catalog — List all transceivers from a vendor
|
||||||
|
* 11. suggest_alternative — Find compatible alternatives for a part
|
||||||
|
* 12. get_market_stats — Get market overview and statistics
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface MCPTool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOLS: MCPTool[] = [
|
||||||
|
{
|
||||||
|
name: 'search_transceivers',
|
||||||
|
description: 'Search for optical transceivers by part number, speed, wavelength, reach, connector, vendor, or form factor. Returns matching transceivers with specs and pricing.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: { type: 'string', description: 'Search text (part number, name, or description)' },
|
||||||
|
category: { type: 'string', description: 'Form factor category (SFP, SFP+, SFP28, QSFP+, QSFP28, QSFP-DD, OSFP)' },
|
||||||
|
data_rate: { type: 'number', description: 'Data rate in Gbps (1, 10, 25, 40, 100, 200, 400, 800)' },
|
||||||
|
wavelength: { type: 'number', description: 'Wavelength in nm (850, 1310, 1550, etc.)' },
|
||||||
|
max_reach_km: { type: 'number', description: 'Maximum reach in km' },
|
||||||
|
connector: { type: 'string', description: 'Connector type (LC, SC, MPO-12, copper_rj45, etc.)' },
|
||||||
|
vendor: { type: 'string', description: 'Vendor slug (cisco, juniper, arista, flexoptix, fs-com, etc.)' },
|
||||||
|
limit: { type: 'number', description: 'Max results (default 20)', default: 20 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_transceiver',
|
||||||
|
description: 'Get full details for a specific transceiver by ID or part number, including specs, pricing history, and compatible devices.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Transceiver ID' },
|
||||||
|
part_number: { type: 'string', description: 'Part number (e.g., SFP-10G-SR, QSFP-100G-LR4-S)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'compare_prices',
|
||||||
|
description: 'Compare current prices across all vendors for a transceiver part number. Shows OEM vs compatible pricing.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
part_number: { type: 'string', description: 'Part number to compare prices for' },
|
||||||
|
},
|
||||||
|
required: ['part_number'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'check_compatibility',
|
||||||
|
description: 'Check if a specific transceiver is compatible with a specific network device (switch/router).',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
transceiver_part: { type: 'string', description: 'Transceiver part number' },
|
||||||
|
device_model: { type: 'string', description: 'Network device model (e.g., C9300-48P, QFX5120-48Y)' },
|
||||||
|
},
|
||||||
|
required: ['transceiver_part', 'device_model'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'find_compatible_transceivers',
|
||||||
|
description: 'Find all compatible transceivers for a given network device, optionally filtered by speed or type.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
device_model: { type: 'string', description: 'Device model' },
|
||||||
|
category: { type: 'string', description: 'Filter by category (SFP+, QSFP28, etc.)' },
|
||||||
|
data_rate: { type: 'number', description: 'Filter by data rate' },
|
||||||
|
},
|
||||||
|
required: ['device_model'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'search_devices',
|
||||||
|
description: 'Search network switches and routers by vendor, model series, or port configuration.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: { type: 'string', description: 'Search text (model number or series)' },
|
||||||
|
vendor: { type: 'string', description: 'Vendor slug' },
|
||||||
|
device_type: { type: 'string', description: 'Type: switch, router, firewall' },
|
||||||
|
min_ports_qsfp_dd: { type: 'number', description: 'Minimum QSFP-DD ports' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_form_factor_info',
|
||||||
|
description: 'Get technical details about a transceiver form factor (dimensions, power, lanes, max speed, generation).',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string', description: 'Form factor name (SFP+, QSFP-DD, OSFP, etc.)' },
|
||||||
|
},
|
||||||
|
required: ['name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_hype_cycle',
|
||||||
|
description: 'Get the current hype cycle phase for a transceiver technology (e.g., 400G ZR, Silicon Photonics, LPO).',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
technology: { type: 'string', description: 'Technology name' },
|
||||||
|
},
|
||||||
|
required: ['technology'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'search_faq',
|
||||||
|
description: 'Search the transceiver knowledge base for troubleshooting, compatibility guides, and technical articles.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: { type: 'string', description: 'Search text' },
|
||||||
|
category: { type: 'string', description: 'Article category' },
|
||||||
|
},
|
||||||
|
required: ['query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_vendor_catalog',
|
||||||
|
description: 'List all transceivers from a specific vendor, grouped by category.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
vendor: { type: 'string', description: 'Vendor slug (cisco, juniper, flexoptix, fs-com, etc.)' },
|
||||||
|
category: { type: 'string', description: 'Filter by category' },
|
||||||
|
},
|
||||||
|
required: ['vendor'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'suggest_alternative',
|
||||||
|
description: 'Find compatible/cheaper alternatives for an OEM transceiver. Compares specs and prices.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
part_number: { type: 'string', description: 'OEM part number to find alternatives for' },
|
||||||
|
},
|
||||||
|
required: ['part_number'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_market_stats',
|
||||||
|
description: 'Get transceiver market overview: total products, vendors, pricing trends, popular categories.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Tool handlers
|
||||||
|
async function handleTool(name: string, args: Record<string, unknown>): Promise<unknown> {
|
||||||
|
switch (name) {
|
||||||
|
case 'search_transceivers': {
|
||||||
|
let sql = `SELECT t.id, t.part_number, t.name, t.category, t.subcategory, t.data_rate, t.data_rate_unit,
|
||||||
|
t.max_reach, t.reach_unit, t.wavelength_nm, t.connector, t.fiber_type, t.status,
|
||||||
|
v.name AS vendor_name, v.slug AS vendor_slug, ff.name AS form_factor
|
||||||
|
FROM transceivers t JOIN vendors v ON t.vendor_id = v.id LEFT JOIN form_factors ff ON t.form_factor_id = ff.id WHERE 1=1`;
|
||||||
|
const params: unknown[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
if (args.query) { sql += ` AND (t.part_number ILIKE $${idx} OR t.name ILIKE $${idx})`; params.push(`%${args.query}%`); idx++; }
|
||||||
|
if (args.category) { sql += ` AND t.category = $${idx}`; params.push(args.category); idx++; }
|
||||||
|
if (args.data_rate) { sql += ` AND t.data_rate = $${idx}`; params.push(args.data_rate); idx++; }
|
||||||
|
if (args.wavelength) { sql += ` AND t.wavelength_nm = $${idx}`; params.push(args.wavelength); idx++; }
|
||||||
|
if (args.connector) { sql += ` AND t.connector = $${idx}`; params.push(args.connector); idx++; }
|
||||||
|
if (args.vendor) { sql += ` AND v.slug = $${idx}`; params.push(args.vendor); idx++; }
|
||||||
|
|
||||||
|
sql += ` ORDER BY t.data_rate DESC, t.name LIMIT $${idx}`;
|
||||||
|
params.push(args.limit ?? 20);
|
||||||
|
|
||||||
|
const result = await query(sql, params);
|
||||||
|
return { transceivers: result.rows, count: result.rowCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get_transceiver': {
|
||||||
|
let sql = `SELECT t.*, v.name AS vendor_name, v.slug AS vendor_slug, ff.name AS form_factor, ff.full_name AS form_factor_full_name
|
||||||
|
FROM transceivers t JOIN vendors v ON t.vendor_id = v.id LEFT JOIN form_factors ff ON t.form_factor_id = ff.id`;
|
||||||
|
const params: unknown[] = [];
|
||||||
|
|
||||||
|
if (args.id) { sql += ` WHERE t.id = $1`; params.push(args.id); }
|
||||||
|
else if (args.part_number) { sql += ` WHERE t.part_number ILIKE $1`; params.push(args.part_number); }
|
||||||
|
else return { error: 'Provide id or part_number' };
|
||||||
|
|
||||||
|
const result = await query(sql, params);
|
||||||
|
if (!result.rows[0]) return { error: 'Transceiver not found' };
|
||||||
|
|
||||||
|
// Get latest prices
|
||||||
|
const prices = await query(
|
||||||
|
`SELECT DISTINCT ON (v.slug) p.price, p.currency, p.in_stock, v.name AS vendor_name, v.vendor_type, p.time
|
||||||
|
FROM prices p JOIN vendors v ON p.vendor_id = v.id
|
||||||
|
WHERE p.transceiver_id = $1 ORDER BY v.slug, p.time DESC`,
|
||||||
|
[result.rows[0].id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...result.rows[0], prices: prices.rows };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'compare_prices': {
|
||||||
|
const result = await query(
|
||||||
|
`SELECT DISTINCT ON (v.slug) t.part_number, t.name, v.name AS vendor, v.vendor_type,
|
||||||
|
p.price, p.currency, p.price_usd, p.in_stock, p.condition, p.url, p.time
|
||||||
|
FROM prices p JOIN transceivers t ON p.transceiver_id = t.id JOIN vendors v ON p.vendor_id = v.id
|
||||||
|
WHERE t.part_number ILIKE $1 ORDER BY v.slug, p.time DESC`,
|
||||||
|
[`%${args.part_number}%`]
|
||||||
|
);
|
||||||
|
return { prices: result.rows, count: result.rowCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get_form_factor_info': {
|
||||||
|
const result = await query(
|
||||||
|
`SELECT ff.*, (SELECT COUNT(*) FROM transceivers t WHERE t.form_factor_id = ff.id) AS total_transceivers
|
||||||
|
FROM form_factors ff WHERE ff.name ILIKE $1`,
|
||||||
|
[args.name]
|
||||||
|
);
|
||||||
|
return result.rows[0] ?? { error: 'Form factor not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get_vendor_catalog': {
|
||||||
|
const result = await query(
|
||||||
|
`SELECT t.part_number, t.name, t.category, t.subcategory, t.data_rate, t.data_rate_unit, t.status
|
||||||
|
FROM transceivers t JOIN vendors v ON t.vendor_id = v.id
|
||||||
|
WHERE v.slug = $1 ${args.category ? 'AND t.category = $2' : ''}
|
||||||
|
ORDER BY t.category, t.data_rate DESC`,
|
||||||
|
args.category ? [args.vendor, args.category] : [args.vendor]
|
||||||
|
);
|
||||||
|
return { vendor: args.vendor, transceivers: result.rows, count: result.rowCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'suggest_alternative': {
|
||||||
|
// Find the original transceiver
|
||||||
|
const orig = await query(
|
||||||
|
`SELECT t.*, v.vendor_type FROM transceivers t JOIN vendors v ON t.vendor_id = v.id
|
||||||
|
WHERE t.part_number ILIKE $1 LIMIT 1`,
|
||||||
|
[args.part_number]
|
||||||
|
);
|
||||||
|
if (!orig.rows[0]) return { error: 'Original transceiver not found' };
|
||||||
|
const o = orig.rows[0];
|
||||||
|
|
||||||
|
// Find alternatives with matching specs
|
||||||
|
const alts = await query(
|
||||||
|
`SELECT t.part_number, t.name, v.name AS vendor_name, v.vendor_type,
|
||||||
|
t.data_rate, t.max_reach, t.wavelength_nm, t.connector
|
||||||
|
FROM transceivers t JOIN vendors v ON t.vendor_id = v.id
|
||||||
|
WHERE t.category = $1 AND t.data_rate = $2 AND t.wavelength_nm = $3
|
||||||
|
AND t.id != $4
|
||||||
|
ORDER BY v.vendor_type, v.name LIMIT 20`,
|
||||||
|
[o.category, o.data_rate, o.wavelength_nm, o.id]
|
||||||
|
);
|
||||||
|
return { original: { part_number: o.part_number, vendor_type: o.vendor_type }, alternatives: alts.rows };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get_market_stats': {
|
||||||
|
const [total, byCategory, byVendorType, recentPrices] = await Promise.all([
|
||||||
|
query('SELECT COUNT(*) AS total FROM transceivers'),
|
||||||
|
query('SELECT category, COUNT(*) AS count FROM transceivers GROUP BY category ORDER BY count DESC'),
|
||||||
|
query('SELECT v.vendor_type, COUNT(*) AS count FROM transceivers t JOIN vendors v ON t.vendor_id = v.id GROUP BY v.vendor_type'),
|
||||||
|
query(`SELECT COUNT(*) AS count, AVG(price_usd) AS avg_price FROM prices WHERE time > NOW() - INTERVAL '7 days'`),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
total_transceivers: parseInt(total.rows[0]?.total ?? '0'),
|
||||||
|
by_category: byCategory.rows,
|
||||||
|
by_vendor_type: byVendorType.rows,
|
||||||
|
recent_prices: recentPrices.rows[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { error: `Unknown tool: ${name}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE MCP Protocol
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
const url = new URL(req.url ?? '/', `http://localhost:${PORT}`);
|
||||||
|
|
||||||
|
if (url.pathname === '/sse') {
|
||||||
|
// SSE endpoint
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send server info
|
||||||
|
const serverInfo = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method: 'notifications/initialized',
|
||||||
|
params: {
|
||||||
|
protocolVersion: '2024-11-05',
|
||||||
|
serverInfo: { name: 'transceiver-intelligence-platform', version: '0.1.0' },
|
||||||
|
capabilities: { tools: { listChanged: false } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.write(`data: ${JSON.stringify(serverInfo)}\n\n`);
|
||||||
|
|
||||||
|
// Keep alive
|
||||||
|
const keepAlive = setInterval(() => { res.write(':keepalive\n\n'); }, 30000);
|
||||||
|
req.on('close', () => clearInterval(keepAlive));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/message' && req.method === 'POST') {
|
||||||
|
let body = '';
|
||||||
|
for await (const chunk of req) body += chunk;
|
||||||
|
const msg = JSON.parse(body);
|
||||||
|
|
||||||
|
let response: unknown;
|
||||||
|
|
||||||
|
if (msg.method === 'tools/list') {
|
||||||
|
response = { jsonrpc: '2.0', id: msg.id, result: { tools: TOOLS } };
|
||||||
|
} else if (msg.method === 'tools/call') {
|
||||||
|
try {
|
||||||
|
const result = await handleTool(msg.params.name, msg.params.arguments ?? {});
|
||||||
|
response = {
|
||||||
|
jsonrpc: '2.0', id: msg.id,
|
||||||
|
result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] },
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
response = {
|
||||||
|
jsonrpc: '2.0', id: msg.id,
|
||||||
|
error: { code: -32603, message: err instanceof Error ? err.message : 'Internal error' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response = { jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'Method not found' } };
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||||
|
res.end(JSON.stringify(response));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ name: 'TIP MCP Server', version: '0.1.0', tools: TOOLS.length }));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, '0.0.0.0', () => {
|
||||||
|
log.info({ port: PORT }, 'TIP MCP server started');
|
||||||
|
});
|
||||||
164
src/types/index.ts
Normal file
164
src/types/index.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
// Transceiver Intelligence Platform — Core Types
|
||||||
|
|
||||||
|
export type TransceiverStatus = 'active' | 'eol' | 'pre_release' | 'nrnd' | 'unknown';
|
||||||
|
export type DataRateUnit = 'Mbps' | 'Gbps' | 'Tbps';
|
||||||
|
export type ReachUnit = 'm' | 'km';
|
||||||
|
export type TemperatureRange = 'commercial' | 'extended' | 'industrial';
|
||||||
|
export type DOMType = 'none' | 'ddm' | 'ddmi' | 'cmis' | 'sff8472' | 'sff8636';
|
||||||
|
export type ConnectorType =
|
||||||
|
| 'LC' | 'SC' | 'MPO-12' | 'MPO-16' | 'MPO-24' | 'CS' | 'SN'
|
||||||
|
| 'FC' | 'ST' | 'MTRJ' | 'E2000' | 'copper_rj45' | 'cx4'
|
||||||
|
| 'dac_passive' | 'dac_active' | 'aoc' | 'none' | 'other';
|
||||||
|
export type FiberType =
|
||||||
|
| 'smf' | 'mmf_om1' | 'mmf_om2' | 'mmf_om3' | 'mmf_om4' | 'mmf_om5'
|
||||||
|
| 'copper' | 'dac' | 'aoc' | 'free_space' | 'other';
|
||||||
|
export type WavelengthBand =
|
||||||
|
| 'O' | 'E' | 'S' | 'C' | 'L' | 'U' | 'visible'
|
||||||
|
| 'cwdm' | 'dwdm' | 'lwdm' | 'swdm' | 'other';
|
||||||
|
export type VendorType = 'oem' | 'compatible' | 'distributor' | 'manufacturer' | 'marketplace' | 'refurbished';
|
||||||
|
export type PriceCurrency = 'USD' | 'EUR' | 'GBP' | 'CNY' | 'JPY' | 'KRW' | 'TWD' | 'THB' | 'INR' | 'CAD' | 'AUD';
|
||||||
|
export type HypePhase =
|
||||||
|
| 'innovation_trigger' | 'peak_inflated' | 'trough_disillusionment'
|
||||||
|
| 'slope_enlightenment' | 'plateau_productivity' | 'decline';
|
||||||
|
export type CrawlStatus = 'pending' | 'running' | 'success' | 'failed' | 'rate_limited';
|
||||||
|
export type MediaType = 'image' | 'datasheet' | 'manual' | 'diagram' | 'video' | 'certificate';
|
||||||
|
|
||||||
|
export interface Standard {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
body: string;
|
||||||
|
version?: string;
|
||||||
|
year?: number;
|
||||||
|
url?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormFactor {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
full_name?: string;
|
||||||
|
standard_id?: number;
|
||||||
|
lanes?: number;
|
||||||
|
max_data_rate?: number;
|
||||||
|
data_rate_unit?: DataRateUnit;
|
||||||
|
width_mm?: number;
|
||||||
|
height_mm?: number;
|
||||||
|
depth_mm?: number;
|
||||||
|
power_max_w?: number;
|
||||||
|
generation?: number;
|
||||||
|
release_year?: number;
|
||||||
|
eol_year?: number;
|
||||||
|
description?: string;
|
||||||
|
image_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Vendor {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
vendor_type: VendorType;
|
||||||
|
website?: string;
|
||||||
|
logo_url?: string;
|
||||||
|
country?: string;
|
||||||
|
founded_year?: number;
|
||||||
|
description?: string;
|
||||||
|
is_oem?: boolean;
|
||||||
|
is_factory?: boolean;
|
||||||
|
aliases?: string[];
|
||||||
|
scrape_url?: string;
|
||||||
|
scrape_enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transceiver {
|
||||||
|
id?: number;
|
||||||
|
part_number: string;
|
||||||
|
vendor_id: number;
|
||||||
|
form_factor_id?: number;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
category?: string;
|
||||||
|
subcategory?: string;
|
||||||
|
data_rate?: number;
|
||||||
|
data_rate_unit?: DataRateUnit;
|
||||||
|
max_reach?: number;
|
||||||
|
reach_unit?: ReachUnit;
|
||||||
|
wavelength_nm?: number;
|
||||||
|
wavelength_rx?: number;
|
||||||
|
wavelengths?: number[];
|
||||||
|
wavelength_band?: WavelengthBand;
|
||||||
|
tx_power_min?: number;
|
||||||
|
tx_power_max?: number;
|
||||||
|
rx_sensitivity?: number;
|
||||||
|
link_budget_db?: number;
|
||||||
|
connector?: ConnectorType;
|
||||||
|
fiber_type?: FiberType;
|
||||||
|
duplex?: boolean;
|
||||||
|
breakout?: string;
|
||||||
|
temp_range?: TemperatureRange;
|
||||||
|
temp_min_c?: number;
|
||||||
|
temp_max_c?: number;
|
||||||
|
power_consumption_w?: number;
|
||||||
|
dom_support?: DOMType;
|
||||||
|
oem_part_number?: string;
|
||||||
|
oem_vendor_id?: number;
|
||||||
|
status?: TransceiverStatus;
|
||||||
|
release_date?: string;
|
||||||
|
eol_date?: string;
|
||||||
|
image_url?: string;
|
||||||
|
datasheet_url?: string;
|
||||||
|
product_url?: string;
|
||||||
|
tags?: string[];
|
||||||
|
raw_specs?: Record<string, unknown>;
|
||||||
|
source?: string;
|
||||||
|
source_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkDevice {
|
||||||
|
id?: number;
|
||||||
|
vendor_id: number;
|
||||||
|
model: string;
|
||||||
|
series?: string;
|
||||||
|
device_type?: string;
|
||||||
|
ports_sfp?: number;
|
||||||
|
ports_sfp_plus?: number;
|
||||||
|
ports_sfp28?: number;
|
||||||
|
ports_qsfp_plus?: number;
|
||||||
|
ports_qsfp28?: number;
|
||||||
|
ports_qsfp_dd?: number;
|
||||||
|
ports_osfp?: number;
|
||||||
|
ports_cfp?: number;
|
||||||
|
ports_rj45?: number;
|
||||||
|
max_throughput?: string;
|
||||||
|
release_year?: number;
|
||||||
|
eol_date?: string;
|
||||||
|
status?: string;
|
||||||
|
image_url?: string;
|
||||||
|
product_url?: string;
|
||||||
|
manual_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Price {
|
||||||
|
time?: string;
|
||||||
|
transceiver_id: number;
|
||||||
|
vendor_id: number;
|
||||||
|
price: number;
|
||||||
|
currency?: PriceCurrency;
|
||||||
|
price_usd?: number;
|
||||||
|
quantity_min?: number;
|
||||||
|
quantity_max?: number;
|
||||||
|
in_stock?: boolean;
|
||||||
|
stock_quantity?: number;
|
||||||
|
lead_time_days?: number;
|
||||||
|
condition?: string;
|
||||||
|
url?: string;
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrawlerConfig {
|
||||||
|
name: string;
|
||||||
|
baseUrl: string;
|
||||||
|
interval: number;
|
||||||
|
maxConcurrency: number;
|
||||||
|
maxRequestsPerMinute: number;
|
||||||
|
usePlaywright: boolean;
|
||||||
|
}
|
||||||
88
src/utils/db.ts
Normal file
88
src/utils/db.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import pg from 'pg';
|
||||||
|
import { pino } from 'pino';
|
||||||
|
|
||||||
|
const log = pino({ name: 'db' });
|
||||||
|
|
||||||
|
const pool = new pg.Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
max: 20,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
connectionTimeoutMillis: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
pool.on('error', (err) => {
|
||||||
|
log.error({ err }, 'Unexpected pool error');
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function query<T extends pg.QueryResultRow = any>(
|
||||||
|
text: string,
|
||||||
|
params?: unknown[]
|
||||||
|
): Promise<pg.QueryResult<T>> {
|
||||||
|
const start = Date.now();
|
||||||
|
const result = await pool.query<T>(text, params);
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
if (duration > 1000) {
|
||||||
|
log.warn({ text: text.slice(0, 100), duration, rows: result.rowCount }, 'Slow query');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClient() {
|
||||||
|
return pool.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transaction<T>(fn: (client: pg.PoolClient) => Promise<T>): Promise<T> {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
const result = await fn(client);
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function batchInsert(
|
||||||
|
table: string,
|
||||||
|
columns: string[],
|
||||||
|
rows: unknown[][],
|
||||||
|
onConflict?: string
|
||||||
|
): Promise<number> {
|
||||||
|
if (rows.length === 0) return 0;
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
let inserted = 0;
|
||||||
|
const batchSize = 100;
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i += batchSize) {
|
||||||
|
const batch = rows.slice(i, i + batchSize);
|
||||||
|
const placeholders = batch.map((row, ri) =>
|
||||||
|
`(${row.map((_, ci) => `$${ri * row.length + ci + 1}`).join(', ')})`
|
||||||
|
).join(', ');
|
||||||
|
|
||||||
|
const values = batch.flat();
|
||||||
|
const conflict = onConflict ? ` ON CONFLICT ${onConflict}` : '';
|
||||||
|
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES ${placeholders}${conflict}`;
|
||||||
|
|
||||||
|
const result = await client.query(sql, values);
|
||||||
|
inserted += result.rowCount ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return inserted;
|
||||||
|
} catch (e) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { pool };
|
||||||
|
export default { query, getClient, transaction, batchInsert, pool };
|
||||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "test"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user