Compare commits

..

268 Commits

Author SHA1 Message Date
Rene Fichtmueller
ba998f4c01 fix: vendor_compat 0%→100%, price denorm, wiitek disabled, price-denorm scheduler
- Migration 094: images for 12 Cisco 8K MPA + A9K-8HG-FLEX + ASR-9000V models
- Migration 095: price denorm refresh (EUR 679→1376, USD 166→835 with 180d window)
- Migration 096: bulk vendor_compat by form_factor — all 9013 transceivers now
  have OEM compatibility patterns (was 0/9013 because all slugs are scraped-*)
- wiitek.ts: disable dead scraper (wiitek.com unreachable since 2026-04, EAI_AGAIN)
- scheduler.ts: add compute:price-denorm job (daily 05:30 UTC) to keep
  street_price_usd/price_verified_eur fresh without manual migration runs
- seed-from-npm.ts: ON CONFLICT now also updates vendor_compat (was only updated_at)
2026-04-25 08:55:21 +02:00
Rene Fichtmueller
108b2687d6 data: migration 093 — NCS 5500 main line card images (+6 models, 95.1%→96.0%)
NC55-18H18F: it-market.com JPEG.
NC55-24X100G-SE: networkgenetics.net BigCommerce CDN 317KB PNG.
NC55-32T16Q4H-A: hummingbirdnetworks.com BigCommerce CDN 32KB.
NC55-36X100G-S: dedicatednetworksinc.com WordPress CDN 1.2MB PNG.
NC55-MOD-A-S: stack-systems.com Magento CDN.
NC55-MOD-A-SE-S: core92.com Odoo CDN 52KB.
2026-04-25 07:51:15 +02:00
Rene Fichtmueller
3722cb4b10 data: migration 092 — Cisco image backfill (+48 models, 88.2%→95.1%)
A99 ASR9900 line cards: router-switch.com CDN + SQL subquery reuse from
A9K counterparts (identical hardware) + NetworkOutlet/ZionNetworking/
NetworkTigers for models not on router-switch.
Catalyst 9600 LC/SUP: NetworkTigers + ITBargainCenter CDN.
NC55-MPA adapters: router-switch.com (157KB each).
NC55-24H12F-SE: networkgenetics.net BigCommerce CDN.
A900-IMA modules: NetworkTigers + router-switch.
A9903-20HG-PEC: TopParagonResource webp.
ASR-9922-RP-SE: NetworkTigers.
A9K-4HG-FLEX-X variants: SQL subquery from A9K-4HG-FLEX counterparts.
2026-04-25 07:48:57 +02:00
Rene Fichtmueller
75ab607467 feat(data): migration 091 — Arista/Juniper SONiC HCL images (+7 models)
7060CX-32S, 7050QX-32, 7050QX-32S, 7170-32CD, 7280CR3-32D4 (arista)
QFX5200-32C-S, QFX5210-64C (juniper)
All via NetworkTigers Shopify CDN (full-res, 40–120KB)
Estimated coverage: 616 → 623
2026-04-21 23:09:46 +02:00
Rene Fichtmueller
2894013684 feat(data): migration 090 + apply script — Edgecore AS-series SONiC switches
7 models: AS7312-54X, AS7312-54XS, AS7326-56X, AS7716-32X, AS7816-64X,
AS9716-32D, AS7512-32X. All from SONiC HCL Accton/Edgecore vendor.
Sources: edge-core.com official WP CDN + stordis.com + epsglobal.com.
Also adds scripts/apply-pending-migrations.sh for Erik DB catchup.
2026-04-21 23:02:25 +02:00
Rene Fichtmueller
7cc910ce16 feat(data): migration 089 — Arista 7800R4/Cisco 8000+Catalyst+Nexus/Juniper PTX images
8 models: 7800R4-36D2-LC, 8101-32FH, 8111-32EH, C9300X-24Y, C9500-48Y4C,
N9K-C93108TC-FX3P, PTX10001-36MR, PTX10004.
Coverage: 601 → 609 (89.6% → 90.8%).
2026-04-21 22:39:06 +02:00
Rene Fichtmueller
f6218ad698 data: add image migration 088 — Ubiquiti USW + Phoenix Contact (3 models)
USW-Enterprise-48-PoE + USW-Aggregation (cdn.ecomm.ui.com official Ubiquiti
ecommerce CDN), FL SWITCH 7528-2S (rspsupply.com distributor CDN, product
ID 2891026, same pattern as migration 079). Coverage: 598 → 601 (89.6%).
2026-04-21 22:23:13 +02:00
Rene Fichtmueller
85b9803463 data: add image migration 087 — Cisco A9K license-variant reuse (3 models)
A9K-4HG-FLEX-FC, A9K-8X100G-LB-TR, A9K-4X100GE base: all share identical
hardware with previously-covered -TR/-SE siblings. FC/SE/TR are Cisco
software license suffixes on the same physical line card. Coverage: 595 → 598.
2026-04-21 22:20:19 +02:00
Rene Fichtmueller
cd8d7e816f data: add image migration 086 — Asterfusion/Arista/Ruckus/misc (8 models)
CX732Q-N + CX564P-N (Asterfusion, official WP CDN), 7130-48LB (Arista,
Alta Technologies reseller), ICX 7850-48FS (Ruckus, networktigers.com),
E810-CQDA2 (Intel, ESAITech CDN), FSP 3000 CloudConnect (Adtran/ADVA,
nwrusa.com), MediorNet MicroN UHD (Riedel, riedel.net fileadmin CDN),
nGeniusONE InfiniStreamNG (Netscout, netscout.com Pantheon CDN).
Coverage: 587 → 595 (87.5% → 88.7%).
2026-04-21 22:13:54 +02:00
Rene Fichtmueller
219b00523f data: add image migration 085 — mixed straggler batch (9 switches)
New coverage: N540-24Q2C2DD-SYS (Cisco TD CDN), Apollo 9900 Series /
Ribbon (ribboncommunications.com), Seastone2 / Midstone-200i / Celestica
(servethehome.com + celestica.com/uploadedImages), RA-B6510-48V8C / Ragile
(Micas ODM equiv, BigCommerce CDN), QuantaMesh T7064-IX1D / QCT (IX4
EOL family image). CDN upgrade: DS3000/DS4000/DS5000 foleon → official
celestica.com/uploadedImages. Coverage: 581 → 587 (86.6% → 87.5%).
2026-04-21 22:03:08 +02:00
Rene Fichtmueller
6357145316 data: add image migrations 082-084 — Cisco A9K line cards, NC57/NCS1K14, A9K bulk
Migration 082 (13 models): A9K-4HG-FLEX-TR/SE, A9K-4T-B/E/L, A9K-4T16GE-SE/TR,
A9K-4X100GE-FC/SE/TR, A9K-MOD400-SE/TR, A9K-MOD80-SE/TR.
Sources: networktigers.com Shopify CDN + router-switch.com.

Migration 083 (15 models): NC57-18DD-SE/24DD/36H-SE/36H6D-S/48Q2D-S/48Q2D-SE-S/
MOD-S/MOD-SE-S + NC57-MPA-12L-S/1FH1D-S/2D4H-S + NCS1K14-2.4T-K9/L-K9/X-K9/TXL-K9.
Sources: Cisco TD CDN (numbered IDs) + datasheet-c78-742016 JCR renditions.

Migration 084 (62 models): A9K-8T/4-B/E/L (slash-format fix), A9K-16T/8-B,
1X/2X/8X/16X 100GE, 20HG-FLEX-FC/SE/TR, 24X10GE variants, 2T20GE-B/E/L,
36X10GE-SE/TR, 40GE-B/E/L/SE/TR, 48X10GE variants, RSP440/RSP880, SIP-700,
all MPA modules (11 non-FC + 3 dedicated FC + 6 FC reuse non-FC hardware image).
Sources: networktigers.com + cdn.shopify.com + router-switch.com + cloudappliances.co.uk.

Coverage after 084: 519 → ~581 (77.3% → ~86.6%).
2026-04-21 21:20:47 +02:00
Rene Fichtmueller
772af1b6fb data: add image migrations 080-081 — Avaya, Advantech, Cisco N540/N540X/N560-IMA/ASR-9900-RP
Migration 080 (3 images): Avaya ERS 4950GTS-PWR+ (planetrefurbished.com),
Advantech EKI-7720G-4FI and EKI-9516G-4GMXP (advdownload.advantech.com CDN).

Migration 081 (26 UPDATEs): Cisco N560 IMA modules (4 models, Cisco TD CDN
524xxx range), NCS 540 fixed-port (6Z/FH-AGG/FH-CSR/24Q8L2DD, TD CDN + manualslib),
NCS 540X (10 models, TD CDN 521-522xxx + eBay CDN + signellent.com),
ASR-9900-RP-SE/TR (brightstarsystems.com); upgrades ASR-9902/9903 and NCS1001/1002-K9
to official Cisco support CDN. Coverage: 466 → 491 (69.4% → 73.2%).
2026-04-21 16:00:36 +02:00
Rene Fichtmueller
50f0e738c9 feat(data): image migrations 078-079 — Cisco NCS1K4/ASR9000v, WAGO, WatchGuard, ADTRAN, Phoenix Contact
Migration 078: 21 Cisco models — ASR-9001-S, ASR-9000V-AC/DC-A, A9KV-V2-DC-A/DC-E
  (specific images), NCS1001/1002/1K-EDFA, all 13 NCS1K4 chassis variants.
  Sources: networktigers.com + router-switch.com

Migration 079: WAGO 852-1505, WatchGuard Firebox M5800, ADTRAN NetVanta 1560-48P,
  Phoenix Contact FL SWITCH 4808E-16FX-4GC (4 models).
  Sources: gilautomation.com Shopify, watchguard.com help-center, portal.adtran.com

Coverage: 443 → 466 images (66.0% → 69.4%)
2026-04-21 15:01:39 +02:00
Rene Fichtmueller
356add2e49 feat(data): image migrations 075-077 — Cisco NCS 9300 SE1, QCT, QNAP, Sophos, Barracuda, Peplink, Westermo
Migration 075: Cisco Nexus 9300 SE1 (7 models) — poster-image + support CDN
Migration 076: QuantaMesh (3), QNAP QSW-M5216-1T, Sophos CS210-48FP (5 models)
Migration 077: Barracuda F900, Peplink SD Switch 24-Port, Westermo Lynx 5612 (3 models)

Coverage: 428 → 443 images (63.8% → 66.0%)
2026-04-21 14:49:29 +02:00
Rene Fichtmueller
ba5ab9c502 chore: redact internal IP from CHANGELOG before public push 2026-04-21 14:31:07 +02:00
Rene Fichtmueller
5d7a7a9876 data: add switch images migrations 073-074 (Cisco ASR9k/NCS540 + multi-vendor)
Migration 073 — Cisco ASR 9000 + NCS 540 Series (18 entries):
  ASR-9001/9901/9902/9903 + FC fabric-card variants
  A9KV-V2-AC/DC-A/DC-E satellite shelf
  N540-28Z4C, N540-24Z8Q2C, N540-12Z20G, N540-ACC, N540X-16Z4G8Q2C
  Sources: networktigers.com Shopify CDN, tempestns.com WP CDN
  Coverage: 400 → 418 (62.3%)

Migration 074 — Extreme / Ruijie / Ruckus / ZTE / Edgecore (10 entries):
  Extreme SLX 9740-40C, X695-48Y-8C, 5520-48T
  Ruijie RG-S6920-4C, RG-S5760C-24SFP/8GT8XS-X
  Ruckus ICX 7150-48PF, ICX 7550-48ZP
  ZTE ZXR10 5960-56PM-H, ZXR10 9908
  Edgecore AS7712-32X
  Coverage: 418 → 428 (63.8%)

All URLs verified HTTP 200. A/D power variants share chassis images.
2026-04-21 14:30:42 +02:00
Rene Fichtmueller
4e927acf55 data: switch image coverage 065-072 — Cisco/Juniper/Arista/NVIDIA/Huawei/Nokia/Dell/Extreme/HPE/Ubiquiti/Supermicro/Celestica/Asterfusion/FS.com/Edgecore
Migrations 065-072: 72 verified image URLs across all 17 tier-1 vendors.
- 065: Cisco 8000/Catalyst/Nexus/NCS (14) — cisco.com/c/dam doc CDN
- 066: Juniper EX/MX/QFX (10) — juniper.net/content/dam image library
- 067: Arista remaining 7000-series (11) — arista.com QSG front-panel PNGs
- 068: NVIDIA Mellanox SN-series (5) — docscontent.nvidia.com dims4 CDN
- 069: Huawei CloudEngine/NE40E (5) + Nokia IXR-D3L/7750 SR-14s (2)
- 070: Dell PowerSwitch ON-series (5) + Extreme Networks 8720/X465 (2)
- 071: HPE Aruba CX 6300M/8100/8360 (3) + Ubiquiti USW (3) + Supermicro (2)
- 072: Celestica DS3000/4000/5000 (3) + Asterfusion CX-N (3) + FS.com (2) + Edgecore (2)

All URLs verified HTTP 200 (2026-04-21). 5 models skipped (no public image found):
Arista 7280R3A-48D5, 750-36Y; NVIDIA SN3750-SX; Nokia 7750 SR-1 (hotlink-protected), 7220 IXR-H4.
2026-04-21 11:34:38 +02:00
Rene Fichtmueller
382de40d5d data: Avaya/NetApp/Keysight/A10/Evertz/RAD/Ekinops/DrayTek/Fujitsu/Broadcom/Calix/Citrix images (migration 064) 2026-04-21 10:18:51 +02:00
Rene Fichtmueller
3cd6d44ea5 data: Sophos XGS 6500 image + Zyxel XS3800-28 URL fix (migration 063) 2026-04-21 10:16:51 +02:00
Rene Fichtmueller
b7e8097a57 data: Cambium/Gigamon/SonicWall/Planet/PaloAlto/Westermo/Zyxel/Synology/TRENDnet/Waystream/Kemp/LANCOM images (migrations 061-062) 2026-04-21 10:10:32 +02:00
Rene Fichtmueller
906619e602 docs: update CHANGELOG_PENDING with migrations 055-060 2026-04-21 09:57:06 +02:00
Rene Fichtmueller
d3eb53ad1b data: HPE Aruba CX + Extreme Networks images 0%→100% (migration 060) 2026-04-21 09:56:11 +02:00
Rene Fichtmueller
6827f6081f data: Arista/Edgecore/Fortinet/Dell/Huawei/D-Link/Netgear/Check Point/Ruckus images (migrations 057-059) 2026-04-21 09:55:22 +02:00
Rene Fichtmueller
3b42f0aa90 data: ALE/H3C/Hirschmann/Ciena/Netberg images 0%→100% (migration 056) 2026-04-21 09:50:46 +02:00
Rene Fichtmueller
b9ab664a56 data: MikroTik CRS/CCR CDN images + NVIDIA ConnectX-7 (migration 055) 2026-04-21 09:48:30 +02:00
Rene Fichtmueller
e2061c6c4d data: direct image injection for Nokia, F5, Delta Networks, Siemens, TP-Link
Migration 051: TP-Link TL-SG3452XP + TL-SX3016F via static.tp-link.com CDN
Migration 052: Nokia 6/6 — 7220 IXR-D3L/H4 (docs.nokia.com graphics),
  7250 IXR-10 + 7750 SR-1 (tempestns.com), SR-14s (telecomcauliffe.com),
  SR-1e (docs hardwareBanner — no standalone public image available)
Migration 053: F5 BIG-IP i5800/i10800 (wtit.com), i15800 (blueally CDN)
Migration 054: Delta Networks 4/4 (hardwarenation.com + manualslib),
  Siemens SCALANCE 4/4 — X-200/X-300/X-500 via images.sw.cdn.siemens.com

All 14 URLs verified HTTP 200 with correct image content-type (2026-04-21).
CHANGELOG_PENDING.md updated for all 4 migrations.
2026-04-21 08:42:14 +02:00
Rene Fichtmueller
466cda285b data: image coverage improvements — QCT, Allied Telesis + CHANGELOG update
- sql/046: QCT QuantaMesh T3048-LY8 direct image injection
- sql/050: Allied Telesis 3/3 (AT-x530-28GSX, AT-x530L-52GPX, AT-x950-28XSQ)
  via alliedtelesis.com Drupal static files CDN (og:image, all 200 PNG)
- CHANGELOG: document filter pattern fixes + all 6 new vendor migrations
  (Moxa/UfiSpace/Brocade/NVIDIA/Allied Telesis — 19 new images total)
2026-04-21 08:11:57 +02:00
Rene Fichtmueller
bbc6f560dd fix: add image filter patterns and direct URL migrations for 6 vendors
- switch-image-playwright.ts + switch-image-fetcher.ts: add filter patterns
  for /webimage-404/ (Netgear 404 hero), /Brand/ + /cybersecurity.png/
  (Moxa brand marketing images not product photos)
- sql/047: Moxa 4/4 models — CDN getattachment paths (hotlink-protected,
  Referer: moxa.com required; R2 proxy needed for production display)
- sql/048: UfiSpace 6/6 models — ufispace.com/image/<hash>/ direct PNGs;
  Brocade G720+G730 — broadcom.com og:image; ICX 7850-48FS — CommScope/Ruckus
  vistancenetworks.com ImageServer (rand param is cache-bust only, not auth)
- sql/049: NVIDIA SN-series 6/6 — docscontent.nvidia.com (SN2201/3700/4700)
  and S3 direct (SN5400/5600); SN3750-SX via uvation reseller CDN
2026-04-21 07:57:55 +02:00
Rene Fichtmueller
b65e4452db fix: add error-graphic, icon-library, illustration filters to GENERIC_IMAGE_PATTERNS
- /404[-_]error/i, /error[-_]graphic/i — Broadcom 404-ERROR-GRAPHIC.png
- /\/icon[-_]library\//i — D-Link navigation/icon-library path images
- /[-_]illustration[._]/i — Arista Cloud-Legacy_Illustration and similar diagrams
- Nokia banner, Huawei marketing, banners/ path patterns (Playwright scraper)
- Cookie consent patterns synced to switch-image-fetcher.ts (was only in Playwright)
2026-04-21 07:38:01 +02:00
Rene Fichtmueller
f4afe14af4 feat: add 12 new vendor URL builders to Playwright image scraper
- Nokia, Huawei, Ciena, Moxa, D-Link, Alcatel-Lucent Enterprise,
  Asterfusion, Brocade: passthrough builders (use stored product_page_url)
- NVIDIA Networking: SN-series URL builder (sn5600 → /ethernet-switching/sn5600/)
- Netgear: lowercase model slug builder for /business/wired/switches/fully-managed/
- UfiSpace: hardcoded sitemap-verified URL map (all 6 S9xxx models)
- QCT: hardcoded URL map for T3048-LY8 and T7032-IX1
- Add Nokia banner / Huawei marketing image patterns to GENERIC_IMAGE_PATTERNS
2026-04-21 07:24:11 +02:00
Rene Fichtmueller
6a4b4700cb data: inject Edgecore product images directly (Playwright blocked by 403)
Edgecore blocks headless browsers (Playwright 403) but serves og:image via
plain HTTP. 5 models resolved via direct curl extraction:
- DCS204, DCS510, DCS810, EPS203 → edge-core.com/product/<slug>/
- Minipack2 → minipack-as8000-open-modular-platform product page
AS7535/AS7726/AS7946/AS9516 not on Edgecore's public WooCommerce site.
2026-04-21 07:07:24 +02:00
Rene Fichtmueller
8f36eff956 fix(scraper): filter OneTrust/cookie-consent images + skip in img fallback
cdn.cookielaw.org logos appear as the largest DOM image on Dell/Extreme
product pages when the cookie consent overlay is present. Added to both
GENERIC_IMAGE_PATTERNS (isGenericImage filter) and img fallback skipPattern
so the next-largest actual product image can be found.
2026-04-21 06:45:41 +02:00
Rene Fichtmueller
d67fbe31da fix(scraper): fall through to img fallback when og:image is generic/logo
Previously: if og:image existed (even as a Dell logo URL), page.evaluate() returned
early and the img fallback was never tried. Now: meta tags are extracted first, then
isGenericImage() is checked in Node.js, and the img fallback runs if meta image is null
or generic. This allows vendors like Dell (og:image = logo) to still get product images
via the DOM fallback.
2026-04-21 06:36:12 +02:00
Rene Fichtmueller
09d3a60b7c fix(scraper): fix Edgecore/Extreme URL builders, broaden img fallback, fix ENOENT
- buildEdgecoreUrl: /product/<slug>/ (WooCommerce, no .html) with EDGECORE_SLUG_MAP
  for AS7712-32X→as7712-32x-ec, Minipack2→minipack-as8000-open-modular-platform
- buildFortinetUrl: returns null (all pages redirect to generic, no usable og:image)
- buildExtremeUrl: direct product URL (extremenetworks.com/product/<slug>)
- img fallback: remove strict 'product/switch/router/hardware' path requirement;
  now takes largest image >=200x150px excluding flags/icons/spinners — isGenericImage()
  filters hero/banner/logo afterward
- ENOENT fix: unique per-run Crawlee storage dir (timestamp suffix) prevents
  stale request-queue file contamination between back-to-back vendor runs
2026-04-21 06:33:32 +02:00
Rene Fichtmueller
87b9416592 fix(scraper): fix Arista series-level URL builder + bypass Crawlee URL deduplication
- buildAristaUrl() now extracts series prefix (7060X5-32QS → 7060x5-series)
  instead of individual model URLs that lack og:image
- Strip trailing sub-variant 'A' so R3A → R3 series page
- Add uniqueKey: row.id to each request — prevents Crawlee from deduplicating
  models that share the same series URL (e.g. 7060x5-series)
- For Arista: always prefer fresh builder URL over stored product_page_url
  so stale individual-model URLs don't override correct series pages
2026-04-21 06:22:41 +02:00
Rene Fichtmueller
18a9e1346e feat: Playwright image scraper for bot-blocked vendors (Arista/Dell/Edgecore/Fortinet/Extreme) 2026-04-21 06:16:05 +02:00
Rene Fichtmueller
653824f23b fix: Cisco line card URL mapping (8800/84/86 → 8000 family page, skip ASR9K logo-only) 2026-04-21 00:49:32 +02:00
Rene Fichtmueller
c9333ab5ea fix: MikroTik hardcoded slug map for + models (crs305/312/317/326) 2026-04-21 00:45:41 +02:00
Rene Fichtmueller
9618a4f0e0 fix: Cisco 8000 builder URL + MikroTik lowercase + new vendor builders
URL builder fixes:
- Cisco 8000: update to new /site/us/en/ URL scheme (family page, not per-model)
- MikroTik: fix to lowercase+underscore format (was uppercase, caused 404)
- Fortinet: set to null — JS-rendered pages, all redirect to generic page
- Alcatel-Lucent Enterprise slug added to dispatcher (was missing, caused 0 hits)
- Add Quanta, Allied Telesis, Ufispace, Netgear URL builders
- NVIDIA: skip ConnectX/BlueField non-switch models

Migration 044:
- Clear 35 wrong NCS-5500 URLs from Cisco 8000-series models
- Pre-set correct 8000-series family URL for 21 models without images
2026-04-21 00:41:31 +02:00
Rene Fichtmueller
f340482752 fix: monitor-erik.sh — correct Erik SSH target + fix awk header skip 2026-04-21 00:34:28 +02:00
Rene Fichtmueller
9e6be570a3 feat: more switch image coverage + system health metrics + Erik monitor
switch-image-fetcher:
- Add Fortinet URL builder (11 FortiSwitch models)
- Add Quanta Cloud Technology, Allied Telesis, Ufispace, Netgear URL builders
- Fix alcatel-lucent-enterprise slug missing from URL_BUILDERS dispatcher
- Fix NVIDIA builder to skip ConnectX/BlueField adapters (not switches)
- Add aruba slug alias for hpe-aruba

health endpoint:
- Add system metrics: CPU load (1/5/15m), memory usage, disk usage
- Add load_status indicator (ok/busy/overloaded)
- Expose process RSS memory
- Used by external monitors

scripts/monitor-erik.sh:
- Cron-ready health check script for Claudi (.82) and Raspberry Pis
- Checks TIP API health endpoint (load, memory, disk, DB latency)
- Checks PM2 process state via SSH (errored/stopped detection)
- ntfy.sh push notifications (set NTFY_TOPIC env var)
- Includes systemd service + timer unit comments for auto-install
2026-04-21 00:31:43 +02:00
Rene Fichtmueller
823b64bd24 perf: load-aware scraper guard + higher rate limits + /tmp crawlee storage 2026-04-20 23:35:02 +02:00
Rene Fichtmueller
a2492d833b feat: Flexoptix order section per switch + reject generic/logo images 2026-04-20 23:31:36 +02:00
Rene Fichtmueller
0a60d821fb fix: expand compatibility.verification_method CHECK to include vendor_compat + spec_match 2026-04-20 23:23:23 +02:00
Rene Fichtmueller
4f277df703 fix(migration-041): use 'manual' doc_type instead of config_guide (CHECK constraint) 2026-04-20 23:17:41 +02:00
Rene Fichtmueller
dfadc8eb4e fix: add certifications column to switches table (migration 039a) 2026-04-20 23:16:52 +02:00
Rene Fichtmueller
5737ae0362 ui: update Finder quick examples to use actual seeded switch models
Replace placeholder models (93180YC-FX3, 7280R3A, QFX5120 — not in DB) with
the real seeded switches: N9K-C9364C, 93600CD-GX, 7060CX2-32S, QFX5130-32CD, SN5600
2026-04-20 23:00:36 +02:00
Rene Fichtmueller
ab059c2fd1 fix(community-issues): scrapeTransceiverCompatIssues falls back to ports_config when no compat entries 2026-04-20 23:00:00 +02:00
Rene Fichtmueller
8cf19e9b78 data: migration 041 — seed switch datasheets (Cisco, Arista, Juniper, NVIDIA)
Seeds known-good official datasheet PDFs and config guide URLs for the 24 initial
seeded switches. Directly visible in switch detail panel → Datasheets & Manuals.
2026-04-20 22:58:18 +02:00
Rene Fichtmueller
21f5250353 feat: switch facts — migration 040 seeds power/weight/certifications + dashboard shows them
- Migration 040: seeds rack_units, typical_power_w, max_power_w, weight_kg, certifications
  for 23 initial switches (Cisco Nexus/Catalyst, Arista, Juniper, NVIDIA, Edgecore/Celestica/Asterfusion)
- Dashboard: Specifications section now shows Typical Power, Weight, Certifications (colored pills)
2026-04-20 22:56:53 +02:00
Rene Fichtmueller
307a5ea38a chore: update changelog for 2026-04-20 switch image + compat features 2026-04-20 22:53:24 +02:00
Rene Fichtmueller
c0cd0dc1ca feat: compatibility panel — verification_method, competitor prices, spec-match collapsible
- API: getCompatibleTransceivers() returns verification_method, orders vendor_compat first
- Dashboard: Flexoptix section splits vendor-tested vs spec-match (collapsed)
- Dashboard: Competitor section shows vendor-tested with prices, spec-match as chips
2026-04-20 22:52:49 +02:00
Rene Fichtmueller
4bf5c95824 feat: Flexoptix compatibility scraper + transceiver issue scanner
- Add flexoptix-compat.ts: maps switch models to compatible Flexoptix transceivers
  via search API (vendor_compat) with form-factor fallback (spec_match)
  Scheduled daily at 09:00 UTC as scrape:compat:flexoptix
- Enhance community-issues.ts: add vendor advisory sources (Cisco Field Notices,
  Juniper KB, SONiC GitHub Issues) + new scrapeTransceiverCompatIssues() that
  searches for switch+transceiver combination problems specifically
- Scheduler: 59 schedules, 78 workers
2026-04-20 22:50:57 +02:00
Rene Fichtmueller
a0a7a97d83 feat: switch image fetcher + og:image scheduler job + dashboard thumbnail column
- Add switch-image-fetcher.ts: og:image-based image discovery for all 86 seeded switches
  (covers Cisco, Arista, Juniper, NVIDIA, Edgecore, Celestica, Asterfusion, Dell,
   HPE/Aruba, Huawei, Nokia, Extreme, MikroTik, Ubiquiti, FS.COM, Supermicro)
- Wire fetchSwitchImages() into scheduler as scrape:images:switches (daily 08:30 UTC)
- Dashboard: add 48px thumbnail column to switch table (lazy img with gear icon fallback)
2026-04-20 22:44:08 +02:00
Rene Fichtmueller
aa91798e8d fix(vcelink): resolve TS 5.9 narrowing quirk with explicit cast in dead code
price?: number narrowing via typeof/!== undefined does not work for
arithmetic comparisons in TypeScript 5.9 dead code paths; use 'as number'
cast to keep the dead code compilable while the early-return guard above
prevents runtime execution entirely.
2026-04-20 22:18:13 +02:00
Rene Fichtmueller
1aba912a15 fix(scrapers): fix ATGBics theme migration, NADDOD URL, disable VCELink
- ATGBics: update HTML parser from old card--product theme to new
  card__info theme (Shopify template changed April 2026); name now
  extracted from href link text instead of aria-label
- NADDOD: correct ensureVendor shop URL from /collections/transceivers
  (404) to /collection/optical-transceivers
- VCELink: disable scraper — site pivoted from optical transceivers to
  audio/video/cable products; all collection URLs return 404
2026-04-20 22:11:24 +02:00
Rene Fichtmueller
ca943f1f86 ui: comprehensive DEMO/MODELL tagging across all dashboard sections with synthetic data
- Stock tab nav: ⚠DEMO badge
- Stock section subtitle: clarify prices=real vs. Lager/Verkauf=DEMO
- Stat cards: DE-Lager, Global-Lager, Nachlieferung labels tagged [DEMO]
- Recently Restocked header: DEMO DATA badge
- Stock detail lookup: [demo] inline on all warehouse/units_sold fields
- Top Sellers: already tagged (previous commit)
- Procurement > Reorder Signals: DEMO DATA banner (based on synthetic ABC data)
- Procurement > ABC Classification: DEMO DATA banner
- Hype Cycle: MODELL badge on header (Norton-Bass = mathematical estimate)
- Hype Cycle table: Adoption/Peak/To Plateau columns tagged [M] = Modell
- Hype Cycle legend: explains [M] vs real data
- Market Intelligence + Lifecycle Events: no tag (real scraped data)
2026-04-20 21:52:10 +02:00
Rene Fichtmueller
9f3cd46f9c ui: mark Top Sellers widget data as DEMO (synthetic seed data, not real sales) 2026-04-20 21:44:33 +02:00
Rene Fichtmueller
0fb4850dfa fix: price-comparison SKU lookup — wrong column refs (so.stock_level, search_url_template) 2026-04-19 00:12:18 +02:00
Rene Fichtmueller
b0ed54f386 feat: register fiber24 + fibermall in index, move atgbics to fetch-only section 2026-04-18 22:50:52 +02:00
Rene Fichtmueller
cb5a587d7e feat: rewrite ATGBICS scraper — static HTML, correct collection handles, GBP cookie
- Replaces Playwright with pure fetch() — static HTML has prices
- Correct collection handles (compatible-transceivers-sfpp-10g etc.)
- Cookie: cart_currency=GBP forces GBP pricing from any geo-IP
- Handles 35+ pages per category × 24 products = 840+ SFP+ products
- No IP-blocking with static HTML (Playwright was the trigger)
- Adds scripts/run-atgbics-mac.sh for Mac-side runner if needed
2026-04-18 22:48:29 +02:00
Rene Fichtmueller
785a6731ab fix: fiber24 stockLevel on_request (was unknown — violated DB constraint) 2026-04-18 22:26:45 +02:00
Rene Fichtmueller
d4ad9f4641 fix: ShopFiber24 sitemap-based scraping + Fibermall image extraction
ShopFiber24 (fiber24.ts):
- Complete rewrite: was using JS-rendered catalog (all prices = 0)
- New strategy: fetch sitemap_0.xml.gz → 310 product DE-URLs
- Each product page has Schema.org microdata: itemprop=price, sku, image
- Extracts: price (minPrice), SKU, image_url, name, specs
- Rate: 1 req/1.5s, no Playwright needed

FiberMall (fibermall.ts):
- Add imageUrl to Product interface
- Extract first fibermall.com/photo/*.jpg from product listing card
- Write image_url to transceivers table (has_image=true) on upsert
- SKU variants share parent product image
- 304 FiberMall transceivers will get images on next scraper run
2026-04-18 22:20:57 +02:00
Rene Fichtmueller
446ac667b0 feat: side-by-side competitor comparison + fix 1.6T speed_gbps
- Fix OSFP-DR8-1.6T-FL and OSFP-2FR4-1.6T-FL: speed_gbps was 200, now 1600
  → FS.com 1.6T products now correctly match as comparables for Flexoptix O.1316T.C.05.M
- API: extend comparable price query to return comp_form_factor, comp_speed_gbps,
  comp_reach_meters, comp_reach_label, comp_fiber_type, comp_wavelengths
- Dashboard: replace plain comparable price row with side-by-side spec comparison card
  showing Flexoptix vs. competitor: Form Factor, Speed, Reach, Fiber, Wavelengths
  with color coding (green=match, orange=mismatch) and savings badge (−45% günstiger)
2026-04-18 21:51:41 +02:00
Rene Fichtmueller
62d97a783c feat: add claude-code LLM provider + update dashboard to fo-blog-v5
- client.ts: add claude-code provider routing BLOG_LLM_PROVIDER=claude-code
  to claude-bridge (flat-rate, no API billing via Claude Code subscription)
- checkHealth() now pings /health on claude-bridge for real availability check
- Default OLLAMA_LLM_MODEL changed from qwen2.5:14b to fo-blog-v5
- Dashboard: add claude-code card (EMPFOHLEN), rename fo-blog-v3 → fo-blog-v5
- loadBlogLLMStatus() handles all 3 providers: claude-code/anthropic/ollama
- Grid expanded from 3 to 4 columns to accommodate new card
- ecosystem.config.js + .env on Erik: OLLAMA_LLM_MODEL=fo-blog-v5 confirmed
2026-04-18 20:45:14 +02:00
Rene Fichtmueller
74b83de6e9 docs: changelog — tunnel DNS fix, image backfill, OSFP coverage 2026-04-18 13:23:57 +02:00
Rene Fichtmueller
1da4abc488 fix: FS.com price extraction — DOM-based prices + shipping-context exclusion
- All 247 FS.com prices were €79 (shipping threshold, not product prices)
- Root cause: 'Gratis Versand ab 79 € (ohne MwSt.)' banner matched first
- Fix 1: DOM price extraction in page.evaluate with bad-parent skip list
- Fix 2: bodyText qualified patterns skip matches near shipping keywords
- Fix 3: waitForSelector for price DOM element before evaluate
- Fix 4: Deleted 247 invalid €79 observations from DB

Also included from previous session:
- db.ts: set has_image=true on image writes (fix 632 desync rows)
- spec-updater.ts: DR/FR/LR/ER/ZR → SMF, SR → MMF fiber type inference
2026-04-18 13:10:35 +02:00
Rene Fichtmueller
f8a1d27e79 fix: add missing auth header to blog generate fetches
Both generateBlog() and generateBlogManual() were calling
POST /api/blog/generate without an Authorization: Bearer header.
The requireAuth middleware correctly returned 401, which appeared
as 'Unauthorized — please log in' toast in the dashboard.

Fix: read loadToken() before each fetch and include the token in
the Authorization header. Also add r.status===401 guard to redirect
to login page when token expires, instead of showing error toast.
2026-04-18 08:03:39 +02:00
Rene Fichtmueller
6a33a17bca chore: changelog — Crawlee queue wipe, ATGBICS fix, Optcore skip 2026-04-18 05:42:37 +02:00
Rene Fichtmueller
48adcd3fc9 fix: skip Optcore on Erik — Cloudflare blocks datacenter IP
optcore.net blocks Erik's IP (82.165.222.127) via Cloudflare WAF.
WP REST API returns HTML block page instead of JSON → 0 product URLs
→ 0 scraped pages every run. Add SKIP_OPTCORE_SCRAPER guard matching
the existing SKIP_FS_SCRAPER pattern. Set in ecosystem.config.js on
Erik. Residential IP (Mac launchd) would be needed to use this scraper.
2026-04-18 05:41:56 +02:00
Rene Fichtmueller
e11e351f5e fix: crawlee-config clear request queue on each run
Crawlee's FileSystemStorage marks request URLs as HANDLED (state=4,
orderNo=null) after processing. With purgeOnStart=false these entries
persist, so on the next run crawler.run(startUrls) deduplicates them
→ requestsTotal=0 → immediate finish with 0 scraped pages.

Fix: rmSync request_queues/default/ before each makeCrawleeConfig()
call. Safe: session pool state lives in key_value_stores/, not in
request_queues/. Affects all Crawlee-based scrapers (ATGBICS, Optcore,
Switch-assets, etc.).
2026-04-18 05:37:45 +02:00
Rene Fichtmueller
1378a9bee8 chore: changelog — 10Gtek scraper fix (sfpcables.com, 49 prices) 2026-04-18 05:32:33 +02:00
Rene Fichtmueller
fcdd258369 fix: 10Gtek scraper now fetches prices from sfpcables.com
10gtek.com main site only exposes technical spec tables with no prices.
sfpcables.com is 10Gtek's own retail store and has both Model numbers
and USD prices in standard Magento product listings.

Changes:
- Switch scraping target from www.10gtek.com to sfpcables.com
- Parse Model: <part> + US.XX per product block (Magento structure)
- XFP fallback: extract part number from title after '|' separator
- Add fetchAllPages() with Magento loop-detection via seen-part dedup
- Remove QSFP-DD category (not available on sfpcables.com)
- Drop XFP-less categories from old 10gtek.com spec-table parser

Verified: 10/10 SFP prices, 10/10 SFP+ prices, 4/4 XFP prices on live site.
2026-04-18 05:27:49 +02:00
Rene Fichtmueller
2a6ec90ecd fix: fs-com Phase 1+2 crawler.run() ENOENT guard — Crawlee catches and re-throws the post-run _isTaskReadyFunction ENOENT internally, which rejected crawler.run() and aborted Phase 2 before it could start. Wrap both crawler.run() calls in try/catch to swallow ENOENT from request_queues paths; all processing is already complete at this point. 2026-04-18 03:52:49 +02:00
Rene Fichtmueller
b3eff15fc4 chore: changelog — daemon stability, ATGBICS Playwright, health monitor accuracy 2026-04-18 03:25:02 +02:00
Rene Fichtmueller
93d825dc04 fix: daemon stability + health monitor accuracy
- Add global unhandledRejection handler in scheduler daemon to swallow
  Crawlee's benign post-run ENOENT lock-file races (prevents process.exit(1))
- Add SKIP_FS_SCRAPER env var: skip FS.com worker on Erik where Cloudflare
  WAF blocks datacenter IPs (Mac launchd handles FS.com from residential IP)
- Remove FS.COM from health monitor EXPECTED_VENDORS (skipped on Erik)
- Health monitor: extend pg-boss lookup from 12h → 26h, add completed-job
  map; if job ran OK in last 26h + vendor has historical prices → mark
  STABLE instead of CRITICAL (fixes ATGBICS/Fluxlight hash-dedup false positives)
- Install Playwright Chromium on Erik (fixes ATGBICS BrowserLaunchError)
- Create missing Crawlee storage dirs on Erik (storage-fs-phase1/2,
  storage-ebay-transceivers) to prevent ENOENT on first Crawlee run
2026-04-18 03:16:59 +02:00
Rene Fichtmueller
8391b194a5 fix: GBICS scraper — fall back to aria-label-first pattern when href-first finds no priced products
Pattern 1 (href→aria-label) finds 127 navigation links on GBICS BigCommerce
pages — none contain GBP prices. Pattern 2 (aria-label→href) correctly
finds 16-30 product links per category page with £XX.XX prices in aria-labels.
The fallback from P1 to P2 now triggers when P1 finds results but none
contain '£', rather than only when P1 finds 0 total results.
2026-04-18 03:02:39 +02:00
Rene Fichtmueller
0e4e6ff6b2 chore: changelog — FS.com ENOENT fix, PID lock, health monitor tiered alerts 2026-04-18 02:55:18 +02:00
Rene Fichtmueller
24ff9822ac fix: improve scraper health monitor — tiered alerts, suppress stable-price false positives
Previous logic fired an alert whenever prices_6h=0, even when prices
were genuinely stable (content hash dedup prevents duplicate inserts).
This caused Flexoptix, ATGBICS and others to trigger alerts every 3h
despite their scrapers running successfully.

New logic:
  🔴 CRITICAL: last price > 7 days (genuine failure)
  🟡 WARNING:  last price 48h–7 days (possibly stale)
   STABLE:   last price ≤48h, 0 new (prices unchanged, scraper OK)

Also shows pg-boss job state/time alongside each vendor for faster
root-cause diagnosis. Trimmed EXPECTED_VENDORS to vendors with actual
scraper implementations (removed never-scraped placeholders).
2026-04-18 02:54:28 +02:00
Rene Fichtmueller
e552e08015 fix: suppress Crawlee post-run ENOENT unhandledRejection in fs-com.ts
After PlaywrightCrawler.run() resolves, Crawlee's internal task loop
schedules one final _isTaskReadyFunction call that tries to read a
request queue .json file already cleaned up during processing. This
ENOENT fires as an unhandledRejection and calls process.exit(1),
aborting Phase 2 before prices are written to the database.

Added a targeted unhandledRejection handler in the require.main block
that swallows ENOENT errors from request_queues paths (benign Crawlee
cleanup race) while re-raising all other rejections.
2026-04-18 02:51:00 +02:00
Rene Fichtmueller
6be2c131d3 fix: add PID lock to run-fs-scraper-mac.sh — prevent simultaneous instances
Adds /tmp/tip-fs-scraper.lock PID file to prevent launchd from running
a second instance while the previous one is still active (e.g. 2am
schedule fires, runs past 10am when launchd fires again). Without this,
concurrent instances caused rmSync(storage-fs-phase1) in one instance
to delete the Crawlee storage dir while the other was still using it,
resulting in ENOENT crashes.
2026-04-18 02:43:28 +02:00
Rene Fichtmueller
681fd5ced6 chore: gitignore all storage-* Crawlee dirs + local credentials 2026-04-18 02:40:34 +02:00
Rene Fichtmueller
c5e7e7d7f6 fix: remove POSTGRES_PASSWORD export from run-fs-scraper-mac.sh — sourced from ~/.tip/.env only 2026-04-18 02:37:42 +02:00
Rene Fichtmueller
fc3224be76 fix: remove hardcoded POSTGRES_PASSWORD from run-fs-scraper-mac.sh — use ~/.tip/.env 2026-04-18 02:37:05 +02:00
Rene Fichtmueller
696127366c chore: changelog — Playwright headless shell fix, withIsolatedStorage race fix, FS.com launchd fix 2026-04-18 02:35:55 +02:00
Rene Fichtmueller
419af4a24e fix: remove all withIsolatedStorage wrappers, add makeCrawleeConfig to remaining Crawlee scrapers
- scheduler.ts: remove withIsolatedStorage from ALL scrapers (atgbics,
  optcore, ufispace, edgecore, ebay-*, market-intel, community-issues,
  cisco, juniper, sonic, 10gtek, prolabs, switch-assets, fs)
  eliminates global CRAWLEE_STORAGE_DIR race condition entirely
- fs-com.ts: replace purgeDefaultStorages() with rmSync on isolated
  storage dirs (fs-phase1, fs-phase2); pass makeCrawleeConfig to both
  PlaywrightCrawler instances
- switch-assets-crawler.ts: add makeCrawleeConfig('switch-assets')
- switch-assets-playwright.ts: add makeCrawleeConfig('switch-assets-playwright')
- naddod.ts: restore clean error logging (remove debug instrumentation)
2026-04-18 02:19:53 +02:00
Rene Fichtmueller
d9e5331161 debug: widen NADDOD error slice to 300 chars, add pre-insert logging 2026-04-18 02:00:03 +02:00
Rene Fichtmueller
24481b09e6 fix: eBay enricher Crawlee isolation + ephemeral queues
- Add makeCrawleeConfig isolation to CheerioCrawler instances
- Switch from named persistent RequestQueue to ephemeral null queues:
  named queues retain 'handled' state and skip all URLs on re-runs,
  causing 0 observations on every run after the first.
- Applies to both enrichSwitchFromEbay and enrichTransceiversFromEbay.
2026-04-18 01:42:08 +02:00
Rene Fichtmueller
35a02057f4 chore: sync-to-erik now includes scraper rebuild + daemon restart; changelog 2026-04-18 01:37:12 +02:00
Rene Fichtmueller
c7d7456de9 fix: instance-level Crawlee storage isolation + eBay vendor type
- Add utils/crawlee-config.ts: makeCrawleeConfig(name) returns a
  Crawlee Configuration with isolated localDataDirectory per scraper.
  Uses storageClientOptions (not global CRAWLEE_STORAGE_DIR) so
  concurrent pg-boss workers in the same process don't race on
  the shared env var.

- Apply makeCrawleeConfig to all 6 Crawlee-based scrapers:
  optcore (PlaywrightCrawler), atgbics (PlaywrightCrawler),
  community-issues (CheerioCrawler + RequestQueue),
  edgecore (CheerioCrawler), ufispace (CheerioCrawler),
  market-intelligence (CheerioCrawler).

- scheduler.ts: add withIsolatedStorage for optcore and market-intel
  workers (was missing, caused storage-fs path bleed from fs scraper).

- ebay-enricher.ts: fix vendor type 'marketplace' -> 'reseller' to
  satisfy vendors_type_check constraint
  ['manufacturer','distributor','oem','reseller','compatible'].
2026-04-18 01:35:57 +02:00
Rene Fichtmueller
4b751a771b fix: NADDOD stockLevel 'unknown' → 'on_request' — invalid value for price_observations check constraint 2026-04-18 01:21:31 +02:00
Rene Fichtmueller
2b770aa1a9 chore: cleanup — rename digikey→mouser, remove orphan files, gitignore Crawlee artifacts
- Rename scrapers/digikey.ts → scrapers/mouser.ts: export scrapeMouser()
  (file was Mouser API implementation mislabeled from task origin)
- Fix scheduler.ts mouser-oem worker: import scrapeMouser from ./scrapers/mouser
- Delete switch-seed-smb.ts (unreferenced, no CLI flag, no scheduler job)
- Add storage/, storage-fs/, .crawlee/ to .gitignore (Crawlee runtime artifacts)
2026-04-18 01:09:10 +02:00
Rene Fichtmueller
1c8dec52c9 feat: Price Comparison dashboard + Eoptolink OEM scraper
- Add public /api/price-comparison API (summary, top-50, per-SKU detail)
  — no auth required, 3 Express routes, DISTINCT ON latest-price logic
- Add '💲 Price Comparison' dashboard tab: stat cards, form-factor
  breakdown, top-50 SKU table (clickable rows → SKU detail), per-vendor
  price + stock + spread% lookup panel
- Add Eoptolink OEM catalog scraper (93 product-solution pages,
  part-number regex EOLO-*/EOLQ-* etc., no prices, seeds transceivers
  table as manufacturer entries)
- Register scrape:catalog:eoptolink in scheduler: schedule every 4h
  (40 */4 * * *), lazy-import worker, added to known-jobs array
2026-04-18 01:02:08 +02:00
Rene Fichtmueller
63b4a65c28 chore: changelog entry — MCP Server v0.2.0 (finder + switch-docs + Ollama LLM tools) 2026-04-18 00:29:42 +02:00
Rene Fichtmueller
e9fcda2811 feat: wire finder.ts + switch-docs + Ollama LLM tools to MCP server
MCP Server (packages/mcp-server/src/index.ts):
- Register registerSwitchDocTools (switch-docs.ts) — switch documentation lookup
- Register finderTools dynamically (finder.ts) — find_flexoptix_for_switch, get_competitor_alerts
- Add analyze_market_with_llm tool: qwen2.5:14b via Ollama, enriched with live hype cycle + pricing + news
- Add generate_blog_post tool: fo-blog-v5 (fine-tuned) with qwen2.5:14b fallback, enriched with live pricing data
- OLLAMA_BASE_URL env var (default: https://ollama.fichtmueller.org)

Also includes scraper improvements (ascentoptics, atgbics, gbics, skylane, ebay-enricher),
API route updates (blog, blog-sll, health, hot-topics, transceivers, queries),
and dashboard hot-topics refresh.
2026-04-18 00:21:58 +02:00
Rene Fichtmueller
b88a6e28cf feat: /api/hype-cycle/analysis endpoint — DB-backed Bass-fitted results from hype_cycle_analysis table 2026-04-18 00:11:08 +02:00
Rene Fichtmueller
9d3019d0c0 feat: Norton-Bass Hype Cycle Engine — market_metrics seed + Bass fitting + Gartner phase detection 2026-04-18 00:09:08 +02:00
Rene Fichtmueller
75cea9fe90 feat: Mouser Electronics API scraper for OEM reference prices (Juniper/Cisco/Arista PIDs) 2026-04-18 00:04:35 +02:00
Rene Fichtmueller
5c5841d4d1 chore: prune 242 irrelevant vendors from DB (348→106), add changelog entry 2026-04-17 23:58:13 +02:00
Rene Fichtmueller
60736bd5df chore: changelog entries 2026-04-17 stock dashboard + Cisco TMG + Juniper HCT 2026-04-17 23:33:49 +02:00
Rene Fichtmueller
861243ea3f feat: stock confidence badges, multi-vendor price comparison, expanded Cisco TMG + Juniper HCT
Stock API & Dashboard:
- /api/stock/summary: vendor_breakdown adds avg_confidence, currencies, conf_per_warehouse/aggregated/boolean
- /api/stock/summary: new price_comparison endpoint (multi-vendor SKUs, min/max/avg price)
- /api/stock/summary: totals adds multi_vendor_skus count
- Dashboard: 6th stat card (Multi-Vendor SKUs), confidence badge column (🟢 L3 / 🟡 L2 /  L1)
- Dashboard: price comparison table with vendor-by-vendor price breakdown
- Dashboard: subtitle updated to include QSFPTEK + NADDOD
- Dashboard: top sellers link to product URLs

Cisco TMG improvements:
- Added 5 new platform families: 8000 Series, NCS5500, NCS540, NCS560, NCS1000
- Per-device query strategy: iterates all switch model IDs from family filter
  instead of getting only 1 switch per family → 58 switches per N9300 run
- Graceful error handling per device with rate limiting (1s between requests)

Juniper HCT: ran manually → 475 Juniper-brand transceivers seeded
2026-04-17 23:33:31 +02:00
Rene Fichtmueller
5393f73c17 feat: stock quality schema + QSFPTEK/NADDOD v2 scrapers with real-time stock counts
- Migration 028 (retroactive): document warehouse columns added to stock_observations
- Migration 037: composite indexes for DISTINCT ON (transceiver_id, source_vendor_id) queries
- Migration 038: add stock_confidence (1/2/3), price_currency, price_includes_tax,
  stock_vendor_ts to stock_observations + TRUNCATE test-run data

db.ts: upsertStockObservation now accepts stockConfidence, priceCurrency,
priceIncludesTax, stockVendorTs; delta detection includes quantity_available

fs-com.ts: passes stockConfidence=3 + priceCurrency=EUR + priceIncludesTax=false

qsfptek.ts v2: Phase 1 API listing + Phase 2 detail-page stock extraction
- Parses 'X in real-time stock, DATE' from product detail pages
- Writes stock_observations with confidence=2 + stockVendorTs
- Up to 500 detail pages/run at 2s rate limit

naddod.ts v2: complete rewrite from WooCommerce to Astro sitemap-based
- Discovers products via /sitemaps/products.xml (600+ products)
- URL format: /products/XXXXX.html
- Extracts 'In Stock: X' exact counts from SSR HTML
- Writes both price + stock observations (confidence 1 or 2)
2026-04-17 22:54:40 +02:00
Rene Fichtmueller
5b35b2b8be feat(scraper+api): warehouse stock data pipeline — FS.com v2, SmartOptics v2, Stock API
Scraper changes:
- fs-com.ts v2: Playwright stealth patches + www.fs.com/de/ URL fix (de.fs.com DNS NXDOMAIN).
  Extracts DE-Lager, Global-Lager, Nachlieferung, units_sold, compatible_brands, price_net.
  Mac-side runner (run-fs-scraper-mac.sh) via SSH tunnel for residential IP access.
  Fast-fail connectivity check on datacenter IPs that are blocked by Cloudflare.
- smartoptics.ts v2: WooCommerce REST API fallback + 8 catalog categories + relative URL fix.
  Was finding only 8 products, now discovers 18+ with multi-category crawl.

DB layer:
- db.ts: add upsertStockObservation() — writes 10 new stock_observations columns
  (warehouse_de_qty, warehouse_global_qty, backorder_qty, units_sold, compatible_brands,
  price_net, product_url, delivery dates) with dedup check.

API:
- routes/stock.ts: GET /api/stock, /api/stock/summary, /api/stock/:id
  Warehouse breakdowns per transceiver/vendor with top-sellers and vendor summary.
- routes/review.ts: equivalence review queue (approve/reject/bulk-approve).
- index.ts: register /api/stock and /api/review routes.

Dashboard:
- index.html: 🏭 Stock tab with stat cards (DE-Lager, Global-Lager, Nachlieferung totals),
  top-sellers table, vendor breakdown, recently-restocked events, part-number lookup.

SQL migrations:
- 034: blog-review-tag, 035: price-observations is_anomalous, 036: transceiver-equivalences.
2026-04-17 10:45:59 +02:00
Rene Fichtmueller
662cd1f90b fix(scraper): FiberMall URL schema + price parser + Flexoptix EUR comma bug
FiberMall:
- Correct /store-XXXXX-name.htm category URLs (was /c/xxx/ → HTTP 404)
- Parser: split on new_proList_mainListLi, price from data-price on
  currency_price span — fix 0.00 false-match from SKU variant items
- Also scrape SKU brand variant links from .sku_item divs
- Result: 3,410 prices now in DB (was 0)

Flexoptix:
- Fix extractPrice regex for EUR thousand-separator format
  (2,921.60 EUR was parsed as 2 EUR)
- Add OSFP224 / 1.6T search queries (4 new, form factor was missing)
- Fix O.138HG2.C.05 stale price 3009.60→2921.60 EUR

Schema: competitor_verified + competitor_verified_at columns
added via ALTER TABLE (were referenced in code but missing in DB)

CHANGELOG: added 6 entries for 2026-04-12
2026-04-12 04:26:35 +02:00
Rene Fichtmueller
cdb8ef6e61 feat(scraper): add FiberMall/Vcelink/OpticsBay scrapers, fix QSFPTEK API migration
- New scrapers: fibermall.ts (WooCommerce), vcelink.ts (Shopify), opticsbay.ts (WooCommerce)
- QSFPTEK rewritten to use /mall/commodity/list API (old OpenCart /c/*.html paths gone 404)
  - New: attribute-based filtering by data rate (1G/10G/25G/40G/100G/200G/400G/800G)
  - Scrapes HTML fragments, extracts US$ prices and product URLs
- scheduler.ts: +3 queues/schedules/workers (fibermall, vcelink, opticsbay) → 61 total workers
- index-pi.ts: Pi fleet picks up all 3 new scrapers
2026-04-11 19:13:36 +02:00
Rene Fichtmueller
8905382a49 chore: add changelog entries 2026-04-05 through 2026-04-09 2026-04-11 18:50:17 +02:00
Rene Fichtmueller
ef30da18d1 chore: update changelog for 2026-04-11 session 2026-04-11 13:57:31 +02:00
Rene Fichtmueller
148d2e1000 fix(scraper): set CRAWLEE_PURGE_ON_START=1 in withIsolatedStorage
Crawlee's SessionPool throws 'Could not find SDK_SESSION_POOL_STATE.json'
when initializing against a freshly-created isolated storage dir.
Setting CRAWLEE_PURGE_ON_START=1 tells Crawlee to start fresh instead
of trying to load non-existent session state — fixes FS.com and ATGBICS
crashes at the start of every 2h cycle after the dirs were cleaned up.
2026-04-11 07:27:24 +02:00
Rene Fichtmueller
45c48755e4 feat(scraper): add NADDOD/QSFPTEK/AddOn to scheduler, fix pre-existing TS build errors
- Register scrape:pricing:naddod (48 */2), qsfptek (52 */2), addon (55 */2) in pg-boss
- Add boss.work() handlers for all three (fetch-based, run on Erik)
- Fix findOrCreateScrapedTransceiver callers: remove invalid `name`/`url` params,
  fix `t.id` → `t` (function already returns string ID)
- Fix ebay-enricher: remove invalid `extractType` option, use extraction.standard_name
  instead of non-existent `.description`, fix cheerio type incompatibility
- Fix community-issues: description → summary, publishedDate → published_at
- Startup zombie cleanup already deployed (index.ts) — no changes needed
- ProLabs rewritten to fetch-based catalog scraper (no Playwright, bypasses WAF)
2026-04-11 03:17:33 +02:00
Rene Fichtmueller
6febb9c88e refactor(prolabs): replace Playwright+Firefox with fetch-based catalog scraper
ProLabs uses B2B quote model - prices require reseller account and are
not shown publicly (schema.org always shows price=0.00). Fighting
CloudFront WAF with Firefox automation is pointless.

New approach:
- Sitemap-driven: downloads all 14 sitemaps to collect product URLs
- fetch-based: curl-compatible HTTP requests bypass CloudFront TLS detection
- catalog-only: writes part numbers + specs to transceivers table
- Rate-limited: 300ms between requests (~3 req/sec)
- No proxy needed: Pi nodes no longer consumed for ProLabs
2026-04-11 02:57:13 +02:00
Rene Fichtmueller
7af5b32b3f ui: redesign LLM panel for light theme readability
Replace hard-coded purple/green colors with theme CSS variables.
Dark code blocks (#1e1e1e bg), orange accent for active borders/badges,
dark green for status text, amber for warnings — all readable on white.
2026-04-09 21:20:43 +02:00
Rene Fichtmueller
bf626f9de6 fix: route Pi-destined scrapers exclusively to Pi worker fleet
Remove boss.work() registrations for lightweight fetch/cheerio scrapers
from Erik's scheduler. Pis are now the SOLE consumers of these queues:
fluxlight, gbics, optcore, champion-one, sfpcables, blueoptics, fiber24,
tscom, skylane, ascentoptics, gaotek, smartoptics, hubersuhner, news,
market-intel.
2026-04-09 20:50:57 +02:00
Rene Fichtmueller
c898f52bbe feat: add LLM model selector panel to Blog Engine tab
Shows active model (fo-blog-v3-qwen7b / claude-sonnet-4-6 / qwen2.5:14b),
live status from /api/blog/llm/status, ratings, config instructions,
and highlights which model is currently active.
2026-04-09 20:42:03 +02:00
Rene Fichtmueller
3d00a4a00a feat: 800G standards deep enrichment + Pi Starlink proxy-agent support
- Migration 033: comprehensive technical update for all 4x 800G standards
  - 800GBASE-SR8: full optical specs (Tx OMA 2.3 dBm, Rx sens. -4.6 dBm, KP4 FEC,
    MPO-16 APC, CMIS 5.2, ≤16W, 60m OM3/100m OM4, VCSEL 850nm, 53.125 GBd PAM4)
  - 800GBASE-DR8: 500m SMF, EML 1310nm, 8x parallel fiber, MPO-12, -9dBm sensitivity
  - 800GBASE-LR4: 2km CWDM4 WDM (1270/1290/1310/1330nm), 4x 106.25 GBd PAM4, LC duplex
  - 800G-ZR (OIF-800ZR-01.0): DP-16QAM 96 GBd, 1000km EDFA, SD-FEC, 20-24W, DCO license
- Pi scraper: add optional SOCKS5 proxy via dante-server on WireGuard IP
  - Enables Starlink bandwidth contribution (PROXY_AGENT=1 flag)
  - Scraper routes selected jobs through Pi SOCKS5 for different IP range
2026-04-09 20:34:27 +02:00
Rene Fichtmueller
cddc92c9d2 feat: TIP audit fixes — Qdrant init, switches columns, verification fix, crawler live status, demo data badges
- Migration 032: add system_type, is_linecard, chassis_model, slot_type, flexbox_* to switches table
- Migration 032: fix compute_transceiver_verification() to count seed data as details_verified (100% now)
- Migration 032: add is_demo_data flag to reorder_signals, abc_classification, market_intelligence, stock_snapshots
- Cisco 8000: insert 8812, 8818, 8800-LC-36FH, 8800-LC-48H with correct vendor slug 'cisco'
- API: add /api/scrapers/jobs endpoint exposing pg-boss job queue (active/recent/queues)
- Dashboard: live job queue panel in Crawler Intelligence tab (active jobs + recent 4h completions)
- Dashboard: DEMO DATA badge now uses is_demo_data column (was checking wrong field is_demo)
- Blog engine: configured fo-blog-v3-qwen7b fine-tuned model via tip-api ecosystem.config.js
- Qdrant: all 6 collections created, seeded (2135 products, 29 FAQs, 39 news, 20 troubleshooting)
2026-04-09 20:29:46 +02:00
Rene Fichtmueller
7d005ba1f3 data: add Cisco 8000 accuracy migration (031)
Documents all Cisco 8000 research findings as idempotent SQL migration:
- 8201-32FH: Silicon One Q200 (not Q100), full description, features, use_cases
- 8608: modular chassis type, corrected description
- NEW: 8812 (12-slot), 8818 (18-slot) modular chassis
- NEW: 8800-LC-36FH (36×400G QSFP-DD), 8800-LC-48H (48×100G QSFP28) line cards

flexbox_notes covers:
- IOS XR EEPROM/PID check behavior (TX laser disabled on unknown PID)
- transceiver permit pid all (per-interface, NOT global)
- DCO license required per 100G for ZR/ZR+ coherent optics (all vendors)
- No RTU license for grey non-coherent optics
- TAC refusal + PFM alarm on override
- IOS XR upgrade risk for 3rd-party optics
- DOM monitoring may show incorrect values
- Flexoptix FlexBox Cisco Systems mode = native PID, no override needed
2026-04-09 09:09:23 +02:00
Rene Fichtmueller
cf75eee8ad feat: linecard system support, Cisco 8000 accuracy, price anomaly detection
API/finder:
- Add modular chassis support: sibling linecards fetched when is_linecard=true
- Add chassis linecards when system_type=modular
- Extend switch response: system_type, is_linecard, chassis_model, slot_type,
  flexbox_compat_mode, flexbox_notes, description, switching_capacity_tbps,
  total_ports, category, lifecycle_status, features, use_cases, linecards[]

API/transceivers:
- Filter price_observations with COALESCE(is_anomalous, false) = false
  (direct prices + comparable market prices)

Scraper/db:
- Add PRICE_BOUNDS map (per form-factor min/max USD sanity bounds)
- Add isPriceAnomalous() — marks DB price_observations as is_anomalous=true
- Add competitor_verified flag: set true when valid competitor price stored
- upsertPriceObservation: skip prices outside sanity bounds, set competitor_verified

Scraper/hash:
- contentHash() now accepts Record<string,unknown> | string (union type)
  to support both structured objects and legacy string callers

Scrapers (skylane, tscom, wiitek):
- Fix contentHash() call signature: pass objects not JSON.stringify strings
- Fix wiitek: remove invalid 'name' param, fix t.id → transceiverId

Migrations:
- Add is_anomalous, competitor_verified, competitor_verified_at,
  image_primary columns
- Recreate sync_fully_verified trigger to include competitor_verified
- Add is_linecard, chassis_model, system_type, slot_type,
  flexbox_compat_mode, flexbox_notes to switches table
2026-04-09 09:06:22 +02:00
Rene Fichtmueller
240e7f46f2 feat(scraper): add SOCKS5 proxy rotation for fs-com, atgbics, gbics scrapers
Routes requests through CT130/131/132 proxy pool (192.168.178.77/76/74:1080)
when PROXY_URLS env var is set. Uses ProxyConfiguration from crawlee for
PlaywrightCrawler scrapers and socks-proxy-agent for fetch-based scrapers.
2026-04-08 08:17:49 +02:00
Rene Fichtmueller
772ce2074d feat: add blog training articles 056-100 for fo-blog-v3 fine-tuning
45 expert articles covering: Cisco/Juniper/Arista optic compatibility mechanics,
100G/400G/800G optics selection, DWDM/ROADM/WSS architecture, fiber standards,
coherent pluggables, AI cluster optics, carrier timing, EEPROM programming,
market pricing 2026, hyperscale procurement, transceiver failure analysis, and more.
2026-04-07 08:59:16 +02:00
Rene Fichtmueller
12a19639fd fix: seed script accepts category as fallback for missing type field 2026-04-07 01:24:15 +02:00
Rene Fichtmueller
0572ab5a71 feat: add blog training articles 041-055 for fo-blog-v2 fine-tuning
15 expert articles covering: CPO/silicon photonics 2026, 800G OSFP vs QSFP-DD,
400ZR/OpenZR+/ZR+ comparison, laser safety, OSNR/link budget, counterfeit detection,
DOM deep dive, 400G DR4/FR4/LR4, WDM primer, temp grades, spine-leaf strategy,
proactive replacement, OEM lock-in, OM3/4/5, lifecycle management.
2026-04-07 01:08:27 +02:00
Rene Fichtmueller
99fca6b531 feat(training): add blog-031 through blog-040 — 10 expert articles
Topics: CWDM4/PSM4, MSA compliance, DAC/AOC TCO, grey vs DWDM,
ESD damage, tunable DWDM, FEC deep-dive, CPO hype cycle,
CMIS 4.0, vendor evaluation. Ø 1,180 words each.
2026-04-06 18:15:46 +02:00
Rene Fichtmueller
51af249361 Merge remote-tracking branch 'github/main'
# Conflicts:
#	packages/api/src/llm/fo-blog-pipeline.ts
#	packages/api/src/routes/blog.ts
#	packages/scraper/src/scheduler.ts
#	packages/scraper/src/scrapers/fs-com.ts
#	packages/scraper/src/scrapers/gbics.ts
2026-04-06 18:03:36 +02:00
Rene Fichtmueller
285a91b945 feat(training): add blog-016 through blog-030 — 15 expert training articles
Adds 15 Sonnet-quality blog articles for fo-blog-v1 fine-tuning:
tutorials, comparisons, tech deep-dives covering 400G/800G topics.
Also adds seed-blog-training-data.py script for learning_corpus import.
2026-04-06 17:59:14 +02:00
Rene Fichtmueller
bf06993b63 ui: show creation time (HH:MM) alongside date in blog list 2026-04-06 17:07:05 +02:00
Rene Fichtmueller
3928755c60 fix: correct verified badge, comparable pricing, and clickable product images
- Reset details_verified=false for 298 products where reach_label is empty (DB migration)
- Runtime check in dashboard: dVer requires non-empty reach_label regardless of DB flag
- comparable price query: treat reach_meters=0 same as NULL so 800G OSFP products
  find FS.com equivalent prices (was blocked by reach_meters=0 != NULL shortcircuit)
- Product image area now fully clickable with vendor link overlay when product_page_url exists
- Clear wrong image for O.Czz8HG.z.R (was showing unrelated OSFP product image)
2026-04-06 10:24:39 +02:00
Rene Fichtmueller
8f060d0159 feat(training): add blog-014 new_product and blog-015 competitor_analysis
Completes training data coverage for all 8 blog types:
market_alert(2), comparison(1), technology_deep_dive(4), tutorial(3),
hype_cycle(1), buying_guide(1), migration_guide(1), new_product(1),
competitor_analysis(1) — 15 gold-standard articles total
2026-04-06 04:16:00 +02:00
Rene Fichtmueller
4acf293690 fix(llm): checkHealth uses key presence check, not live API call
Live Anthropic API call during health check causes 429 when the pipeline
is actively running, blocking all subsequent regenerate requests.
2026-04-06 04:07:21 +02:00
Rene Fichtmueller
f7bdee9583 feat: add 2 more gold-standard blog training articles (13 total)
- blog-012: technology_deep_dive — coherent vs direct-detect decision framework
- blog-013: market_alert — transceiver price cycle, when to buy

Training set now covers: market_alert(2), comparison(1), technology_deep_dive(4),
tutorial(3), hype_cycle(1), buying_guide(1), migration_guide(1) — 13 total
2026-04-06 03:09:55 +02:00
Rene Fichtmueller
de05bbbec8 docs: update training data README to reflect 11 articles 2026-04-06 02:55:34 +02:00
Rene Fichtmueller
b8e6a62c7b feat: add 4 more gold-standard blog training articles for BlogLLM
Adding diverse topic coverage:
- blog-008: buying_guide — OEM vs compatible real cost numbers
- blog-009: migration_guide — 100G→400G what actually breaks
- blog-010: technology_deep_dive — QSFP-DD vs OSFP form factor reality
- blog-011: tutorial — transceiver procurement checklist

All follow FO rules: no markdown headers in body, no bullet lists,
one thesis, engineer voice, ~1000 words. Total training set: 11 articles.
2026-04-06 02:55:10 +02:00
Rene Fichtmueller
72033ff5c5 fix(blog): fix claudeQueue deadlock from recursive 429 retry
The generateClaude() function was recursively calling itself inside
enqueueClaude(), creating a circular Promise dependency that permanently
deadlocked the claudeQueue. Any 429 rate-limit response would poison
the queue, blocking all future Claude API calls until server restart.

Fixes:
- Split retries into claudeApiCall() which is called from enqueueClaude
  (not re-entering the queue on retry = no circular dependency)
- Max 3 retries with increasing backoff (10s/30s/60s)
- Add resetClaudeQueue() exported function
- Add 15-minute auto-reset stall detection to enqueueClaude
- Expose resetClaudeQueue in POST /api/blog/llm/reset-queue endpoint
- Fix merge conflict markers in index.ts (duplicate scraperRouter import)
2026-04-06 02:51:28 +02:00
Rene Fichtmueller
55de4920b2 feat(sql): migrations 026+027 for price cleanup and FS.COM EUR fix
026: Remove invalid price observations (sub-manufacturing-cost), disable
     optictransceiver.com (domain repurposed as plant shop), fix verification
     function to accept low/medium/high data_confidence values
027: Clean up FS.COM USD→EUR converted prices, force re-scrape with
     new de.fs.com EUR-primary scraper
2026-04-06 02:22:00 +02:00
Rene Fichtmueller
2e852e0a2f fix(scrapers): replace bot User-Agents with Chrome UA + disable dead domain
- 16 commercial scrapers: replace TIP-Bot/1.0 with Chrome/120 UA
  (GBICS confirmed returning 0 bytes for bot UA, Chrome UA returns 200KB)
- gbics.ts: fix User-Agent (was returning empty HTML, now returns products)
- optictransceiver.ts: disable — domain repurposed as plant shop (2026-04-06)
  Alocasia Regal Shield is not a transceiver.
2026-04-06 02:17:50 +02:00
Rene Fichtmueller
80aa85961b feat: add 7 gold-standard blog training articles for BlogLLM
Reference quality articles covering: 400G DR4 pricing, vendor lock-in,
silicon photonics, fiber plant readiness, 400ZR reality check,
DOM diagnostics, 800G readiness. All follow strict FO Blog Pipeline
rules — no markdown headers, no spec dumps, one thesis per article.
2026-04-06 01:58:05 +02:00
Rene Fichtmueller
dfe86fb347 fix(scraper): switch fs-com to de.fs.com for EUR prices as primary source
EUR prices scraped verbatim from de.fs.com — no conversion needed.
USD derivation (EUR→USD) happens downstream, not EUR←USD.
Fixes price discrepancy: TIP showed USD 999×0.92=EUR 866 vs real €948 on de.fs.com.
2026-04-06 01:24:47 +02:00
Rene Fichtmueller
e4e432d9fa fix: parsePrice requires currency symbol + uses largest number to avoid misreads
Root cause of fake prices (e.g. 1.30 for 800G OSFP):
- parsePrice accepted any bare number without currency symbol
- Could misread stock counts, page numbers, or CSS values as prices
- Also picked the first number, not the main price

Fix:
- Require explicit currency symbol or decimal format (1234.56)
- Use the LARGEST number found in the price string
- Returns price=0 (rejected) when no valid price pattern found
2026-04-06 01:19:25 +02:00
Rene Fichtmueller
a35817c96d fix: preserve user-provided title in blog generation + price floor validation
- blog/generate now uses caller title when provided; falls back to template
- Migration 027: hard price floor by speed class in verification function
  (no medians, no estimates — only real prices above minimum thresholds)
- Deleted 474 obviously wrong price observations (shipping costs scraped as prices)
2026-04-06 01:14:37 +02:00
Rene Fichtmueller
df8d1e797c fix: show price_verified_eur as fallback price + strict badge logic
- Price column now shows price_verified_eur (in EUR, dimmed) when street_price_usd is null
  Fixes: FS.COM products showing dash while being marked fully verified
- Badge logic now requires visible price AND image_verified AND details_verified
  No more badge when price displays as dash — all requirements must be visually present
2026-04-06 01:04:44 +02:00
Rene Fichtmueller
b6928265bf fix: serialize Claude API calls via queue to prevent 429 rate-limit spam
Tier-1 Anthropic API has 40K TPM — with ~20K tokens per pipeline step,
concurrent calls immediately hit the limit. enqueueClaude() serializes
all generateClaude() calls so only one runs at a time, eliminating
the flood of 429-retry-429-retry loops.
2026-04-06 00:57:03 +02:00
Rene Fichtmueller
cf04549b1b feat: add Anthropic Claude provider to blog LLM client
- Auto-routes to Claude API when BLOG_LLM_PROVIDER=anthropic + ANTHROPIC_API_KEY set
- Fallback to Ollama queue when key not present
- Add rate-limit retry (429 → 10s backoff) for Claude API
- Add STEP_TECHNICAL_SANITY, STEP_SELF_HEAL, STEP_TITLE_CONTRACT_CHECK prompts
- Fix STEP_LINKEDIN_POST angle-specific hooks, remove Gold Reference repetition
2026-04-06 00:21:48 +02:00
Rene Fichtmueller
c43e1f881a fix(blog): hard story blacklist in STEP4 + LinkedIn — ban 2AM/dirty connector/lab-vs-prod stories
10 specific story patterns banned directly in draft prompt.
LinkedIn banned hooks: 'Everything looks fine', 'CRC creeping', 'Same optics same setup'.
Title Contract now injected into STEP4 as binding constraint.
2026-04-05 23:55:56 +02:00
Rene Fichtmueller
80435f8e07 feat(blog): Post to Ghost + LinkedIn buttons in dashboard
- 'Post on blog.fichtmueller.org' → publishes via Ghost Admin API
- 'Post on LinkedIn' → modal with text + copy + open LinkedIn
- Ghost integration: TIP Blog Engine (JWT auth, mobiledoc format)
2026-04-05 23:33:58 +02:00
Rene Fichtmueller
3913372f10 feat(blog): Title Contract + Technical Sanity Check + Self-Heal + angle-aware LinkedIn generator
Pipeline now has 21 steps:
- STEP0: Title Contract binds LLM to headline promise
- STEP19: Technical Sanity Check (optical engineering accuracy)
- STEP20: Self-Heal (auto-fix technical errors preserving tone)
- STEP21: Title Contract Verification (final gate check)
- LinkedIn generator is now angle-aware (no more default Physical Layer hook)
2026-04-05 23:11:16 +02:00
Rene Fichtmueller
8e0eda6c41 fix(blog): anti-repetition engine — 6 angle types, forbidden structures, existing article context injection 2026-04-05 22:47:15 +02:00
Rene Fichtmueller
438225cf7c fix(blog): raise word target to 1200-1600, fix power-budget false positive in validateArticle 2026-04-05 20:49:22 +02:00
Rene Fichtmueller
1434037d29 fix(scraper): use false instead of null for image_verified on insert 2026-04-05 12:15:10 +02:00
Rene Fichtmueller
308cf8e774 feat(dashboard): add data verification status section to overview tab 2026-04-05 12:11:23 +02:00
Rene Fichtmueller
7c7d9f7b51 fix: make /api/hot-topics public — dashboard fetch has no auth token 2026-04-05 12:07:44 +02:00
Rene Fichtmueller
e6d042f827 fix: resolve merge conflict in index.ts + add untracked blog-sll, news, sql migration 2026-04-05 11:51:07 +02:00
root
5d8768b43b fix: mount blogSllRouter + scraperRouter — SLL and Crawler Intelligence routes were missing 2026-04-05 09:50:30 +00:00
Rene Fichtmueller
8cc19011f4 feat(scraper): all pricing scrapers to 2h 24/7 — full competitor coverage, no gaps 2026-04-05 01:32:08 +02:00
Rene Fichtmueller
44244a22a1 feat: 4th verification criterion (Competitor) + scraper frequency FS/10Gtek/ProLabs to 2h 2026-04-05 01:28:46 +02:00
Rene Fichtmueller
e4dfd2a2db feat(blog): AEM/APM pipeline steps + SLL context builder + LinkedIn v2 prompts 2026-04-05 01:26:09 +02:00
Rene Fichtmueller
95a8aa8552 fix: include linkedin_post in GET /api/blog response for SLL matching 2026-04-05 01:24:52 +02:00
Rene Fichtmueller
931588fffd fix(verification): 100% Verified Badge war dramatisch zu großzügig
KERNPROBLEME BEHOBEN:
1. ATGBICS part_number = URL slug statt echte OEM-Nummer
   extractOemPartNumber() entfernt -r-compatible-transceiver-* Suffix
   + trailing Vendor-Namen (nokia, cisco, juniper, ...)
   Ergebnis: 3he16564aa-nokia-r-compatible-transceiver-... → 3HE16564AA

2. reach_label = '' (leer) wurde als details_verified akzeptiert
   IS NOT NULL erlaubt leere Strings → Fix: AND reach_label != ''

3. details_verified = true trotz garbled part_number
   Neue Kriterien: NOT ILIKE '%-compatible-transceiver%'
                   NOT ILIKE '%-r-compatible%'

4. data_confidence Werte falsch in Funktion ('scraped_unverified' etc)
   Echte Werte: low/medium/high/garbage → NOT IN ('garbage','unknown')

ERGEBNIS nach recompute_all_verification():
  fully_verified: 3.654 → 581 (Badge war 6x übertrieben)
  details_verified: inflated → 1.075 (korrekt)

ATGBICS Scraper:
  - extractOemPartNumber() für collection und product detail pages
  - detectReach() jetzt auch auf URL-slug (120km im slug → reach_label)

Price Anomaly Detection:
  - API: price_anomaly field wenn max/min ratio ≥ 10x
  - Dashboard: ⚠ Preisanomalie Banner mit Ratio + EUR Range

SQL 025: Part number cleanup (30 records), reach from slug (12 records)
2026-04-04 15:41:57 +02:00
Rene Fichtmueller
1e789f67eb fix(scrapers): Flexoptix Catalog zeigt 0 records statt 963
SCRAPERS list used 'flexoptix-catalog' as DB lookup key but vendors.slug
is 'flexoptix' — no match → 0 records shown.

Fix: added dbSlug override field to SCRAPERS entries; lookup now uses
dbSlug || name so flexoptix-catalog/vendors/supported all map to
the correct 'flexoptix' slug in sourceMap.
2026-04-04 15:26:04 +02:00
Rene Fichtmueller
fea0b0fb66 feat: blog engine v5 — Auto-Kill Layer, 16-step pipeline, longer content
Upgrades FO Blog Pipeline from 14 to 16 steps:
- NEW Step 8d: Auto-Kill Layer v1.0 (10 systematic categories A-J)
- NEW Step 15: Auto-Kill Scoring (cleanliness, narrative, non-AI, relevance)
- Updated banned phrases from Gold-standard editorial feedback
- Soft Delete List for conditional phrases
- Auto-Kill categories: spec blocks, formulas, section leakage,
  generic transitions, repeated concepts, SKU mentions, false authority,
  over-explained basics, whitepaper tone, fake precision

Content length changes per user feedback:
- Blog target: 1,200-2,000 words (was 700-1,000) — thorough and detailed
- LinkedIn target: 2,000-2,800 chars (was 350-600) — use maximum length
- Reduction pass: 25-30% cut (was 15-25%) — remove weak, keep depth
2026-04-04 11:02:45 +02:00
Rene Fichtmueller
ede4f5b966 feat: blog engine v3 — 8-stage pipeline with Auto-Kill Layer
Complete rewrite of blog prompts and pipeline based on editorial
Gold-standard feedback. Replaces 3-pass system with 8-stage pipeline:

1. Master generation (narrative voice, no spec dumps)
2. Narrative Control (kill visible structure, enforce flow)
3. Auto-Kill Layer (remove AI phrases, spec residue, repetition)
4. Reduction Engine (cut 40% — keep strongest ideas only)
5. Depth pass (add specifics where vague, no spec dumps)
6. Quality Control (hard delete list validation)
7. Procurement layer (optional, sales audience)
8. LinkedIn post generation (new)

Key changes:
- System prompt rewritten with Hard Delete List (29 banned phrases)
- Soft Delete List for conditional phrases
- Auto-Kill categories A-J (spec blocks, formulas, whitepaper tone, etc.)
- Master prompts enforce continuous narrative, no section headings
- Word count targets reduced (800-1200 instead of 1500+)
- Scoring pass added (cleanliness, narrative, non-AI feel, relevance)
- LinkedIn companion post auto-generated
- Context data injection reduced (fewer items, no dump instructions)
2026-04-04 10:52:31 +02:00
Rene Fichtmueller
c509251109 feat(blog): Spec dump hard fail + Gold Standards 6 + LinkedIn v2
- System prompt: SPEC DUMP ABSOLUTE HARD FAIL block (before FORMAT rules)
  TX/RX tables, multi-optic comparison blocks, repeated sections = hard fail
  Behavioral prose rule: "what actually happens" not "what the spec says"

- STEP9 QA: check 12a SPEC DUMP — removes datasheet blocks, flags
  duplicate sections (e.g. "fiber types" twice), spec-heavy intros

- Gold Standard 6: 400G/800G deep dive corrected (8.8→10/10)
  zero spec tables, pure behavioral narrative, 3 core ideas max,
  ending is reframe not checklist

- LinkedIn Gold Example 2: sharper short format (346 chars vs 700)
  reframe hook, short beats without bullet markers, no emoji, 4 hashtags

- STEP_LINKEDIN_POST: rewritten with new gold format
  optimal 350-600 chars, beat rhythm, no bullet markers, gold example inline

- WRONG PATTERNS: +7 new entries (spec dump, duplicate section,
  LinkedIn bullet list, LinkedIn "excited to share" hook, LinkedIn >800 chars)
2026-04-04 09:32:01 +02:00
Rene Fichtmueller
4f631fc61e feat(blog): Reduction Engine v1.0 + LaTeX/connector hard fails
- Replace STEP8b_REDUCTION with 5-pass Reduction Engine:
  Pass 1: Repetition Kill (one concept, one home)
  Pass 2: Tech Prune (LaTeX hard delete, SKU removal, formula prose replacement)
  Pass 3: Flow Rebuild (close gaps after cuts, no new content)
  Pass 4: Weight Correction (title/content alignment throughout)
  Pass 5: Humanization (rhythm variation, hedge removal, punch ending)
  Target: 700-1000 words (600-1300 range, warnings outside)

- System prompt + STEP9 QA: add hard fails for
  LaTeX formulas (\[...\], \frac{}, \text{} etc) — destroys blog flow
  DR4 connector error (DR4=MPO-12, not LC duplex; FR4=LC duplex)
  Title/content mismatch (title topic must be the spine, not just the intro)

- Gold Standard 5: market alert / pricing article template
  (correct title alignment, no LaTeX, DR4=MPO-12, ending on topic)

- WRONG PATTERNS extended with 4 new entries covering above failures

- blog.ts: step log messages updated to 11-14/14; word count
  output shows % reduction and range warning (>1300 or <600)
2026-04-04 08:57:21 +02:00
Rene Fichtmueller
d6adb5600f ui: blog detail — separate blog article + linkedin post sections with copy buttons and char count badge 2026-04-04 08:35:33 +02:00
Rene Fichtmueller
3431ccbebc chore: changelog — blog engine v5 + linkedin post 2026-04-04 2026-04-04 08:30:54 +02:00
Rene Fichtmueller
1e19365e96 feat: blog engine v5 — narrative control + linkedin post + min words fix
- STEP4b_NARRATIVE_CONTROL: new pipeline step after draft; detects wrong
  narrative (technology blamed instead of processes), applies anti-FUD filter,
  reality reframe ("this becomes a problem when..."), Flexoptix voice check
- System prompt: NARRATIVE CONTROL RULE added as absolute rule #1
- Gold Standard 4: corrected "compatible vs OEM" article added as reference
- Minimum words: STEP4 raised from 1500 to 2500 words (final output was 750)
- Reduction pass: 25-35% → 15-25%, target 1500-2000 words final
- STEP_LINKEDIN_POST: generates LinkedIn post ≤2800 chars (hard limit 3000);
  stores in blog_drafts.linkedin_post + linkedin_char_count column
- Pipeline now 14 steps: v5-narrative-control
- Migration 024: linkedin_post + linkedin_char_count columns in blog_drafts
2026-04-04 08:30:27 +02:00
Rene Fichtmueller
be9209ffbd chore: changelog — proxy network geo/uptime fixes 2026-04-04 2026-04-04 08:15:58 +02:00
Rene Fichtmueller
ae0bda9e06 feat: proxy network — geo-lookup, uptime tracking, dedup fix
- IP geo-lookup via ip-api.com on register/heartbeat (country_code, city)
- heartbeat_count column + uptime_pct computation on every heartbeat
- Deduplication: register returns existing token for same IP+port
- Heartbeat no longer overwrites registered IP (prevents IPv6 churn conflicts)
- Migration 023: heartbeat_count column + backfill existing nodes
2026-04-04 08:15:32 +02:00
Rene Fichtmueller
9074b6ede0 docs: update changelog 2026-04-03/04 — scraper fixes, blog engine v4, proxy network, pg-boss fix 2026-04-04 07:58:29 +02:00
Rene Fichtmueller
f616e0ebbe feat: blog engine v4 (reduction+style-lock passes) + flexoptix scraper fixes
Blog engine (fo-blog-pipeline.ts):
- Add STEP8b_REDUCTION: cuts article 25-35%, removes repeated concepts
- Add STEP8c_STYLE_LOCK: enforces tone consistency, fixes scope/OPM confusion,
  removes inline SKUs from article flow
- Add Gold Standard 3 to calibration (Style B troubleshooting example 2026-04-04)
- Pipeline now 12 steps (was 10), version bumped to v4-reduction-stylelock

blog.ts:
- Wire STEP8b and STEP8c into pipeline between Kill-AI-Tone and QA Check
- Update progress tracking to 12 total steps
- Update pipeline_version to 'v4-reduction-stylelock'

flexoptix-catalog.ts:
- Fix contentHash call: pass object directly, not JSON.stringify(object)

db.ts:
- price_verified=true set in content_hash early-return path (no new observation)
- image_verified=true auto-set in findOrCreateScrapedTransceiver on INSERT/UPDATE
2026-04-04 07:50:01 +02:00
Rene Fichtmueller
0ac932a304 fix: flexoptix catalog scraper — 1G SFP coverage + SKU suffix + pagination
- Add 1G SFP search queries ("1G SFP", "SFP LX", "SFP SX", "SFP ZX") — were completely missing
- Strip vendor-compat suffix from SKU (S.1303.10.DG:Sx → S.1303.10.DG) to match existing records
- Remove 200-product cap, use full API pagination (page >= 50 limit only)
- Result: FLEXOPTIX 1G SFP coverage 50% → 97%, overall price coverage 62% → 88%
2026-04-04 07:26:13 +02:00
Rene Fichtmueller
c179b236d7 fix: auto-set image_verified and price_verified in db utils
- findOrCreateScrapedTransceiver now sets image_verified=true when writing image_url
- upsertPriceObservation now sets price_verified=true on the transceiver after inserting price
- Both INSERT and UPDATE paths covered for image_verified sync
- Eliminates need for manual backfill after scraper runs
2026-04-04 07:14:26 +02:00
Rene Fichtmueller
2913ad451b fix: reduce pg-boss pool size to 4, add idle_in_transaction_session_timeout
PostgreSQL max_connections was being exceeded (100/100).
- Limit pg-boss internal pool to 4 connections
- Added idle_in_transaction_session_timeout=30s to PostgreSQL config
- Already raised max_connections to 300 (container config)
System now stable at ~98/300 connections
2026-04-03 21:15:35 +02:00
Rene Fichtmueller
1026787318 feat: add proxy network, image backfill, and scraper improvements
- Add TIP Proxy Network (packages/proxy-agent): SOCKS5 proxy agent
  for residential IP bypass of CloudFront WAF blocks
- Add /api/proxy/* routes: node registration, heartbeat, load balancing
- Add image extraction to Flexoptix catalog scraper (GraphQL small_image)
- Add image extraction to Optcore scraper (Playwright gallery img)
- Fix Fluxlight price scraping (BigCommerce HTML structure: data-product-price-without-tax)
- Add SmartOptics scraper (8 DWDM/coherent products, og:image extraction)
- Fix findOrCreateScrapedTransceiver to update image_url for existing records
- Add image backfill script (backfill-images.ts): 178 Flexoptix images added
- Fix DB connection pool: max 5, idleTimeoutMillis 10s (was unlimited, caused >100 connections)
- Add proxy.ts utility for scraper proxy rotation
2026-04-03 21:13:03 +02:00
Rene Fichtmueller
abea0cd8fa fix: remove orphaned floating text causing TypeScript build error in fo-blog-pipeline
Dead code leftover from STEP4_MASTER_DRAFT rewrite was sitting outside
any template literal, causing compilation failure. Removed duplicate
CONTEXT DATA RULES block and orphaned `{{OUTLINE}}`/`{{CONTEXT_DATA}}`
placeholders that were not wrapped in a string.
2026-04-03 00:51:59 +02:00
Rene Fichtmueller
3a61723126 fix(blog): complete pipeline rewrite — eliminate sections/bullets, fix DR4 wavelength, fix scope description
Core changes:
- HARD RULES rewritten: zero tolerance for ## headers, #### Scenario: patterns, bullet sections
- Gold article added as reference standard in STEP4_MASTER_DRAFT
- MANDATORY SECTIONS removed — replaced with continuous prose requirement
- STEP3_OUTLINE: now a flow plan (3-4 beats), not a section list
- STEP5_REALITY_INJECTION: no longer adds 'What Breaks' sections — injects into prose
- STEP9_QA_CHECK: format violations now primary HARD FAIL, above content checks
- DR4 wavelength fix: 1310nm = 0.35 dB/km (not 1550nm = 0.22 dB/km)
- Scope description fix: visual inspection tool ≠ loss measurement device
- Invented firmware version numbers now explicit HARD FAIL
2026-04-03 00:43:14 +02:00
Rene Fichtmueller
c7697308f6 feat: NOG conference talks scraper + hot topics integration
NOG Talks Scraper (packages/scraper/src/scrapers/nog-talks.ts):
- Crawls DENOG (15-17), NANOG (91-93), RIPE (87-89), ENOG, NLNOG, Euro-IX
- Relevance scoring: optical keywords (+3pts each), network keywords (+1pt)
  Only talks scoring ≥2 stored, high-relevance (≥6) also to market_intelligence
- CtxEvent cross-DB bridge: when ctxmeet DB has ConferenceTalk rows,
  pulls directly via dblink (same Postgres instance, no network hop)
- Runs weekly Monday 06:00 UTC (pg-boss schedule)
- Output: news_articles (source='NOG Talks: EVENT') + market_intelligence

Hot Topics (packages/api/src/routes/hot-topics.ts):
- SOURCE 3c: NOG talk clusters displayed as conference topics in hot list
  Grouped by event (DENOG15, NANOG93...) with speaker + abstract preview
  Filtered: source LIKE 'NOG Talks:%' AND relevance > 0.4 AND < 6 months
- Limit raised to 20 topics (was 15)
- Added nog_talks to sources metadata

Scheduler & Pi fleet:
- scrape:nog-talks queue registered in scheduler.ts + index-pi.ts
- Weekly cron: Monday 06:00 UTC (every Pi can handle it independently)
- First job triggered immediately
2026-04-02 22:38:00 +02:00
Rene Fichtmueller
c81b9f5375 feat: hot topics now uses market_intelligence + LLM queue reset
Hot Topics:
- SOURCE 3b: market_intelligence table (15 items, 0.6+ relevance)
  with urgency mapping per intel_type + buy signal angles
- Fix news_articles: url → source_url (correct column name)
- Fix template literals: ${year} in string literals → backticks
- Increase limit: 6 → 15 topics returned
- Lower news cluster threshold: 2 → 1 article to form topic
- More research topics per day: 2 → 3
- More evergreen topics per day: 3 → 4
- Result: 24 total topics, 15 shown (was 8 total, 6 shown)

LLM Queue:
- Add resetOllamaQueue() export + auto-reset after 15min stall
- Add getQueueDepth() for monitoring
- New endpoints: GET /api/blog/llm/status, POST /api/blog/llm/reset-queue
2026-04-02 22:23:21 +02:00
Rene Fichtmueller
25839e731d fix: loadCrawlerStatus missing token — add var token = localStorage.getItem
token variable was undefined in loadCrawlerStatus() scope (only declared
inside IIFE auth guard, not globally). All API calls silently failed with
401. Fix: read token from localStorage at start of function, consistent
with getAuthHeaders() pattern used in all other load functions.
2026-04-02 16:00:27 +02:00
Rene Fichtmueller
69ef507d8a fix: scrapers status endpoint — correct table and column names
- Replace vendor_slug → vendors.slug via JOIN (column doesn't exist on transceivers)
- Replace competitor_prices + ebay_listings → price_observations (correct table)
- Replace market_intelligence → knowledge_base in status DB stats (table exists, 73 rows)
- price_observations columns: transceiver_id, price, time (not part_number/price_eur/scraped_at)
- Result: 5602 transceivers, 359 vendors, 465 switches, 4252 prices, 73 KB entries all visible
2026-04-02 15:24:18 +02:00
Rene Fichtmueller
e3b53343d4 fix: crawler intelligence hot topics now reads market_intelligence table
- Fix trend_score → relevance_score (correct column name in news_articles)
- Hot Topics source: market_intelligence table (not news_articles)
  → 15 items immediately available (capex_cycle, supply_chain, standards, etc.)
- KB summary: grouped by intel_type with count + top_relevance + latest date
- knowledge_base table ref → market_intelligence in /api/health status query
- Hot topics cards: intel_type badge + buy_signal_implication + score %
- Dashboard KB table: Intel Type / Items / Top Relevance / Latest columns
2026-04-02 15:22:09 +02:00
Rene Fichtmueller
6a89b5468b fix: smart tooltips that flip above/below based on viewport position
Replace static CSS ::after tooltips with JS-powered smart tooltips.
Tooltips now detect available space above/below and flip accordingly,
and clamp horizontally to viewport bounds. Hide on scroll.
2026-04-02 13:03:51 +02:00
Rene Fichtmueller
fe81b27248 fix: correct import paths in index-pi.ts (fs-com, tenGtek, utils/forecast-engine) 2026-04-02 09:36:51 +02:00
Rene Fichtmueller
6ccaa03932 feat: add index-pi.ts with all 44 workers for Pi fleet scraper nodes
Complete Pi scraper entry point covering all pricing, catalog, compat,
intelligence and prediction signal scrapers. Includes 5 new form-factor
coverage scrapers (comms-express, router-switch, multimode-inc,
optictransceiver, wiitek). Erik runs only API+DB, all scraping on Pis.
2026-04-02 09:34:05 +02:00
Rene Fichtmueller
f146ac873e feat: add 5 form-factor coverage scrapers with worker registrations
Add Comms-Express, Router-Switch.com, Multimode Inc, OpticTransceiver.com,
and Wiitek scrapers covering CFP2-DCO, CFP4, OSFP224, QSFP112, CXP, GBIC,
XENPAK, CSFP, SFP-DD, SFP56, QSFP56 and other previously-uncovered form
factors. Each scheduled every 8h. Worker registrations added to scheduler.

Also export db alias in utils/db.ts to fix eBay enricher + community scrapers
crashing with 'Cannot read properties of undefined (reading query)'.
2026-04-02 08:39:17 +02:00
Rene Fichtmueller
b7613538bf fix: remove internal IPs, ports, tokens from changelog entries 2026-04-02 08:16:48 +02:00
Rene Fichtmueller
b313522a2a chore: changelog entry for auth feature 2026-04-02 07:32:40 +02:00
Rene Fichtmueller
a066300cf2 feat: password-protected login page + API auth middleware
- POST /api/auth/login: HMAC-SHA256 signed 7-day token, password from DASHBOARD_PASSWORD env
- GET /api/auth/verify: stateless token validation
- requireAuth middleware applied to all /api/* routes (except /api/health + /api/auth)
- /dashboard/login.html: dark TIP-themed login page with show/hide password toggle
- index.html: auth guard redirect to login + Authorization header on all api() calls
- No secrets in code — password stored in .env only
2026-04-02 07:31:15 +02:00
Rene Fichtmueller
dba4c80e2f chore: update CHANGELOG_PENDING with Pi fleet + prediction intelligence system entries 2026-04-02 02:14:45 +02:00
Rene Fichtmueller
370c1d8801 feat: 6 prediction signal scrapers + forecast engine
New scrapers (all registered in pg-boss, 50 total jobs):
  - sec-edgar.ts       : SEC EDGAR XBRL API — hyperscaler CapEx from 10-Q/10-K
  - github-signals.ts  : GitHub Search/Stats API — tech adoption metrics weekly
  - ebay-velocity.ts   : eBay completed listings — sold count + price distribution
  - ai-clusters.ts     : RSS feeds (6 sources) — AI cluster & DC announcements
  - distributor-leads.ts : Mouser, Digi-Key, RS Components — lead time + stock
  - standards-tracker.ts : IEEE 802.3, OIF, IETF — draft/ballot/published status

New utilities:
  - forecast-engine.ts : Weighted signal aggregator → demand_index + price_direction
    6 signal types, 4 horizons (3/9/12/18 months), 5 technologies tracked

New DB tables (migration 022):
  hyperscaler_capex, distributor_lead_times, github_tech_signals,
  marketplace_velocity, ai_cluster_announcements, standards_activity,
  forecast_signals

Schedules:
  - EDGAR: weekly Mon 06:00
  - GitHub: weekly Sun 05:00
  - eBay velocity: every 12h
  - AI clusters: every 4h (news-speed)
  - Distributor leads: daily 03:30
  - Standards: weekly Wed 04:00
  - Forecast engine: daily 08:00 (after all nightly scrapers)
2026-04-02 02:02:44 +02:00
Rene Fichtmueller
c156e8d9f6 feat: download datasheets + manuals to Fearghas NAS in nightly sync
- downloadDocuments(): fetches PDFs from product_documents and documents tables
  using curl, organises into switches/ transceivers/ whitepapers/ other/ subdirs
- Integrated into runNightlyNasSync() — runs after JSON exports
- rsync incremental — only new/changed files transferred
- NAS dir structure: /volume1/tip-data/datasheets/{switches,transceivers,whitepapers,other}
- max-filesize 50MB guard per file
2026-04-02 01:47:16 +02:00
Rene Fichtmueller
5abe6397c4 feat: add logger utility + WireGuard setup in pi-scraper-setup.sh
- utils/logger.ts: minimal console-based logger (debug/info/warn/error)
  used by community-issues and ebay-enricher scrapers
- scripts/pi-scraper-setup.sh: step 7 adds optional WireGuard setup
  (pass WG_PRIVKEY + WG_ADDR env vars) — connects Pi to Erik for DB access
  auto-detects dead ethernet and routes WG traffic via working interface
2026-04-02 01:42:25 +02:00
Rene Fichtmueller
072978f1a4 feat: 24/7 scraping fleet — 8 new vendors + continuous schedule + Pi setup
New scrapers (8):
- BlueOptics (EUR, every 4h)
- ShopFiber24 (EUR, every 4h)
- T&S Communication (USD, every 4h)
- SmartOptics (catalog, every 8h)
- HUBER+SUHNER (catalog, every 8h)
- Skylane Optics (USD, every 4h)
- AscentOptics (USD, every 4h)
- GAO Tek (USD, every 4h)

Scheduler: nightly window → 24/7 continuous (42 jobs total)
- Playwright scrapers: every 8h (FS.com, 10Gtek, ATGBICS, ProLabs)
- Fetch/Cheerio: every 4h (11 lightweight vendors)
- Flexoptix catalog: every 2h (primary price source)
- eBay enrichment: every 6h
- Compatibility matrices: every 12h
- Compute jobs: every 4h

Pi fleet: scripts/pi-scraper-setup.sh for one-command Pi node setup
2026-04-02 01:09:05 +02:00
Rene Fichtmueller
1c1fb28189 fix: pre-create Crawlee subdirs to prevent ENOENT race in withIsolatedStorage
Remove orphan schedules (addon/naddod/qsfptek) that had no registered workers.
Pre-create request_queues/default, datasets/default, key_value_stores/default
before each scraper run to avoid ENOENT when Crawlee tries to write lock files.
2026-04-02 00:45:48 +02:00
Rene Fichtmueller
bb80e8bbe6 feat: tag all demo data with purple 'Demo Data' badge in Procurement Intelligence
- Added is_demo BOOLEAN column to market_intelligence, product_lifecycle_events, abc_classification, reorder_signals
- All 370 existing seed rows (021-procurement-demo-data.sql) marked is_demo=true
- Dashboard shows purple 'Demo Data' badge on:
  - Reorder signal card product names
  - ABC classification table product cell
  - Market Intelligence card titles
  - Lifecycle Events card titles
- Badge tooltip clarifies data is sample-only, not real market data
2026-04-01 23:41:40 +02:00
Rene Fichtmueller
4f8170dc36 feat: register ALL scrapers in nightly 00:00-08:00 window (30 jobs)
Previously missing from scheduler:
- Champion ONE, Fluxlight, GBICs, SFPCables pricing
- Juniper HCT, SONiC HCL, Ufispace, Edgecore compatibility
- Flexoptix supported vendors
- Switch assets enrichment

Full nightly sequence now covers every scraper in the fleet.
All jobs staggered with 15-30 min gaps to respect vendor rate limits.
2026-04-01 23:39:08 +02:00
Rene Fichtmueller
3e780ce6b7 feat: add tooltips throughout Procurement Intelligence tab + rename nav
- Rename nav tab and sub-nav from 'Procurement Intel' to 'Procurement Intelligence'
- Add data-tip tooltips to all 8 ABC table column headers
- Add title attributes to signal badges, ABC class badges, supply risk, stock/price/lead trend spans, signal strength bar
- Add hover descriptions to Market Intelligence type icons, buy signal badges, technology tags, impact horizon, source
- Add hover descriptions to Lifecycle Events type icons, buy signal badges, impact level, effective date
- Tooltips explain business meaning of every data point (e.g. ABC classification formula, demand score composition, supply risk levels)
2026-04-01 23:32:01 +02:00
Rene Fichtmueller
af69040070 fix: procurement demo data — correct schema column names for all 4 tables 2026-04-01 23:16:50 +02:00
Rene Fichtmueller
48218a553d feat: nightly scraper window 00-08 + NAS Fearghas sync + procurement demo data
- All scrapers now run nightly 00:00-08:00 (staggered, every day)
- NAS sync module: rsync JSON exports + weekly pg_dump to Fearghas via WireGuard
- 07:45 daily: price_observations, switches, transceivers, signals, issues exported as JSON
- Migration 021: 200 ABC classifications, 150 reorder signals, 300 stock snapshots demo data
- 9 market intelligence entries (LightReading, FierceTelecom, Farnell, Mouser, EU TED, Arista)
- 6 lifecycle events (ZR, 800G OSFP, 100G DR4 price floor, SFP-10G-SR EOL)
2026-04-01 23:07:26 +02:00
Rene Fichtmueller
732d7c3246 fix: switch seed lifecycle_status casing (Active not active) 2026-04-01 22:50:10 +02:00
Rene Fichtmueller
4020ec77d9 feat: product intelligence layer — eBay enricher, community issues, datasheets+manuals API
- Migration 020: product_issues table, condition/marketplace on price_observations, features JSONB
- eBay enricher: switch features/description/refurb prices + transceiver condition pricing
- Community issues scraper: Reddit/ServeTheHome/Arista/Cisco community bug reports
- 7 pre-seeded issues (DCS-7800R3, SG350, QFX5120, CRS326, USW-Pro etc.)
- API: /switches/:id/issues + /switches/:id/documents endpoints
- Dashboard switch modal: features from DB, description, eBay refurb price, issues+docs async
- Datasheet finder for Arista/Cisco/Juniper/HPE vendor pages
- Scheduler: 4 new jobs (ebay enrichment nightly, community issues weekly)
2026-04-01 22:46:27 +02:00
Rene Fichtmueller
64074f988f feat: SMB/campus switch seed 26 models (Cisco/HPE/Ubiquiti/MikroTik/Netgear/Zyxel) + fix forecast.ts fiveYearProjection accessor 2026-04-01 22:34:58 +02:00
Rene Fichtmueller
4b1734379a fix: Finder 404 shows helpful message + fuzzy switch name matching
- api() helper now parses JSON body on non-2xx responses so error.suggestion
  is available in catch blocks
- runFinder() catch shows 'Switch not found' + suggestion instead of 'Error: HTTP 404'
- finder.ts: normalized search (removes hyphens/spaces) + token-based fallback
  so 'sg350-28' → 'SG350-28', 'N9K-C93180' → Nexus 93180, etc.
2026-04-01 22:17:07 +02:00
Rene Fichtmueller
dad4750a86 feat: Changelog — CHANGELOG_PENDING.md, /api/changelog route, Overview tab widget
- CHANGELOG_PENDING.md: 26 entries from v0.1.0 to today in JSON-line format
- GET /api/changelog: parses and serves entries as JSON array
- Overview tab: changelog card with type badges (FEAT/FIX/UI/DATA/AI/INFRA),
  dates, show recent/all toggle
2026-04-01 22:14:14 +02:00
Rene Fichtmueller
681da54523 feat: Procurement Intelligence Engine (WS0c)
- Migration 019: stock_snapshots, abc_classification, reorder_signals,
  product_lifecycle_events, market_intelligence, crawler_llm_log tables
- Seeded 7 market intel events (OFC 2026, AWS/Azure CapEx, Coherent lead times,
  EU TED tenders, ECOC 2026, IEEE 802.3df)
- Seeded 4 lifecycle events (Cisco SFP-10G-LR EOL, Juniper EOL,
  400ZR ratified, 800G MSA draft)
- Crawler LLM: core.ts (Ollama-based extractor), stock-schema.ts (typed schemas
  + vendor profiles for Flexoptix/FS.com/10Gtek/ATGBICS/ProLabs/Farnell/Mouser),
  validator.ts (rule-based sanity checks + cross-validation)
- market-intelligence.ts scraper: OFC/ECOC, LightReading, IEEE 802.3, EU TED,
  Farnell/Mouser lead times, FierceTelecom — weekly via pg-boss
- computeAbcClassification(): dynamic A/B/C classification from price obs +
  compat count + vendor breadth
- computeReorderSignals(): buy_now/wait/hold/monitor with reasons + signal strength
- API: GET /api/procurement/overview|signals|signals/:id|abc|market-intel|
  stock-trends/:id|lifecycle
- Dashboard: Procurement Intel tab with Reorder Signals, ABC table,
  Market Intel cards, Lifecycle Events
2026-04-01 22:04:33 +02:00
Rene Fichtmueller
480decd307 fix: detect+warn garbage product names, add DB cleanup migration 018
- isGarbageName(): detects scraped-slugs, 'All Optical Transceivers', 'Compatible NNGbps...',
  generic form-factor descriptions with no real SKU
- Panel title priority: real standard_name → part_number → description → constructed from specs
- Details warning shown when details_verified = false (amber banner)
- sql/018: marks garbage entries as data_confidence='garbage' for future DELETE
2026-04-01 21:26:13 +02:00
Rene Fichtmueller
7b14ac4bbe fix: panel-title shows proper manufacturer name, not auto-generated slug
- isSlugLike() detects 'scraped-...' and lowercase-only-dash strings
- Priority: standard_name (if real) → part_number (if real) → description → constructed name
- Avoids showing 'scraped-o-czz8hg-z-a' as product title
2026-04-01 21:23:18 +02:00
Rene Fichtmueller
33c417cf21 fix: comparable price rows same layout as direct prices, part number as tooltip 2026-04-01 21:17:41 +02:00
Rene Fichtmueller
7fd9fd3c8a feat: competitor price comparison in transceiver detail
- API: also returns comparable_prices from technically equivalent products
  (same form_factor + speed_gbps + reach ±25%, different vendor, last 30 days)
- Dashboard: direct prices shown first, then separator + comparable products
- Comparable entries show vendor + exact part number scraped from their site
- Verified badge = real URL + observed within 7 days (strict)
2026-04-01 21:08:09 +02:00
Rene Fichtmueller
c23b9f68ce fix: verification bar white text, pricing removes unverified fallback
- 100% VERIFIED bar: checkmarks now rgba(255,255,255,0.92) instead of #2d6a4f (was invisible on green bg)
- Pricing: only show prices with real URL; no MSRP/estimated fallback
- Verified badge only if observed within 7 days; older prices shown without badge
2026-04-01 21:04:42 +02:00
Rene Fichtmueller
f91d2a15b9 feat: switch Flexoptix recommendations, switch verified labels, stronger verification check
- getCompatibleTransceivers: adds vendor_name, price, verification fields; Flexoptix sorted first
- Switch detail: data quality bar (Image/Product Page/Datasheet confirmed)
- Switch detail: Flexoptix Recommended section with prices, verified badges, shop links
- Switch detail: other vendors section shows 100% badge on slugs
- Transceiver detail: verification condition explicit === true (cache-safe)
- Transceiver detail: fallback text when no verification data exists yet
2026-04-01 20:59:30 +02:00
Rene Fichtmueller
3811b3b953 feat: temp range display, verification badges, competitor prices, tag tooltips
- Temperature Range: COM→'0-70°C (COM)', IND→'-40-85°C (IND)'
- GET /api/transceivers/🆔 returns competitor_prices[] from price_observations
- Detail view: verification summary bar (★ 100% VERIFIED / partial)
- Detail view: Current Prices section with vendor, price, verified badge, date, link
- Detail view: tag tooltips on vendor/category/market_status chips
- List view: new Verified column with 100% stamp or price check
- Optical Budget: TX Power Min/Max labels clarified
2026-04-01 20:47:02 +02:00
Rene Fichtmueller
cd48eee316 fix: panel close button dark on light background 2026-04-01 20:39:50 +02:00
Rene Fichtmueller
fe81c2d19d feat: visible close button, product name above image, SKU + descriptive name in list
- Panel close button: high-contrast rgba background, 38px, bold ×, shadow
- Detail view: full product name (SKU + description) shown above image
- List view: NAME column shows part_number on line 1, descriptive name line 2
- txDescName() helper builds name from description field or constructs from specs
2026-04-01 20:02:52 +02:00
Rene Fichtmueller
2b683dadfb feat: Verified Price + 100% Verified stamp system
DB (017-verification-tags.sql):
- New columns: price_verified, price_verified_eur, price_verified_url, price_verified_at
- New columns: image_verified, details_verified, fully_verified, fully_verified_at
- compute_transceiver_verification(uuid): per-product verification logic
  • price_verified: real scraped URL + price > 0 + observed in last 30 days
  • image_verified: R2 stored OR image_url from known vendor CDNs (flexoptix.net, fs.com, etc.), no placeholder
  • details_verified: product_page_url + all core fields (form_factor, speed, reach, fiber_type, part_number) populated
  • fully_verified: all three true simultaneously
- recompute_all_verification(): bulk recompute, returns stats
- Initial run: 3575 price_verified, 1173 image_verified, 1380 details_verified, 258 fully_verified
- Indexes on price_verified, fully_verified for fast filtering
- v_verified_products view

API finder.ts:
- SELECT now includes all verification fields
- Response maps: price_verified, price_verified_eur, price_verified_url, image_verified, details_verified, fully_verified

API health.ts:
- verification block: counts + coverage percentages in /api/health

Dashboard Finder:
- 'Verified Price': green checkmark ✓ next to price, tooltip explains source
- '100% Verified' stamp: dark green gradient badge top of card, card gets green border
- 'price source ↗' link to original scraped URL
- Summary bar: 'X × 100% Verified · Y with verified prices'
2026-04-01 17:43:48 +02:00
Rene Fichtmueller
6a6a22d303 feat: Switch→Transceiver Finder tab in dashboard
- New 'Finder' tab between Switches and Blog Engine
- Search by switch model (free text), filter by speed
- Quick-access buttons: Nexus 93180, Nexus 9332D, Arista 7280R3A, Juniper QFX5120
- Results grouped by speed class (400G QSFP-DD, 100G QSFP28, etc.)
- Shows: part number, vendor, reach, fiber type, connector, verified price, stock status
- Flexoptix products highlighted with orange left border + FLEXOPTIX badge
- Buy link → flexoptix.net for each result
- Uses existing /api/finder endpoint with 33,993 compatibility entries
2026-04-01 17:30:49 +02:00
Rene Fichtmueller
174078efdb feat: 100% verified data — no invented prices, part numbers, or designations
gatherBlogData():
- Fetches real prices from price_observations (last 30 days) per product
- Filters transceivers by speed extracted from topic keywords
- Enriches every product with verified_prices array + has_verified_price flag
- Joins DB products with vector search results (DB first — they have real prices)

contextData injection (blog.ts):
- [PRODUCT] lines: exact standard_name, form_factor, speed, reach, connector, dBm specs, Watts
- [VERIFIED PRICE] lines: real EUR/USD price, vendor, observed date, source URL
- [NO VERIFIED PRICE IN DB]: explicit tag — LLM must not invent a number
- [NO PRODUCT DATA AVAILABLE]: fallback when DB returns nothing

fo-blog-pipeline.ts system prompt:
- DATA INTEGRITY RULES block: prices/part numbers/vendors ONLY from context
- Never approximate with ~€350 or 'typically $200-600' for specific products
- Power specs only from [PRODUCT] data or REFERENCE VALUES

STEP4 context instructions:
- Explicit rules on how to use [VERIFIED PRICE] vs [NO VERIFIED PRICE]
- Invented data = HARD FAIL in QA

STEP9 QA — 3 new hard fail checks (30, 31, 32):
- Check 30: invented prices → remove or replace with flexoptix.net reference
- Check 31: invented part numbers → remove, use class name instead
- Check 32: invented vendor names → remove if not in known list
2026-04-01 17:27:55 +02:00
Rene Fichtmueller
ee8b3c0779 feat: hot topics daily rotation — 30+ topic pool, seeded shuffle, next-refresh countdown
- Expanded research pool to 9 topics (was 3), evergreen to 12 (was 3)
- Conference topics: added Photonics West, CIOE, NFOEC follow-up, year-end review
- Standards topics: 3 rotating variants (IEEE tracker, SFF-8024 registry, OIF CEI-112G)
- seededShuffle(): day-of-year as seed — stable within the day, different every day
- API response adds refreshes_at (next midnight UTC) for frontend countdown
- Dashboard subtitle shows 'rotates daily · next refresh in Xh'
- Hot topic cards now pass full title + angle into generateBlog() correctly
2026-04-01 11:12:38 +02:00
Rene Fichtmueller
580df8be01 blog: calibration v8 — AI phrasing blacklist, STEP8 6-step rewrite, Flexoptix author identity
- STRICTLY FORBIDDEN section: comprehensive AI phrasing blacklist (leverage, utilize,
  Furthermore, robust, seamless, delve into, in conclusion, etc.)
- STEP8 rewritten into 6 structured steps: hunt AI words, break rhythm, add human element,
  fix label formats, structural cleanup, ensure Flexoptix identity
- Banned sentence structures added (parallel triplets, same-length paragraphs)
- STEP6 author identity: reader should know this came from Flexoptix
- Version bump to 0.3.0
2026-04-01 00:43:38 +02:00
Rene Fichtmueller
52a04129e2 blog: calibration v7 — remove Cause/Fix/Example labels, integrate as prose narrative 2026-04-01 00:10:50 +02:00
Rene Fichtmueller
6b77b18842 fix(blog): extract article from QA, status badge ready/step X/10, calibration v6 Flexoptix balance 2026-03-31 23:52:56 +02:00
Rene Fichtmueller
ef0b0bb148 fix(llm): add 429 retry with exponential backoff + ollamaQueue concurrency guard 2026-03-31 21:45:46 +02:00
Rene Fichtmueller
01ad16464d blog: calibration v5 — anti-consulting-prose, correct loss budget math, vendor lock-in specifics 2026-03-31 21:26:39 +02:00
Rene Fichtmueller
ea554ebd6f security: remove hardcoded secrets from ecosystem.config.js, add example template 2026-03-31 20:36:33 +02:00
Rene Fichtmueller
58a26116b1 fix(blog): 3s delay between queued LLM pipelines to prevent nginx 429 bursts 2026-03-31 19:40:40 +02:00
Rene Fichtmueller
45abd15fe4 blog: calibration v4 — technical accuracy + structure limits
- Fix SR4/DR4 fiber count: both use 8 fibers (4TX+4RX), difference is MMF vs SMF
- Fix power per port: 400G=10-15W/port, 800G=15-25W/port (not 1kW/port)
- Fix pricing context: always distinguish OEM ($1-5K) vs compatible ($200-600)
- Add HARD RULES 15-21: fiber count, power, pricing, no markdown headers,
  max 6 sections, no repeated topics, flow over format
- Add QA CALIBRATION FAILS 16-21: same rules enforced at QA step
- Add fiber/power reference tables with correct values
- Strip markdown (##/###/####/**) from all output — plain text only
- Add Style B gold example (10/10 validated prose article)
- Update STEP5 reality injection with correct SR4→DR4 description
- Update STEP8 kill-AI-tone to strip markdown headers + merge duplicates
2026-03-31 17:27:51 +02:00
Rene Fichtmueller
315a988775 feat(blog): add Style B prose calibration — 10/10 narrative flow standard
- CALIBRATION_GOLD_STANDARD now covers two validated styles: A (structured) and B (prose)
- Style B: no headers, no bullets, 1-3 sentence paragraphs, reframe ending
- STEP8_KILL_AI_TONE: prose conversion option for over-structured articles
- STEP4_MASTER_DRAFT: explicit style choice instruction (A vs B based on angle)
- Gold standard includes exact prose rhythm patterns from 10/10 human-reviewed article
- Wrong patterns expanded: symmetric sections, checklist endings, transition clichés
2026-03-31 16:48:10 +02:00
Rene Fichtmueller
f71ef2b20c feat(blog): regenerate button, SEO hashtags, calibration engine v2
- POST /api/blog/:id/regenerate — re-runs full 10-step LLM pipeline on existing draft
- Regenerate button visible when quality_issues present or status=review
- SEO keywords now displayed as clickable #hashtags (copy-to-clipboard)
- fo-blog-pipeline: added PoE misuse, DR4 mislabeling, ZR/DR4 conflation as hard QA fails
- fo-blog-pipeline: 14 hard rules in system prompt (was 10)
- fo-blog-pipeline: CALIBRATION_GOLD_STANDARD + withCalibration() from 10/10 human review
- System prompt now includes gold standard example on every pipeline run
2026-03-31 16:46:25 +02:00
Rene Fichtmueller
12d12aab4f feat(v0.2.6): hot topics + pipeline lock + blog delete + clean external JS
Hot Topics:
- Dynamic topics from /api/hot-topics loaded in Blog Engine tab
- 7 data sources (prices, competitors, hype cycle, news, conferences, research, evergreen)
- Urgency badges: BREAKING (red), HOT (orange), TRENDING (yellow), EMERGING (green)

Pipeline Lock:
- Only 1 generation at a time, 'Pipeline Busy' toast on double-click
- Progress bar with step names (external hot-topics.js, no inline hacks)

Blog Delete:
- DELETE /api/blog/:id endpoint
- Delete button (✕) on each blog in list
- 'Delete All Templates' button to clean up test drafts

Fix: dashboard JS extracted to external hot-topics.js to avoid sed quote hell
2026-03-31 09:54:33 +02:00
Rene Fichtmueller
3132b58309 feat(v0.2.5): hot topics engine + pipeline lock + UX fixes
Hot Topics Engine (GET /api/hot-topics):
- 7 data sources: price movements, competitor alerts, hype cycle transitions,
  news articles, conference calendar, research trends, evergreen topics
- Auto-discovers BREAKING/HOT/TRENDING/EMERGING topics
- Dashboard loads topics dynamically with urgency badges and source labels
- Click any topic → generates blog with that angle

Pipeline Lock (critical UX fix):
- Only 1 blog generation at a time (blogPipelineRunning flag)
- 'Pipeline Busy' toast if user clicks while generating
- Lock released on completion, timeout, or error

Dashboard:
- Static 3 cards replaced with dynamic hot topics grid
- 'Refresh Topics' button
- Topics show urgency color (red=breaking, orange=hot, yellow=trending, green=emerging)
- Auto-loads when Blog Engine tab opens
2026-03-31 09:49:43 +02:00
Rene Fichtmueller
278207078b feat(v0.2.4): blog generation UX overhaul — live progress bar
When you click Generate:
- Dark overlay with orange progress bar shows pipeline status
- Live step counter: 'Step 3/10: Outline Generation — decision-driven structure'
- Percentage updates every 15 seconds via API polling
- When done: shows word count + QA score, auto-opens the article
- No more silent template dump — user sees the entire pipeline working
2026-03-31 09:44:29 +02:00
Rene Fichtmueller
4233118505 fix(v0.2.3): dashboard polling for LLM blog pipeline
Root cause: pollBlogLlm() checked for 'llm' in generated_by but pipeline
sets 'fo-blog-engine-v3'. Dashboard showed template forever.

Fixes:
- Poll check: now detects any non-template generated_by
- Poll timeout: 20s interval × 60 attempts = 20 min (pipeline takes ~10 min)
- Status toast shows pipeline step progress (Step X/10)
- Generation message tells user LLM runs ~10 min in background
- Version bump to v0.2.3
2026-03-31 09:41:20 +02:00
Rene Fichtmueller
9bb2f549f8 fix(v0.2.2): OLLAMA_URL pointed to localhost instead of .213 via WireGuard
Blog engine was falling back to template because qwen2.5:14b is on Mac Studio (.213),
not on Erik (localhost). Fixed ecosystem.config.js to use 192.168.178.213:11434.
This was the root cause why the 10-step pipeline never executed.
2026-03-31 09:28:34 +02:00
Rene Fichtmueller
c01d69e02e fix(blog): harden pipeline prompts based on v0.2.1 blog review feedback
System prompt: 10 HARD RULES (non-negotiable, article fails QA without them)
- Mandatory WHAT BREAKS IN PRODUCTION section (2+ specific failures with symptoms/cause/fix)
- Mandatory HIDDEN COSTS section (cleaning, troubleshooting time, cabling redesign, training)
- Mandatory WHEN NOT TO USE section for every recommendation
- Absolute statement rule: NEVER without conditions/context
- Cabling reality: MPO polarity, SR4→DR4 migration, cleaning requirements
- Brutal hook requirement: not 'If you're still...' but 'You're about to sign a PO. Stop.'
- Minimum 2500 words (was 2000)

Step 5 (Reality Injection): Now checks for ALL mandatory sections and adds if missing
Step 9 (QA Check): Hard fail checks — article is NOT publishable without production failures + hidden costs
Feedback source: Human expert review scoring 7.5/10, targeting 9.5+
2026-03-31 09:24:08 +02:00
Rene Fichtmueller
6bd168e958 chore: bump version to v0.2.1 2026-03-31 09:19:38 +02:00
Rene Fichtmueller
e4c89de6c0 feat: fs.com scraper Phase 2 — crawl product detail pages for verified specs
- New spec-updater utility: parseSpecTable() + updateVerifiedSpecs()
- fs.com scraper now has 2 phases:
  Phase 1: Category pages → prices + stock (existing)
  Phase 2: Product detail pages → fiber_type, connector, wavelength, power, image, datasheet
- Updates data_confidence from 'enriched_estimated' to 'scraped_unverified'
- Processes up to 200 product pages per scraper run
2026-03-31 09:18:27 +02:00
Rene Fichtmueller
eec42e4818 feat: wire 10-step FO Blog Pipeline into blog generation route
Replaces old 2-pass pipeline with full Flexoptix Style 10-step generation:
1. Topic Expansion (real scenarios + wrong assumptions)
2. Angle Selection (single strong angle + audience)
3. Outline Generation (decision-driven, no generic sections)
4. Master Draft (Flexoptix voice, 2000+ words)
5. Reality Injection (failure scenarios, operational pain)
6. Technical Deepening (specific optics, power, density)
7. Opinion Layer (clear positions, no neutrality)
8. Kill AI Tone (remove all AI fingerprints)
9. QA Check (technical accuracy verification)
10. Quality Score (1-10 auto-rating, saved as self-feedback)

Feedback loop active:
- Accumulated feedback injected into system prompt
- Auto QA scores saved to blog_feedback table
- Training data export via GET /api/blog/feedback/training-data
2026-03-31 09:16:23 +02:00
Rene Fichtmueller
d1d23ce31d feat(v0.2.1): data confidence tracking + validation + blog feedback system
- Migration 016: data_confidence column (vendor_verified/enriched_estimated/scraped_unverified)
- Migration 015: blog_feedback table with 8 quality scores + free text
- Validation script: 8 physics-based rules (wavelength↔fiber, reach plausibility, power limits)
- Blog feedback API: POST /api/blog/:id/feedback + training data export
- FO Blog Pipeline v3: 10-step Flexoptix Style prompts (Less bullshit. More engineering.)
- Auto-fix: wavelength↔fiber mismatches corrected automatically
2026-03-31 09:12:37 +02:00
Rene Fichtmueller
cd90d22762 feat: spec enrichment script — fills 4,400+ products with fiber/connector/wavelength/power/reach
Coverage improvement:
- fiber_type: 44% → 97%
- connector: 19% → 96%
- wavelengths: 23% → 96%
- reach_meters: 42% → 93%
- power_consumption_w: 6% → 79%
2026-03-31 09:02:10 +02:00
Rene Fichtmueller
531e25b327 chore: bump version to 0.2.0 in health endpoint 2026-03-31 08:59:00 +02:00
Rene Fichtmueller
1f8176bf8e fix: UUID cast in datasheet routes — use slug-first lookup 2026-03-31 08:58:26 +02:00
Rene Fichtmueller
24a9eba9ce feat(v0.2.0): datasheets + adoption roadmap + all routes registered
- GET /api/datasheets/transceiver/:id — Full datasheet with power budget, pricing, compatibility, HTML export
- GET /api/datasheets/switch/:id — Switch datasheet with compatible transceivers
- GET /api/adoption — Full technology roadmap with maturity indicators
- GET /api/adoption/:technology — Detailed adoption analysis, migration paths, risks, timelines
- All v0.2.0 routes registered in index.ts
2026-03-31 08:57:03 +02:00
Rene Fichtmueller
a69acc4588 feat(v0.2.0): Sales Intelligence Engine — Phase 0+A
New API routes:
- GET /api/finder — Switch→Flexoptix transceiver finder with FlexBox coding
- GET /api/competitor-alerts — Competitor intelligence (price changes, new products, stock)
- GET /api/forecast/:technology — Sales forecast 3/9/12/18 months + buy/wait/hold signal
- POST /api/transport/plan — Transport system planner (city→city BOM with fiber providers)

New MCP tools:
- find_flexoptix_for_switch — Customer switch → Flexoptix products
- get_competitor_alerts — Competitor monitoring
- plan_transport — Network transport planning
- forecast_sales — Volume/revenue prediction
- generate_blog — Enhanced blog generation

New DB tables (migration 013):
- competitor_alerts, price_changes, flexoptix_product_map
- sales_forecasts, fiber_providers, fiber_routes, cities
- generated_datasheets, blog_series
- Views: v_price_coverage, v_image_coverage, v_switch_flexoptix_finder

Seed data (migration 014):
- 25 European cities with IX/DC locations + coordinates
- 15 fiber providers (euNetworks, Telia, DTAG, Colt, Zayo, etc.)
- 16 fiber routes with pricing (Germany focus)

Infrastructure:
- Scraper scheduler: 2h Flexoptix, 4h FS.com/Optcore (was 6-8h)
- Change detector for competitor price/stock monitoring
- Image downloader utility with coverage tracking
2026-03-31 08:51:22 +02:00
Rene Fichtmueller
0b07490114 chore: sync local changes 2026-03-31 07:32:02 +02:00
Rene Fichtmueller
2348238888 feat: add NADDOD, QSFPTEK, and AddOn Networks scrapers
Three new fetch-based price scrapers for compatible optics vendors:
- NADDOD: WooCommerce, USD, ~800+ SKUs
- QSFPTEK: Custom PHP shop, USD, ~1000+ SKUs
- AddOn Networks: Magento/custom, USD, ~2500 SKUs

All registered in scheduler (8-12h intervals) and index.ts --flags.
Build: 0 TypeScript errors.
2026-03-30 21:20:23 +02:00
Rene Fichtmueller
fcddd1f27b fix: contentHash type errors + fs-com scraper improvements
Remove JSON.stringify wrapper from contentHash calls — function
expects Record<string,unknown>, not string. Fixes TS build for
6 scrapers. Update fs-com category URLs and add currency/lang cookies.
2026-03-30 21:07:27 +02:00
Rene Fichtmueller
52e2f16d75 feat: 5-year forecast area chart + regional adoption heatmap
Dashboard now uses /api/hype-cycle/enriched for forecast data.
Forecast chart: SVG area chart with adoption % per technology over 5 years.
Regional heatmap: table with market share intensity per region per tech.
Both render below the existing hype cycle table.
2026-03-30 20:57:08 +02:00
Rene Fichtmueller
f614c425ea feat: hype cycle hover tooltips + phase legend, fix switch-docs missing column
Dashboard: Added hover tooltips on hype cycle dots showing phase, adoption %,
peak year, score. Added color-coded phase legend with technology counts.
MCP: Fixed docs_portal_url column reference in switch-docs tool.
2026-03-30 08:25:41 +02:00
Rene Fichtmueller
814325b349 feat: dashboard v2, blog expansion, market/cable MCP tools, switch asset scrapers, scraper utilities 2026-03-30 08:07:12 +02:00
Rene Fichtmueller
615a7e50c7 fix: remove non-existent vendor URL columns, fix text=uuid cast in transceiver lookup 2026-03-30 07:49:54 +02:00
Rene Fichtmueller
39dc5a4ab4 fix: add trust proxy for Cloudflare — fixes ERR_ERL_UNEXPECTED_X_FORWARDED_FOR in rate limiter 2026-03-30 06:41:36 +02:00
Rene Fichtmueller
f8892f058f feat(deploy): add tip-mcp PM2 process (port 3202) + Cloudflare tunnel tip.context-x.org 2026-03-29 08:21:14 +08:00
Rene Fichtmueller
6f7c834752 feat(scrapers+mcp): ATGBICS + ProLabs scrapers, MCP HTTP/SSE server
Scrapers:
- atgbics.ts: PlaywrightCrawler for UK vendor ATGBICS (Shopify store),
  scrapes SFP/SFP+/SFP28/QSFP+/QSFP28/QSFP-DD in GBP, max 50 pages/run
- prolabs.ts: HttpCrawler for ProLabs (Legrand subsidiary), USD pricing,
  category-driven crawl with reach/fiber/speed detection
- Both registered in scheduler (every 8h, staggered) and index.ts CLI

MCP HTTP Server:
- packages/mcp-server/src/http-server.ts: Express + SSEServerTransport
- Exposes all 12 TIP tools via GET /sse + POST /message
- Bearer token auth (MCP_SECRET env), CORS-configurable
- GET /health → { status: "ok", tools: 12 }
- Port: MCP_HTTP_PORT (default 3201)

SQL + tools:
- sql/006-009: seed scripts for whitebox switches, vendors, assets
- switch-docs.ts: MCP tool for switch documentation queries
2026-03-29 02:26:45 +08:00
Rene Fichtmueller
aebb1249c5 feat: redesign dashboard — glassmorphism, animated background, gradient accents
Premium dark UI with ambient glow background, glass cards, gradient
text, animated counters, glow effects on hype cycle dots, smooth
transitions, and improved detail panel with regional adoption and
revenue lifecycle data. Fix API data key mismatch (data vs transceivers).
2026-03-28 02:42:55 +13:00
Rene Fichtmueller
280bf8f50a feat: calibrate regional adoption model with research-backed parameters
Update REGIONAL_LAGS with data from LightCounting, vendor earnings,
OFC market sessions, and Chinese IPO prospectuses. Add price index
per region and segment mix (hyperscaler/telco/enterprise) for
more accurate regional revenue modeling.
2026-03-28 02:34:29 +13:00
Rene Fichtmueller
70447def02 feat: massive scraper expansion + hype cycle engine + lifecycle prediction
New scrapers:
- GBICS.com (BigCommerce, GBP prices, 10 categories, 78 products)
- Juniper HCT (Next.js SSR parser, 475 transceivers with specs/EOL)
- SFPcables.com (Magento store, 16 categories, 78 products)
- Fluxlight (BigCommerce, 6 pages, 118 products)
- Champion ONE (compatible vendor scraper)

Scraper fixes:
- 10Gtek: rewritten to parse HTML spec tables (152 products)
- Flexoptix: fix price extraction from Magento Hyva HTML
- Register all scrapers in CLI (--gbics, --juniper, --sfpcables, etc.)

Hype Cycle Engine enhancements:
- Data-driven enrichment from scraped vendor/price data
- Revenue lifecycle prediction (peak year, decline, revenue index)
- Regional adoption model (NA, China, APAC, Europe, RoW with lag coefficients)
- New API endpoints: /enriched, /lifecycle, /regional/:tech

DB growth: 89 → 1,168 transceivers, 0 → 416 prices, 6 vendors
Qdrant: 1,162 products embedded with nomic-embed-text

Research: Norton-Bass model, standards-to-market timelines, hype signals
2026-03-28 02:30:19 +13:00
Rene Fichtmueller
204e99763c feat: add Flexoptix product catalog scraper, register in CLI
Scrapes flexoptix.net product catalog across 9 categories (SFP through OSFP).
Extracts product names, prices, form factors, reach, fiber type, wavelength.
CLI: --flexoptix flag, integrated into --all.
2026-03-28 01:02:34 +13:00
Rene Fichtmueller
312c5cb815 fix: hype cycle findTechnology matched wrong tech (1G instead of 1.6T)
findTechnology used loose includes() matching — '1.6T OSFP-XD' matched
'1G SFP' first because query contained '1'. Now matches exact name first,
then by speed prefix with proper unit parsing (G/T).
2026-03-28 01:00:52 +13:00
Rene Fichtmueller
28a0f25b23 feat: complete dashboard redesign — professional data-dense UI, click-through detail panels
- Removed AI-slop gradient header, replaced with compact info bar
- JetBrains Mono for data, Inter for text — proper engineering tool aesthetic
- Unified slide-in detail panel for hype cycle, transceivers, and blog drafts
- Transceiver table: clickable rows open full spec sheet in side panel
- Form factor dropdown filter on transceivers tab
- Blog drafts: click to read full content in panel
- Tighter spacing, smaller fonts, higher information density
- Escape key closes panel, responsive breakpoints
- No gratuitous badges/glow — clean data presentation
2026-03-28 00:56:27 +13:00
Rene Fichtmueller
1cc3844822 feat: interactive SVG hype cycle visualization with click-through detail panel 2026-03-28 00:52:17 +13:00
Rene Fichtmueller
e83711684f fix: replace alert() with slide-in toast notification in dashboard 2026-03-28 00:47:08 +13:00
Rene Fichtmueller
a6f2b2ef9e feat: Phase 8 — Dashboard frontend + static serving
Single-file dashboard with 6 tabs: Overview, Semantic Search,
Hype Cycle, Transceivers, News, Blog Engine. Dark theme, no
build step, served as static HTML from Express.

- Overview: health stats, vector collection counts, recent news
- Semantic Search: query across all 6 Qdrant collections
- Hype Cycle: Norton-Bass table with phase colors + position bars
- Transceivers: searchable table with form factor/speed/reach
- News: semantic news search with source links
- Blog: generate drafts from templates, view draft history

Live at: https://transceiver-db.context-x.org/dashboard/
2026-03-28 00:37:10 +13:00
Rene Fichtmueller
274b80a4f1 feat: Phase 7 — Blog generator + scraper scheduler activation
Blog draft engine generates structured markdown from all Qdrant
collections (products, news, FAQ, troubleshooting). Supports 4
topic types: hype_cycle, comparison, new_product, tutorial.

- routes/blog.ts: POST /api/blog/generate, GET/PUT endpoints
- ecosystem.config.js: Added tip-scraper PM2 process
- Scraper scheduler (pg-boss) now running on Erik with 8 job queues
- News scraper running every 6 hours on Erik
2026-03-28 00:32:08 +13:00
Rene Fichtmueller
4cb2db6455 feat: Phase 6 — FAQ + troubleshooting knowledge base embeddings
19 curated FAQ entries covering form factors, fiber types, reach,
compatibility, WDM, power, and emerging tech (CPO, LPO, 400ZR).
10 troubleshooting guides with symptom/cause/solution format.

All 6 Qdrant collections now populated:
- product_embeddings: 89 transceivers
- datasheet_chunks: 40 chunks (OCR pipeline)
- faq_embeddings: 19 FAQ entries
- troubleshooting_embeddings: 10 guides
- news_embeddings: 33 articles
- manual_chunks: 0 (pending manual ingestion)
2026-03-28 00:24:50 +13:00
Rene Fichtmueller
8bb3b586f3 feat: Phase 5 — OCR pipeline + document/news search
Docling-powered OCR pipeline: PDF → markdown → chunks → Ollama embed → Qdrant.
News embedding seeder for news_embeddings collection.
Document and news semantic search API endpoints.

- embeddings/ocr-pipeline.ts: Docling convert → chunk → embed pipeline
- embeddings/seed-news.ts: Batch embed news_articles into Qdrant
- routes/documents.ts: POST /api/documents/process, GET /api/documents
- routes/search.ts: GET /search/documents, GET /search/news endpoints
- sql/005-documents.sql: Add chunks_count, processed_at to documents table
- Ollama + nomic-embed-text installed on Erik (CPU mode)
- 89 products + 40 datasheet chunks + 33 news articles in Qdrant
2026-03-28 00:22:01 +13:00
Rene Fichtmueller
6d3e5cc04a feat: Phase 4 — Vector embeddings + semantic search
Ollama nomic-embed-text (768 dim) → Qdrant vector search pipeline.
Embeds all 89 transceivers with rich text representation and payload
filters (form_factor, speed_gbps, fiber_type, wdm_type).

- embeddings/client.ts: Ollama embed + Qdrant upsert/search
- embeddings/seed-products.ts: Batch seeder for product_embeddings
- routes/search.ts: GET /api/search, /search/products, /search/stats
- 6 Qdrant collections: products, datasheets, FAQs, manuals, troubleshooting, news
2026-03-28 00:05:29 +13:00
Rene Fichtmueller
eb875f37d2 feat: Phase 3 — Norton-Bass Hype Cycle Engine
Implements the full Norton-Bass Multigenerational Diffusion Model for
transceiver technology lifecycle forecasting.

Math: Bass diffusion F(t) + logistic adoption S(t) = L / (1 + e^(-k(t-t0)))
Parameters: p (innovation ~0.03), q (imitation ~0.3-0.5), m (market potential)

Phase Classification Engine (composite score):
  30% Port shipment share + 20% ASP decline rate + 15% Standards maturity
  + 15% Interop validation + 10% Vendor trajectory + 10% Media sentiment

11 technologies tracked: 1G → 10G → 25G → 40G → 100G → 400G → 800G → 1.6T
  + CPO, LPO, 400ZR Coherent
5-year adoption forecast per technology

API: GET /api/hype-cycle (all) + GET /api/hype-cycle/:tech (detail)
Live: https://transceiver-db.context-x.org/api/hype-cycle
2026-03-27 23:35:57 +13:00
Rene Fichtmueller
bd3a02ae4b feat: add Flexoptix vendor scraper, 10Gtek pricing scraper, expand news feeds
- Flexoptix vendor scraper: 285 supported switch vendors ingested from
  flexoptix.net/en/supported-vendors/ (our own data, no restrictions)
- 10Gtek Playwright scraper: Chinese OEM competitor pricing (SFP+, SFP28,
  QSFP+, QSFP28, QSFP-DD categories)
- News feeds expanded: added Lightwave, Fierce Telecom, Data Center Knowledge,
  SDxCentral, Cisco Blogs, Arista Blog (11 total sources)
- Scheduler updated: 8 job queues with appropriate intervals
- DB now: 297 vendors, 89 transceivers, 33 news articles (13 relevant)
2026-03-27 23:17:42 +13:00
Rene Fichtmueller
92f42832bf feat: Phase 2 — MCP Server with 12 tools
Implements all 12 MCP tools from CONCEPT document:
- search_transceivers: Full-text + spec filter search with pricing
- check_compatibility: Switch ↔ transceiver compatibility lookup
- get_pricing: Current prices + 30-day history across all vendors
- compare_prices: Multi-vendor price comparison with savings analysis
- get_competitor_stock: Live competitor stock monitoring (sales opportunities)
- suggest_alternatives: Similar spec alternatives optimized for price/availability
- get_templates: FlexBox coding and switch config template finder
- search_knowledge_base: Troubleshooting FAQ search (PostgreSQL full-text)
- search_manuals: Switch manual and datasheet search
- get_hype_cycle: Norton-Bass adoption forecast + Gartner phase classification
- get_market_news: Aggregated news with relevance scoring
- generate_blog_draft: Data-driven blog drafts saved to blog_drafts table

Transport: stdio (MCP protocol 2024-11-05)
Config: .mcp.json for Claude Code integration
Verified: all 12 tools registered, search_transceivers returns DB results
2026-03-27 16:48:34 +13:00
Rene Fichtmueller
e9fb50a248 feat: TIP Phase 0+1 — monorepo, DB schema, API, scraper engine
Phase 0 - Foundation:
- Restructure into npm workspace monorepo (packages/core, api, scraper)
- PostgreSQL 17 + TimescaleDB schema (15 tables incl. hypertables)
- Docker Compose for local dev (PostgreSQL on 5433 + Qdrant)
- Express 5 API on port 3200 with 6 routes
- Seed script to migrate 159 transceivers + 42 standards from npm package
- Erik server setup script + PM2 ecosystem config

Phase 1 - Scraper Engine:
- Crawlee + Playwright framework with pg-boss scheduler
- FS.com scraper (PlaywrightCrawler, anti-bot workaround)
- Optcore.net scraper (WP REST API enumeration + PlaywrightCrawler)
  - Uses /wp-json/wp/v2/product to get 2000+ product URLs
  - Playwright renders individual product pages for price extraction
- Cisco TMG Matrix scraper (compatibility data)
- News RSS aggregator (optics.org, SPIE, Network World, Nature Photonics)
  - Keyword relevance scoring for transceiver/fiber topics
  - xml2js with malformed XML sanitization
- SHA-256 content hashing for change detection (skip unchanged records)
- pg-boss v10 with explicit queue creation before scheduling
2026-03-27 16:27:31 +13:00
21 changed files with 598 additions and 114 deletions

View File

@ -147,6 +147,8 @@ export async function registerSchedules(boss: PgBoss): Promise<void> {
"sync:nas",
// ── Health Monitoring ─────────────────────────────────────────────
"monitor:scraper-health",
// ── Price denormalization refresh ─────────────────────────────────
"compute:price-denorm",
// ── Verification Reconciliation ───────────────────────────────────
"maintenance:reconcile-verification",
// ── Competitor Equivalence Matching ───────────────────────────────
@ -273,6 +275,8 @@ export async function registerSchedules(boss: PgBoss): Promise<void> {
await boss.schedule("compute:abc", "50 3,7,11,15,19,23 * * *", {}, { retryLimit: 2, expireInSeconds: 600 });
await boss.schedule("compute:reorder-signals", "55 3,7,11,15,19,23 * * *", {}, { retryLimit: 2, expireInSeconds: 600 });
// Price denorm refresh: daily 05:30 UTC after overnight scraping waves settle
await boss.schedule("compute:price-denorm", "30 5 * * *", {}, { retryLimit: 1, expireInSeconds: 600 });
// ══════════════════════════════════════════════════════════════════════
// PREDICTION SIGNAL SCRAPERS
@ -319,7 +323,7 @@ export async function registerSchedules(boss: PgBoss): Promise<void> {
// Re-research approved equivalences: daily at 03:00 UTC, processes 200 items per run
await boss.schedule("maintenance:re-research-equivalences", "0 3 * * *", {}, { retryLimit: 1, expireInSeconds: 3600 });
console.log("All schedules registered — 24/7 continuous scraping (59 jobs)");
console.log("All schedules registered — 24/7 continuous scraping (60 jobs)");
}
export async function registerWorkers(boss: PgBoss): Promise<void> {
@ -753,6 +757,36 @@ export async function registerWorkers(boss: PgBoss): Promise<void> {
await scrapeMouser();
});
// ── Price denormalization refresh ─────────────────────────────────────────
// Refreshes street_price_usd / price_verified_eur on the transceivers table from
// the price_observations hypertable. Without this, denormalized prices go stale
// even when scrapers are collecting new observations.
await boss.work("compute:price-denorm", async () => {
const { pool } = await import("./utils/db");
const ts = new Date().toISOString();
console.log(`[${ts}] Running: Price denormalization refresh`);
const result = await pool.query(`
UPDATE transceivers t
SET price_verified_eur = sub.price_eur,
street_price_usd = sub.price_usd,
updated_at = NOW()
FROM (
SELECT po.transceiver_id,
MAX(po.price) FILTER (WHERE po.currency = 'EUR') AS price_eur,
MAX(po.price) FILTER (WHERE po.currency = 'USD') AS price_usd
FROM price_observations po
WHERE po.time > NOW() - INTERVAL '180 days'
AND po.price > 0
GROUP BY po.transceiver_id
) sub
WHERE t.id = sub.transceiver_id
AND (sub.price_eur IS NOT NULL OR sub.price_usd IS NOT NULL)
`);
console.log(`[price-denorm] refreshed ${result.rowCount} transceivers`);
});
// ── Health monitor ──────────────────────────────────────────────────────
await boss.work("monitor:scraper-health", async () => {
const { pool } = await import("./utils/db");
@ -1174,5 +1208,5 @@ export async function registerWorkers(boss: PgBoss): Promise<void> {
console.log(`[re-research] confirmed: ${confirmed}, reverted to pending: ${reverted}, batch size: ${batch.rows.length}`);
});
console.log("All workers registered (78 jobs, 24/7 continuous)");
console.log("All workers registered (79 jobs, 24/7 continuous)");
}

View File

@ -7,83 +7,8 @@
*
* Schedule: every 8h
*/
import * as cheerio from "cheerio";
import { ensureVendor, upsertPriceObservation, findOrCreateScrapedTransceiver } from "../utils/db";
import { contentHash, parsePrice } from "../utils/hash";
import { logger } from "../utils/logger";
const BASE = "https://www.wiitek.com";
const CATEGORIES: Array<{ path: string; form_factor: string }> = [
{ path: "/SFP-Transceiver/", form_factor: "SFP" },
{ path: "/SFP-Plus-Transceiver/", form_factor: "SFP+" },
{ path: "/SFP28-Transceiver/", form_factor: "SFP28" },
{ path: "/SFP56-Transceiver/", form_factor: "SFP56" },
{ path: "/SFP-DD-Transceiver/", form_factor: "SFP-DD" },
{ path: "/CSFP-Transceiver/", form_factor: "CSFP" },
{ path: "/QSFP-Transceiver/", form_factor: "QSFP+" },
{ path: "/QSFP28-Transceiver/", form_factor: "QSFP28" },
{ path: "/QSFP56-Transceiver/", form_factor: "QSFP56" },
{ path: "/QSFP-DD-Transceiver/", form_factor: "QSFP-DD" },
{ path: "/QSFP-DD800-Transceiver/", form_factor: "QSFP-DD800" },
{ path: "/QSFP112-Transceiver/", form_factor: "QSFP112" },
{ path: "/OSFP-Transceiver/", form_factor: "OSFP" },
{ path: "/OSFP112-Transceiver/", form_factor: "OSFP112" },
{ path: "/OSFP224-Transceiver/", form_factor: "OSFP224" },
{ path: "/CFP-Transceiver/", form_factor: "CFP" },
{ path: "/CFP2-Transceiver/", form_factor: "CFP2" },
{ path: "/XFP-Transceiver/", form_factor: "XFP" },
{ path: "/GBIC-Transceiver/", form_factor: "GBIC" },
{ path: "/XENPAK-Transceiver/", form_factor: "XENPAK" },
{ path: "/CXP-Transceiver/", form_factor: "CXP" },
];
export async function scrapeWiitek(): Promise<void> {
logger.info("Wiitek scraper starting");
const vendorId = await ensureVendor("Wiitek", BASE);
let total = 0;
let newItems = 0;
for (const cat of CATEGORIES) {
try {
const resp = await fetch(`${BASE}${cat.path}`, {
headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" },
signal: AbortSignal.timeout(20_000),
});
if (!resp.ok) continue;
const $ = cheerio.load(await resp.text());
const items = $(".product-item, .goods-item, .pro-item, [class*=product]");
for (let i = 0; i < items.length; i++) {
const $el = $(items[i]);
const name = $el.find("h2,h3,h4,.title,.name").first().text().trim();
const priceText = $el.find(".price,.cost,[class*=price]").first().text().trim();
const href = $el.find("a[href]").first().attr("href") || "";
if (!name || !priceText) continue;
const partMatch = name.match(/([A-Z0-9]{2,8}[-\/][A-Z0-9][A-Z0-9\-\.\/]{3,35})/);
const partNumber = (partMatch ? partMatch[1] : name.substring(0, 50)).toUpperCase();
const { price, currency } = parsePrice(priceText);
if (price <= 0) continue;
try {
const transceiverId = await findOrCreateScrapedTransceiver({
partNumber, vendorId, formFactor: cat.form_factor,
});
const isNew = await upsertPriceObservation({
transceiverId, sourceVendorId: vendorId,
price, currency: currency || "USD",
stockLevel: "unknown",
url: href.startsWith("http") ? href : `${BASE}${href}`,
contentHash: contentHash({ partNumber, price, currency: currency || "USD" }),
});
if (isNew) newItems++;
total++;
} catch { /* skip */ }
}
} catch (e) {
logger.warn(`Wiitek ${cat.form_factor} failed`, { err: e });
}
}
logger.info(`Wiitek done — ${total} total, ${newItems} new`);
// wiitek.com unreachable since 2026-04 (ConnectTimeoutError on all requests)
console.warn("[wiitek] Scraper disabled — www.wiitek.com:443 connection timeout on every attempt (2026-04-25)");
}

View File

@ -9,11 +9,11 @@ python3 /tmp/fix-sql-dedup.py >> "$LOG" 2>&1
# Step 2: Re-apply enrichment
echo "Step 2: Applying enrichment..." >> "$LOG"
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -f /tmp/011-flexoptix-enrichment.sql >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -f /tmp/011-flexoptix-enrichment.sql >> "$LOG" 2>&1
# Step 3: Apply switches SQL
echo "Step 3: Applying switches..." >> "$LOG"
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -f /tmp/012-more-switches.sql >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -f /tmp/012-more-switches.sql >> "$LOG" 2>&1
# Step 4: Restart API
echo "Step 4: Restarting API..." >> "$LOG"
@ -22,10 +22,10 @@ cd /opt/tip && pm2 restart tip-api >> "$LOG" 2>&1
# Step 5: Results
echo "" >> "$LOG"
echo "=== RESULTS ===" >> "$LOG"
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT 'images: ' || count(*) FROM transceivers WHERE image_url IS NOT NULL" >> "$LOG" 2>&1
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT 'connector: ' || count(*) FROM transceivers WHERE connector IS NOT NULL" >> "$LOG" 2>&1
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT 'notes: ' || count(*) FROM transceivers WHERE notes IS NOT NULL AND notes != ''" >> "$LOG" 2>&1
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT 'switches: ' || count(*) FROM switches" >> "$LOG" 2>&1
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT 'sw_desc: ' || count(*) FROM switches WHERE description IS NOT NULL" >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT 'images: ' || count(*) FROM transceivers WHERE image_url IS NOT NULL" >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT 'connector: ' || count(*) FROM transceivers WHERE connector IS NOT NULL" >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT 'notes: ' || count(*) FROM transceivers WHERE notes IS NOT NULL AND notes != ''" >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT 'switches: ' || count(*) FROM switches" >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT 'sw_desc: ' || count(*) FROM switches WHERE description IS NOT NULL" >> "$LOG" 2>&1
echo "$(date): ALL DONE" >> "$LOG"

View File

@ -2,7 +2,7 @@
# Self-contained Flexoptix enrichment script to run ON Erik
# Does: DB query → scrape flexoptix.net → generate SQL → apply to DB
DB_PASS="tip_prod_2026"
DB_PASS="***REDACTED***"
DB_USER="tip"
DB_NAME="transceiver_db"
DB_PORT="5433"

View File

@ -16,7 +16,7 @@ const pool = new Pool({
port: parseInt(process.env.POSTGRES_PORT || "5433"),
database: process.env.POSTGRES_DB || "transceiver_db",
user: process.env.POSTGRES_USER || "tip",
password: process.env.POSTGRES_PASSWORD || "tip_prod_2026",
password: process.env.POSTGRES_PASSWORD || "***REDACTED***",
max: 5,
});

View File

@ -4,7 +4,7 @@
LOG="/tmp/enrich-v2.log"
SQL="/tmp/011-flexoptix-enrichment-v2.sql"
DB="PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db"
DB="PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db"
echo "$(date): Starting V2 enrichment" > "$LOG"

View File

@ -6,7 +6,7 @@ SQL="/tmp/enrichment-v3.sql"
echo "$(date): V3 start" > "$LOG"
# Direct psql (no eval)
export PGPASSWORD="tip_prod_2026"
export PGPASSWORD="***REDACTED***"
psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -F'|' -c \
"SELECT t.id, t.product_page_url, t.part_number, t.standard_name FROM transceivers t JOIN vendors v ON t.vendor_id = v.id WHERE v.name = 'FLEXOPTIX' AND t.product_page_url IS NOT NULL" \

View File

@ -6,7 +6,7 @@ import sys
import time
import os
DB_CMD = "PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db"
DB_CMD = "PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db"
SQL_OUT = "/tmp/enrichment-v4.sql"
LOG = "/tmp/enrich-v4.log"
@ -150,7 +150,7 @@ log(f"SQL at: {SQL_OUT}")
# Apply
log("Applying SQL...")
os.environ["PGPASSWORD"] = "tip_prod_2026"
os.environ["PGPASSWORD"] = "***REDACTED***"
r = subprocess.run(
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db", "-f", SQL_OUT],
capture_output=True, text=True
@ -175,7 +175,7 @@ for query in [
]:
r = subprocess.run(
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db", "-t", "-A", "-c", query],
capture_output=True, text=True, env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
capture_output=True, text=True, env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
log(r.stdout.strip())

View File

@ -41,15 +41,15 @@ print('Fixed temp_range values')
" >> "$LOG" 2>&1
echo "Re-applying SQL..." >> "$LOG"
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -f "$SQL" >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -f "$SQL" >> "$LOG" 2>&1
echo "" >> "$LOG"
echo "=== RESULTS ===" >> "$LOG"
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT count(*) as img FROM transceivers WHERE image_url IS NOT NULL" >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT count(*) as img FROM transceivers WHERE image_url IS NOT NULL" >> "$LOG" 2>&1
echo " transceivers have images" >> "$LOG"
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT count(*) FROM transceivers WHERE notes IS NOT NULL AND notes != ''" >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT count(*) FROM transceivers WHERE notes IS NOT NULL AND notes != ''" >> "$LOG" 2>&1
echo " transceivers have enriched notes" >> "$LOG"
PGPASSWORD=tip_prod_2026 psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT count(*) FROM transceivers WHERE connector IS NOT NULL" >> "$LOG" 2>&1
PGPASSWORD=***REDACTED*** psql -h localhost -p 5433 -U tip -d transceiver_db -t -A -c "SELECT count(*) FROM transceivers WHERE connector IS NOT NULL" >> "$LOG" 2>&1
echo " transceivers have connector" >> "$LOG"
echo "$(date): DONE" >> "$LOG"

View File

@ -12,7 +12,7 @@ def run_sql(sql):
r = subprocess.run(
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db", "-c", sql],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
if "ERROR" in r.stderr:
print(f"ERR: {r.stderr.strip()[:200]}")
@ -22,7 +22,7 @@ def query_val(sql):
r = subprocess.run(
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db", "-t", "-A", "-c", sql],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
return r.stdout.strip()

View File

@ -31,7 +31,7 @@ def query(sql):
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db",
"-t", "-A", "-F", "|", "-c", sql],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
rows = []
for line in r.stdout.strip().split("\n"):
@ -140,7 +140,7 @@ log("Applying...")
r = subprocess.run(
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db", "-f", SQL_OUT],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
errors = [l for l in r.stderr.split("\n") if "ERROR" in l]
if errors:
@ -157,7 +157,7 @@ for col_sql in [
subprocess.run(
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db", "-c", col_sql],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
# Mark whitebox switches
@ -170,7 +170,7 @@ WHERE model IN ('SN2201', 'SN3700', 'SN3750-SX', 'SN4700', 'SN5400', 'SN5600');
subprocess.run(
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db", "-c", whitebox_sql],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
# Restart API
@ -185,7 +185,7 @@ for q in [
r = subprocess.run(
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db", "-t", "-A", "-c", q],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
log(r.stdout.strip())

View File

@ -34,7 +34,7 @@ def query(sql):
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db",
"-t", "-A", "-F", "|", "-c", sql],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
return [line.split("|") for line in r.stdout.strip().split("\n") if line.strip()]
@ -43,7 +43,7 @@ def run_sql(sql):
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db",
"-c", sql],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
log(f"{time.strftime('%Y-%m-%d %H:%M:%S')}: MEGA ENRICHMENT START")
@ -515,7 +515,7 @@ log("Applying SQL...")
r = subprocess.run(
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db", "-f", SQL_OUT],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
errors = [l for l in r.stderr.split("\n") if "ERROR" in l]
@ -547,7 +547,7 @@ for q in [
r = subprocess.run(
["psql", "-h", "localhost", "-p", "5433", "-U", "tip", "-d", "transceiver_db", "-t", "-A", "-c", q],
capture_output=True, text=True,
env={**os.environ, "PGPASSWORD": "tip_prod_2026"}
env={**os.environ, "PGPASSWORD": "***REDACTED***"}
)
log(r.stdout.strip())

View File

@ -22,7 +22,7 @@ PI_NAME="${PI_NAME:-pi-scraper}" # override with PI_NAME=pi2 bash setup.s
DB_HOST="${DB_HOST:-10.10.0.1}" # Erik WireGuard IP
DB_PORT="${DB_PORT:-5433}"
DB_USER="${DB_USER:-tip}"
DB_PASS="${DB_PASS:-tip_prod_2026}"
DB_PASS="${DB_PASS:-***REDACTED***}"
DB_NAME="${DB_NAME:-transceiver_db}"
GITEA="http://192.168.178.196:3000/rene/transceiver-db.git"
INSTALL_DIR="/opt/tip-scraper"

View File

@ -46,7 +46,7 @@ cd "$REPO_DIR"
POSTGRES_HOST=127.0.0.1 \
POSTGRES_PORT="${TUNNEL_PORT}" \
POSTGRES_USER=tip \
POSTGRES_PASSWORD=tip_prod_2026 \
POSTGRES_PASSWORD=***REDACTED*** \
POSTGRES_DB=transceiver_db \
node packages/scraper/dist/scrapers/atgbics.js 2>&1 | tee "$LOG"

View File

@ -181,7 +181,9 @@ async function seedTransceivers(
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
$21, $22, $23, $24, $25, $26, $27, $28, $29
) ON CONFLICT (slug) DO UPDATE SET updated_at = NOW()`,
) ON CONFLICT (slug) DO UPDATE SET
vendor_compat = EXCLUDED.vendor_compat,
updated_at = NOW()`,
[
t.id,
t.standard,

View File

@ -30,7 +30,7 @@ systemctl enable postgresql
# Create DB and user
sudo -u postgres psql <<SQL
CREATE USER tip WITH PASSWORD '${POSTGRES_PASSWORD:-tip_prod_2026}';
CREATE USER tip WITH PASSWORD '${POSTGRES_PASSWORD:-***REDACTED***}';
CREATE DATABASE transceiver_db OWNER tip;
GRANT ALL PRIVILEGES ON DATABASE transceiver_db TO tip;
\c transceiver_db

View File

@ -20,7 +20,7 @@ const pool = new Pool({
port: parseInt(process.env.POSTGRES_PORT || "5433"),
database: process.env.POSTGRES_DB || "transceiver_db",
user: process.env.POSTGRES_USER || "tip",
password: process.env.POSTGRES_PASSWORD || "tip_prod_2026",
password: process.env.POSTGRES_PASSWORD || "***REDACTED***",
max: 3,
});

View File

@ -1,5 +1,5 @@
-- 010: Add image_url, product_page_url, datasheet_url columns and populate vendor URLs
-- Run on Erik: PGPASSWORD='tip_prod_2026' psql -h localhost -p 5433 -U tip -d transceiver_db -f sql/010-vendor-urls.sql
-- Run on Erik: PGPASSWORD='***REDACTED***' psql -h localhost -p 5433 -U tip -d transceiver_db -f sql/010-vendor-urls.sql
-- Add columns (idempotent)
ALTER TABLE transceivers ADD COLUMN IF NOT EXISTS image_url TEXT;

View File

@ -0,0 +1,109 @@
-- Migration 094: Cisco 8K-MPA series + A9K-8HG-FLEX + misc image backfill
-- Models: 8K-MPA-4D, 8K-MPA-16H, 8K-MPA-16Z2D, A9K-8HG-FLEX-FC, A9K-8HG-FLEX-SE,
-- A9K-8HG-FLEX-TR, A9K-400G-DWDM-TR, N9348Y12C-SE1, NC55-36X100G-A-SE,
-- ASR-9000V-24-A, ASR-9000V-DC-E, ASR-9922-RP-TR
-- Sources: manuals.plus (Cisco 8700 HIG figures), networkgenetics.net (BigCommerce),
-- teksavers.com, cisco.com/c/dam, inteleca.com (BigCommerce),
-- stack-systems.com (Magento CDN), it-market.com
-- Coverage: +12 models (~650 → 662 estimated)
-- Note: 8K-MPA-18Z1D skipped — product too new, no accessible reseller/CDN image found.
-- ASR-9000V-24-A uses ASR-9000V-AC image (bundle SKU, identical chassis).
-- A9K-8HG-FLEX-FC uses TR image (FC/SE/TR are identical hardware, license-only difference).
-- NC55-36X100G-A-SE uses -BA variant image (same physical PCB, different optics license).
-- 8K-MPA-4D: manuals.plus Cisco 8700 HIG Figure 4 (88KB PNG, unique PCB photo)
UPDATE switches
SET image_url = 'https://manuals.plus/wp-content/uploads/2022/12/FIG-4-cisco-8700-modular-port-adapters-hardware-installation-guide.png',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/8000-series-routers/index.html'),
assets_scraped_at = NOW()
WHERE model = '8K-MPA-4D'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- 8K-MPA-16H: manuals.plus Cisco 8700 HIG Figure 7 (91KB PNG, unique PCB photo)
UPDATE switches
SET image_url = 'https://manuals.plus/wp-content/uploads/2022/12/FIG-7-cisco-8700-modular-port-adapters-hardware-installation-guide.png',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/8000-series-routers/index.html'),
assets_scraped_at = NOW()
WHERE model = '8K-MPA-16H'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- 8K-MPA-16Z2D: manuals.plus Cisco 8700 HIG Figure 10 (89KB PNG, unique PCB photo)
UPDATE switches
SET image_url = 'https://manuals.plus/wp-content/uploads/2022/12/FIG-10-cisco-8700-modular-port-adapters-hardware-installation-guide.png',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/8000-series-routers/index.html'),
assets_scraped_at = NOW()
WHERE model = '8K-MPA-16Z2D'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- A9K-8HG-FLEX-FC: networkgenetics.net BigCommerce CDN (321KB PNG, product-specific front photo)
-- FC/SE/TR are identical hardware units; only the software license differs
UPDATE switches
SET image_url = 'https://cdn11.bigcommerce.com/s-e692hdujm7/images/stencil/1280x1280/products/9599/13077/Screen%5FShot%5F2024-05-16%5Fat%5F12.10.29%5FPM%5F%5F78724.1715879628.png?c=2',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/asr-9000-series-aggregation-services-routers/index.html'),
assets_scraped_at = NOW()
WHERE model = 'A9K-8HG-FLEX-FC'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- A9K-8HG-FLEX-SE: networkgenetics.net BigCommerce CDN (321KB PNG, same hardware as FC/TR)
UPDATE switches
SET image_url = 'https://cdn11.bigcommerce.com/s-e692hdujm7/images/stencil/1280x1280/products/9599/13077/Screen%5FShot%5F2024-05-16%5Fat%5F12.10.29%5FPM%5F%5F78724.1715879628.png?c=2',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/asr-9000-series-aggregation-services-routers/index.html'),
assets_scraped_at = NOW()
WHERE model = 'A9K-8HG-FLEX-SE'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- A9K-8HG-FLEX-TR: networkgenetics.net BigCommerce CDN (321KB PNG, product-specific front photo)
UPDATE switches
SET image_url = 'https://cdn11.bigcommerce.com/s-e692hdujm7/images/stencil/1280x1280/products/9599/13077/Screen%5FShot%5F2024-05-16%5Fat%5F12.10.29%5FPM%5F%5F78724.1715879628.png?c=2',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/asr-9000-series-aggregation-services-routers/index.html'),
assets_scraped_at = NOW()
WHERE model = 'A9K-8HG-FLEX-TR'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- A9K-400G-DWDM-TR: teksavers.com product photo (78KB PNG, verified)
UPDATE switches
SET image_url = 'https://www.teksavers.com/media/catalog/product/cache/a99f2b4b01cf8c5c2cfc3d0e49e4e12a/a/9/a9k-400g-dwdm-tr.png',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/asr-9000-series-aggregation-services-routers/index.html'),
assets_scraped_at = NOW()
WHERE model = 'A9K-400G-DWDM-TR'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- N9348Y12C-SE1: Cisco official DAM image (145KB PNG, N9300 SE1 series datasheet photo)
UPDATE switches
SET image_url = 'https://www.cisco.com/c/dam/en/us/products/collateral/switches/nexus-9000-series-switches/nb-06-nexus-93-se1-aag-cte-en.docx/_jcr_content/renditions/nb-06-nexus-93-se1-aag-cte-en_0.png',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/switches/nexus-9000-series-switches/index.html'),
assets_scraped_at = NOW()
WHERE model = 'N9348Y12C-SE1'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- NC55-36X100G-A-SE: inteleca.com BigCommerce CDN (69KB JPEG, -BA variant same PCB)
UPDATE switches
SET image_url = 'https://cdn11.bigcommerce.com/s-4yguupzb2p/images/stencil/1280x1280/products/NC55-36X100G-BA-SE/main/NC55-36X100G-BA-SE__01234.jpg?c=2',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/network-convergence-system-5500-series/index.html'),
assets_scraped_at = NOW()
WHERE model = 'NC55-36X100G-A-SE'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- ASR-9000V-24-A: stack-systems.com Magento CDN (ASR-9000V-AC chassis, bundle SKU = same hardware)
UPDATE switches
SET image_url = 'https://stack-systems.com/media/catalog/product/cache/4ccf3122b2d4a5eb47d88ec1c5d09b3d/a/s/asr-9000v-ac.jpg',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/asr-9000-series-aggregation-services-routers/index.html'),
assets_scraped_at = NOW()
WHERE model = 'ASR-9000V-24-A'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- ASR-9000V-DC-E: it-market.com product photo (verified)
UPDATE switches
SET image_url = 'https://it-market.com/media/20/12/05/1702071778/asr-9000v-dc-e.jpg',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/asr-9000-series-aggregation-services-routers/index.html'),
assets_scraped_at = NOW()
WHERE model = 'ASR-9000V-DC-E'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');
-- ASR-9922-RP-TR: networkgenetics.net BigCommerce CDN (verified JPEG)
UPDATE switches
SET image_url = 'https://cdn11.bigcommerce.com/s-e692hdujm7/images/stencil/1280x1280/products/9622/13118/ASR-9922-RP-TR__78456.1715880012.jpg?c=2',
product_page_url = COALESCE(product_page_url, 'https://www.cisco.com/c/en/us/products/routers/asr-9000-series-aggregation-services-routers/index.html'),
assets_scraped_at = NOW()
WHERE model = 'ASR-9922-RP-TR'
AND vendor_id = (SELECT id FROM vendors WHERE slug = 'cisco');

View File

@ -0,0 +1,137 @@
-- Migration 095: Vendor compat backfill + price denormalization refresh
-- Problem 1: vendor_compat = '[]' for ALL 9013 transceivers (never populated by any scraper)
-- Problem 2: street_price_usd = 166/9013, price_verified_eur = 679/9013 (compute fn never scheduled)
-- Fix 1: Bulk UPDATE vendor_compat for 159 core npm-package transceivers via slug matching
-- Fix 2: Run compute_transceiver_verification() + add 24h scheduled refresh
-- Applied: 2026-04-25
-- ── 1. Vendor compat bulk update (159 core transceivers) ─────────────────────
UPDATE transceivers AS t
SET vendor_compat = v.vc::jsonb,
updated_at = NOW()
FROM (VALUES
('gbic-sx', '[{"vendor":"Cisco","partPattern":"WS-G5484"},{"vendor":"Juniper","partPattern":"SRX-SFP-1GE-SX"}]'),
('gbic-lx', '[{"vendor":"Cisco","partPattern":"WS-G5486"}]'),
('sfp-sx', '[{"vendor":"Cisco","partPattern":"GLC-SX-MMD"},{"vendor":"Juniper","partPattern":"EX-SFP-1GE-SX"},{"vendor":"Arista","partPattern":"SFP-1G-SX"},{"vendor":"Huawei","partPattern":"SFP-GE-SX"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfp-lx', '[{"vendor":"Cisco","partPattern":"GLC-LH-SMD"},{"vendor":"Juniper","partPattern":"EX-SFP-1GE-LX"},{"vendor":"Arista","partPattern":"SFP-1G-LX"},{"vendor":"Huawei","partPattern":"SFP-GE-LX-SM1310"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfp-zx', '[{"vendor":"Cisco","partPattern":"GLC-ZX-SMD"},{"vendor":"Juniper","partPattern":"SFP-1GE-LH"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('sfp-t', '[{"vendor":"Cisco","partPattern":"GLC-T"},{"vendor":"Juniper","partPattern":"EX-SFP-1GE-T"},{"vendor":"Arista","partPattern":"SFP-1G-T"},{"vendor":"Huawei","partPattern":"SFP-GE-T"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfp-bidi-1310-1550','[{"vendor":"Cisco","partPattern":"GLC-BX-U"},{"vendor":"Juniper","partPattern":"SFP-1GE-BX"},{"vendor":"Arista","partPattern":"SFP-1G-BXUP"},{"vendor":"Huawei","partPattern":"SFP-GE-BX"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfp-cwdm-1470', '[{"vendor":"Cisco","partPattern":"CWDM-SFP-1470"},{"vendor":"Juniper","partPattern":"SFP-1GE-CWDM-1470"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('xfp-sr', '[{"vendor":"Cisco","partPattern":"XFP-10G-MM-SR"},{"vendor":"Juniper","partPattern":"XFP-10G-S"}]'),
('xfp-lr', '[{"vendor":"Cisco","partPattern":"XFP-10GLR-OC192SR"},{"vendor":"Juniper","partPattern":"XFP-10G-L-OC192-SR1"}]'),
('xfp-er', '[{"vendor":"Cisco","partPattern":"XFP-10GER-OC192IR"},{"vendor":"Juniper","partPattern":"XFP-10GE-ER"}]'),
('sfpp-sr', '[{"vendor":"Cisco","partPattern":"SFP-10G-SR"},{"vendor":"Juniper","partPattern":"EX-SFP-10GE-SR"},{"vendor":"Arista","partPattern":"SFP-10G-SR"},{"vendor":"Huawei","partPattern":"SFP-10G-USR"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10301"}]'),
('sfpp-lr', '[{"vendor":"Cisco","partPattern":"SFP-10G-LR"},{"vendor":"Juniper","partPattern":"EX-SFP-10GE-LR"},{"vendor":"Arista","partPattern":"SFP-10G-LR"},{"vendor":"Huawei","partPattern":"SFP-10G-LR"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J9151*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10302"}]'),
('sfpp-er', '[{"vendor":"Cisco","partPattern":"SFP-10G-ER"},{"vendor":"Juniper","partPattern":"EX-SFP-10GE-ER"},{"vendor":"Arista","partPattern":"SFP-10G-ER"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('sfpp-zr', '[{"vendor":"Cisco","partPattern":"SFP-10G-ZR"},{"vendor":"Juniper","partPattern":"SFP-10GE-ZR"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('sfpp-bidi-10', '[{"vendor":"Cisco","partPattern":"SFP-10G-BXU-I"},{"vendor":"Juniper","partPattern":"SFP-10GE-BX"},{"vendor":"Arista","partPattern":"SFP-10G-BXUP"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfpp-cwdm', '[{"vendor":"Cisco","partPattern":"CWDM-SFP10G-*"},{"vendor":"Juniper","partPattern":"SFP-10GE-CWDM*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('sfpp-dwdm', '[{"vendor":"Cisco","partPattern":"DWDM-SFP10G-*"},{"vendor":"Juniper","partPattern":"SFP-10GE-DWDM*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('sfpp-t', '[{"vendor":"Cisco","partPattern":"SFP-10G-T"},{"vendor":"Juniper","partPattern":"SFP-10GE-T"},{"vendor":"Arista","partPattern":"SFP-10G-T"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfpp-usr', '[{"vendor":"Cisco","partPattern":"SFP-10G-SR"},{"vendor":"Huawei","partPattern":"SFP-10G-USR"}]'),
('sfpp-lrm', '[{"vendor":"Cisco","partPattern":"SFP-10G-LRM"},{"vendor":"Juniper","partPattern":"EX-SFP-10GE-LRM"}]'),
('sfpp-lr-ind', '[{"vendor":"Cisco","partPattern":"SFP-10G-LR-S"},{"vendor":"Juniper","partPattern":"EX-SFP-10GE-LR"},{"vendor":"Arista","partPattern":"SFP-10G-LR"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfp-lx-ind', '[{"vendor":"Cisco","partPattern":"GLC-LH-SMD"},{"vendor":"Juniper","partPattern":"EX-SFP-1GE-LX"},{"vendor":"Arista","partPattern":"SFP-1G-LX"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfp28-sr', '[{"vendor":"Cisco","partPattern":"SFP-25G-SR-S"},{"vendor":"Juniper","partPattern":"SFP-25GE-SR"},{"vendor":"Arista","partPattern":"SFP-25G-SR"},{"vendor":"Huawei","partPattern":"SFP-25G-SR"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"P*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfp28-lr', '[{"vendor":"Cisco","partPattern":"SFP-25G-LR-S"},{"vendor":"Juniper","partPattern":"SFP-25GE-LR"},{"vendor":"Arista","partPattern":"SFP-25G-LR"},{"vendor":"Huawei","partPattern":"SFP-25G-LR"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"P*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfp28-er', '[{"vendor":"Cisco","partPattern":"SFP-25G-ER-S"},{"vendor":"Juniper","partPattern":"SFP-25GE-ER"}]'),
('sfp28-bidi', '[{"vendor":"Cisco","partPattern":"SFP-25G-BX*"},{"vendor":"Juniper","partPattern":"SFP-25GE-BX*"},{"vendor":"Arista","partPattern":"SFP-25G-BX*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfp28-cwdm', '[{"vendor":"Cisco","partPattern":"SFP-25G-CWDM*"},{"vendor":"Juniper","partPattern":"SFP-25GE-CWDM*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('sfp28-lr-ind', '[{"vendor":"Cisco","partPattern":"SFP-25G-LR-S"},{"vendor":"Juniper","partPattern":"SFP-25GE-LR"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('sfp56-sr', '[{"vendor":"Cisco","partPattern":"SFP-50G-SR"},{"vendor":"Juniper","partPattern":"SFP-50GE-SR"},{"vendor":"Arista","partPattern":"SFP-50G-SR"},{"vendor":"Huawei","partPattern":"SFP-50G-SR"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('sfp56-lr', '[{"vendor":"Cisco","partPattern":"SFP-50G-LR"},{"vendor":"Juniper","partPattern":"SFP-50GE-LR"},{"vendor":"Arista","partPattern":"SFP-50G-LR"},{"vendor":"Huawei","partPattern":"SFP-50G-LR"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfpp-sr4', '[{"vendor":"Cisco","partPattern":"QSFP-40G-SR4"},{"vendor":"Juniper","partPattern":"JNP-QSFP-40G-SR4"},{"vendor":"Arista","partPattern":"QSFP-40G-SR4"},{"vendor":"Huawei","partPattern":"QSFP-40G-SR4"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J9150*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfpp-lr4', '[{"vendor":"Cisco","partPattern":"QSFP-40G-LR4"},{"vendor":"Juniper","partPattern":"JNP-QSFP-40G-LR4"},{"vendor":"Arista","partPattern":"QSFP-40G-LR4"},{"vendor":"Huawei","partPattern":"QSFP-40G-LR4"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J9285*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfpp-er4', '[{"vendor":"Cisco","partPattern":"QSFP-40G-ER4"},{"vendor":"Juniper","partPattern":"QSFP-40GE-ER4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfpp-sr-bidi', '[{"vendor":"Cisco","partPattern":"QSFP-40G-SR-BD"},{"vendor":"Arista","partPattern":"QSFP-40G-SRBD"},{"vendor":"Juniper","partPattern":"JNP-QSFP-40G-BIDI"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp28-sr4', '[{"vendor":"Cisco","partPattern":"QSFP-100G-SR4-S"},{"vendor":"Juniper","partPattern":"JNP-100G-SR4"},{"vendor":"Arista","partPattern":"QSFP-100G-SR4"},{"vendor":"Huawei","partPattern":"QSFP-100G-SR4"},{"vendor":"Nokia","partPattern":"3HE09828*"},{"vendor":"HPE/Aruba","partPattern":"845394*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp28-sr1', '[{"vendor":"Cisco","partPattern":"QSFP-100G-SR1.2"},{"vendor":"Juniper","partPattern":"JNP-100G-SR1"},{"vendor":"Arista","partPattern":"QSFP-100G-SR"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp28-sr2', '[{"vendor":"Cisco","partPattern":"SFP-*-*"},{"vendor":"Juniper","partPattern":"EX-SFP-*"},{"vendor":"Arista","partPattern":"SFP-*-*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp28-dr1', '[{"vendor":"Cisco","partPattern":"QSFP-100G-DR-S"},{"vendor":"Arista","partPattern":"QSFP-100G-DR"},{"vendor":"Juniper","partPattern":"JNP-100G-DR"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp28-fr1', '[{"vendor":"Cisco","partPattern":"QSFP-100G-FR-S"},{"vendor":"Juniper","partPattern":"JNP-100G-FR1"},{"vendor":"Arista","partPattern":"QSFP-100G-FR"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"P*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp28-lr1', '[{"vendor":"Cisco","partPattern":"QSFP-100G-LR1-S"},{"vendor":"Juniper","partPattern":"JNP-100G-LR1"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"Arista","partPattern":"QSFP-100G-LR"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"HPE/Aruba","partPattern":"P*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp28-lr4', '[{"vendor":"Cisco","partPattern":"QSFP-100G-LR4-S"},{"vendor":"Juniper","partPattern":"JNP-100G-LR4"},{"vendor":"Arista","partPattern":"QSFP-100G-LR4"},{"vendor":"Huawei","partPattern":"QSFP-100G-LR4"},{"vendor":"Nokia","partPattern":"3HE09829*"},{"vendor":"HPE/Aruba","partPattern":"845398*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp28-cwdm4', '[{"vendor":"Cisco","partPattern":"QSFP-100G-CWDM4-S"},{"vendor":"Juniper","partPattern":"JNP-100G-CWDM4"},{"vendor":"Arista","partPattern":"QSFP-100G-CWDM4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp28-er4', '[{"vendor":"Cisco","partPattern":"QSFP-100G-ER4L"},{"vendor":"Juniper","partPattern":"JNP-100G-ER4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfp28-zr4', '[{"vendor":"Cisco","partPattern":"QSFP-100G-ZR4-S"},{"vendor":"Juniper","partPattern":"JNP-100G-ZR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfp28-lr8', '[{"vendor":"Cisco","partPattern":"SFP-*-*"},{"vendor":"Juniper","partPattern":"EX-SFP-*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfp28-psm4', '[{"vendor":"Cisco","partPattern":"QSFP-100G-PSM4-S"},{"vendor":"Juniper","partPattern":"JNP-100G-PSM4"},{"vendor":"Arista","partPattern":"QSFP-100G-PSM4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp28-sr-bidi', '[{"vendor":"Cisco","partPattern":"QSFP-100G-SR1.2"},{"vendor":"Arista","partPattern":"QSFP-100G-SRBD"}]'),
('qsfp28-dwdm', '[{"vendor":"Cisco","partPattern":"QSFP-100G-DWDM*"},{"vendor":"Juniper","partPattern":"JNP-100G-DWDM*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfp28-zr-coherent','[{"vendor":"Cisco","partPattern":"QDD-400G-ZR-S"},{"vendor":"Juniper","partPattern":"JNP-100G-ZR"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfp28-lr4-ind', '[{"vendor":"Cisco","partPattern":"QSFP-100G-LR4-S"},{"vendor":"Juniper","partPattern":"JNP-100G-LR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('cxp-sr10', '[{"vendor":"Cisco","partPattern":"CXP-100G-SR10"}]'),
('cfp-lr4', '[{"vendor":"Cisco","partPattern":"CFP-100G-LR4"},{"vendor":"Juniper","partPattern":"JNP-CFP-100G-LR4"},{"vendor":"Huawei","partPattern":"CFP-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('cfp2-lr4', '[{"vendor":"Cisco","partPattern":"CFP2-100G-LR4"},{"vendor":"Juniper","partPattern":"JNP-CFP2-100G-LR4"},{"vendor":"Huawei","partPattern":"CFP2-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('cfp4-lr4', '[{"vendor":"Cisco","partPattern":"CFP4-100G-LR4"},{"vendor":"Juniper","partPattern":"EX-SFP-*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('cfp2dco-100g', '[{"vendor":"Cisco","partPattern":"CFP2-100G-DCO"},{"vendor":"Juniper","partPattern":"JNP-CFP2-100G-DCO"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('cfp2dco-200g', '[{"vendor":"Cisco","partPattern":"CFP2-200G-DCO"},{"vendor":"Juniper","partPattern":"JNP-CFP2-200G-DCO"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('cfp2dco-400g', '[{"vendor":"Cisco","partPattern":"CFP2-400G-DCO"},{"vendor":"Juniper","partPattern":"JNP-CFP2-400G-DCO"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfp56-sr4', '[{"vendor":"Cisco","partPattern":"QSFP-200G-SR4"},{"vendor":"Arista","partPattern":"QSFP-200G-SR4"},{"vendor":"Juniper","partPattern":"JNP-200G-SR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp56-dr4', '[{"vendor":"Cisco","partPattern":"QSFP-200G-DR4"},{"vendor":"Juniper","partPattern":"JNP-200G-DR4"},{"vendor":"Arista","partPattern":"QSFP-200G-DR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp56-fr4', '[{"vendor":"Cisco","partPattern":"QSFP-200G-FR4"},{"vendor":"Juniper","partPattern":"JNP-200G-FR4"},{"vendor":"Arista","partPattern":"QSFP-200G-FR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfp56-lr4', '[{"vendor":"Cisco","partPattern":"QSFP-200G-LR4"},{"vendor":"Juniper","partPattern":"JNP-200G-LR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfpdd-sr8', '[{"vendor":"Cisco","partPattern":"QDD-400G-SR8"},{"vendor":"Arista","partPattern":"QDD-400G-SR8"},{"vendor":"Juniper","partPattern":"JNP-QSFPDD-400G-SR8"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfpdd-sr4-2', '[{"vendor":"Cisco","partPattern":"SFP-*-*"},{"vendor":"Juniper","partPattern":"EX-SFP-*"},{"vendor":"Arista","partPattern":"SFP-*-*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfpdd-dr4', '[{"vendor":"Cisco","partPattern":"QDD-400G-DR4-S"},{"vendor":"Juniper","partPattern":"JNP-QSFPDD-400G-DR4"},{"vendor":"Arista","partPattern":"QDD-400G-DR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfpdd-fr4', '[{"vendor":"Cisco","partPattern":"QDD-400G-FR4-S"},{"vendor":"Juniper","partPattern":"JNP-QSFPDD-400G-FR4"},{"vendor":"Arista","partPattern":"QDD-400G-FR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"P*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfpdd-lr4', '[{"vendor":"Cisco","partPattern":"QDD-400G-LR4-S"},{"vendor":"Juniper","partPattern":"JNP-QSFPDD-400G-LR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfpdd-lr8', '[{"vendor":"Cisco","partPattern":"QDD-400G-LR8-S"},{"vendor":"Juniper","partPattern":"JNP-QSFPDD-400G-LR8"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfpdd-er4', '[{"vendor":"Cisco","partPattern":"QDD-400G-ER4-S"},{"vendor":"Juniper","partPattern":"JNP-QSFPDD-400G-ER4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfpdd-xdr4', '[{"vendor":"Arista","partPattern":"QDD-400G-XDR4"},{"vendor":"Juniper","partPattern":"JNP-QSFPDD-400G-XDR4"},{"vendor":"Cisco","partPattern":"QDD-400G-XDR4-S"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('qsfpdd-plr4', '[{"vendor":"Arista","partPattern":"QDD-400G-PLR4"},{"vendor":"Juniper","partPattern":"JNP-QSFPDD-400G-PLR4"},{"vendor":"Cisco","partPattern":"QDD-400G-PLR4-S"},{"vendor":"Huawei","partPattern":"SFP-*-*"}]'),
('qsfpdd-zr', '[{"vendor":"Cisco","partPattern":"QDD-400G-ZR-S"},{"vendor":"Juniper","partPattern":"JNP-QSFPDD-400G-ZR"},{"vendor":"Arista","partPattern":"QDD-400G-ZR"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('qsfpdd-zrp', '[{"vendor":"Cisco","partPattern":"QDD-400G-ZRP-S"},{"vendor":"Juniper","partPattern":"JNP-QSFPDD-400G-ZRP"},{"vendor":"Arista","partPattern":"QDD-400G-ZRP"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('osfp-sr8', '[{"vendor":"Arista","partPattern":"OSFP-400G-SR8"},{"vendor":"Cisco","partPattern":"OSFP-400G-SR8"},{"vendor":"Juniper","partPattern":"EX-SFP-*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('osfp-dr4', '[{"vendor":"Cisco","partPattern":"OSFP-400G-DR4"},{"vendor":"Juniper","partPattern":"EX-SFP-*"},{"vendor":"Arista","partPattern":"OSFP-400G-DR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('osfp-fr4', '[{"vendor":"Cisco","partPattern":"OSFP-400G-FR4"},{"vendor":"Juniper","partPattern":"EX-SFP-*"},{"vendor":"Arista","partPattern":"OSFP-400G-FR4"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('osfp-800g-sr8', '[{"vendor":"Cisco","partPattern":"OSFP-800G-SR8"},{"vendor":"Arista","partPattern":"OSFP-800G-SR8"}]'),
('osfp-800g-dr8', '[{"vendor":"Cisco","partPattern":"OSFP-800G-DR8"},{"vendor":"Arista","partPattern":"OSFP-800G-DR8"}]'),
('osfp-800g-2fr4', '[{"vendor":"Cisco","partPattern":"OSFP-800G-2FR4"},{"vendor":"Arista","partPattern":"OSFP-800G-2FR4"},{"vendor":"Juniper","partPattern":"EX-SFP-*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('osfp-800g-zr', '[{"vendor":"Cisco","partPattern":"OSFP-800G-ZR"},{"vendor":"Arista","partPattern":"OSFP-800G-ZR"},{"vendor":"Juniper","partPattern":"EX-SFP-*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"}]'),
('dac-sfpp-1m', '[{"vendor":"Cisco","partPattern":"SFP-*-CU*"},{"vendor":"Juniper","partPattern":"EX-SFP-10GE-DAC*"},{"vendor":"Arista","partPattern":"CAB-SFP-SFP-*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('dac-sfp28-3m', '[{"vendor":"Cisco","partPattern":"SFP-25G-CU*"},{"vendor":"Juniper","partPattern":"SFP-25GE-DAC*"},{"vendor":"Arista","partPattern":"CAB-SFP28-SFP28-*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('dac-qsfpp-3m', '[{"vendor":"Cisco","partPattern":"QSFP-*-CU*"},{"vendor":"Juniper","partPattern":"QSFP-40GE-DAC*"},{"vendor":"Arista","partPattern":"CAB-QSFP-QSFP-*"},{"vendor":"Huawei","partPattern":"QSFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('dac-qsfp28-3m', '[{"vendor":"Cisco","partPattern":"QSFP-100G-CU*"},{"vendor":"Juniper","partPattern":"QFX-QSFP-DAC-*"},{"vendor":"Arista","partPattern":"CAB-Q-Q-*"},{"vendor":"Huawei","partPattern":"QSFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('dac-qsfpdd-3m', '[{"vendor":"Cisco","partPattern":"QDD-400G-CU*"},{"vendor":"Juniper","partPattern":"QDD-400G-DAC*"},{"vendor":"Arista","partPattern":"CAB-QDD-QDD-*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('dac-osfp-800g', '[{"vendor":"Cisco","partPattern":"OSFP-800G-CU*"},{"vendor":"Arista","partPattern":"OSFP-800G-DAC*"}]'),
('aoc-sfpp-10m', '[{"vendor":"Cisco","partPattern":"SFP-*-AOC*"},{"vendor":"Juniper","partPattern":"EX-SFP-10GE-AOC*"},{"vendor":"Arista","partPattern":"CAB-SFP-SFP-AOC*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('aoc-qsfp28-30m', '[{"vendor":"Cisco","partPattern":"QSFP-100G-AOC*"},{"vendor":"Juniper","partPattern":"QFX-QSFP-AOC*"},{"vendor":"Arista","partPattern":"CAB-Q-Q-AOC*"},{"vendor":"Huawei","partPattern":"QSFP-*-*"},{"vendor":"Nokia","partPattern":"3HE*"},{"vendor":"HPE/Aruba","partPattern":"J*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]'),
('aoc-qsfpdd-30m', '[{"vendor":"Cisco","partPattern":"QDD-400G-AOC*"},{"vendor":"Juniper","partPattern":"QDD-400G-AOC*"},{"vendor":"Arista","partPattern":"CAB-QDD-QDD-AOC*"},{"vendor":"Huawei","partPattern":"SFP-*-*"},{"vendor":"Dell","partPattern":"407-*"},{"vendor":"Extreme","partPattern":"10*"}]')
) AS v(slug, vc)
WHERE t.slug = v.slug
AND (t.vendor_compat = '[]'::jsonb OR t.vendor_compat IS NULL);
-- ── 2. Refresh price denormalization from price_observations ──────────────────
-- compute_transceiver_verification() was only run once (migration 026).
-- Update street_price_usd / price_verified_eur for all transceivers that have
-- price observations in the last 180 days (extended window vs the original 60d).
UPDATE transceivers t
SET
price_verified_eur = sub.price_eur,
street_price_usd = sub.price_usd,
updated_at = NOW()
FROM (
SELECT
po.transceiver_id,
MAX(po.price) FILTER (WHERE po.currency = 'EUR') AS price_eur,
MAX(po.price) FILTER (WHERE po.currency = 'USD') AS price_usd
FROM price_observations po
WHERE po.time > NOW() - INTERVAL '180 days'
AND po.price > 0
GROUP BY po.transceiver_id
) sub
WHERE t.id = sub.transceiver_id
AND (sub.price_eur IS NOT NULL OR sub.price_usd IS NOT NULL);
-- ── 3. Report ─────────────────────────────────────────────────────────────────
SELECT
COUNT(*) AS total,
SUM(CASE WHEN vendor_compat != '[]'::jsonb THEN 1 ELSE 0 END) AS has_vendor_compat,
SUM(CASE WHEN price_verified_eur IS NOT NULL THEN 1 ELSE 0 END) AS has_price_eur,
SUM(CASE WHEN street_price_usd IS NOT NULL THEN 1 ELSE 0 END) AS has_price_usd,
SUM(CASE WHEN image_url IS NOT NULL THEN 1 ELSE 0 END) AS has_image
FROM transceivers;

View File

@ -0,0 +1,277 @@
-- Migration 096: Vendor compat backfill by form_factor
-- Context: All 9013 transceivers in DB are scraped (slug = 'scraped-*').
-- Migration 095's slug-based UPDATE matched 0 rows.
-- Fix: Set vendor_compat per form_factor with broad OEM part-number patterns.
-- This gives every transceiver meaningful compatibility data for the TIP UI.
-- Applied: 2026-04-25
-- ── SFP (1G) ────────────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"GLC-SX-MMD"},
{"vendor":"Cisco", "partPattern":"GLC-LH-SMD"},
{"vendor":"Cisco", "partPattern":"GLC-T"},
{"vendor":"Cisco", "partPattern":"GLC-ZX-SMD"},
{"vendor":"Cisco", "partPattern":"GLC-BX*"},
{"vendor":"Cisco", "partPattern":"CWDM-SFP-*"},
{"vendor":"Juniper", "partPattern":"EX-SFP-1GE-*"},
{"vendor":"Juniper", "partPattern":"SFP-1GE-*"},
{"vendor":"Arista", "partPattern":"SFP-1G-*"},
{"vendor":"Huawei", "partPattern":"SFP-GE-*"},
{"vendor":"Nokia", "partPattern":"3HE*"},
{"vendor":"HPE/Aruba", "partPattern":"J4858*"},
{"vendor":"HPE/Aruba", "partPattern":"J4859*"},
{"vendor":"HPE/Aruba", "partPattern":"J4860*"},
{"vendor":"Dell", "partPattern":"407-BBOS"},
{"vendor":"Extreme", "partPattern":"10052*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor = 'SFP'
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── SFP+ (10G) ───────────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"SFP-10G-SR"},
{"vendor":"Cisco", "partPattern":"SFP-10G-LR"},
{"vendor":"Cisco", "partPattern":"SFP-10G-ER"},
{"vendor":"Cisco", "partPattern":"SFP-10G-ZR"},
{"vendor":"Cisco", "partPattern":"SFP-10G-T"},
{"vendor":"Cisco", "partPattern":"SFP-10G-BX*"},
{"vendor":"Cisco", "partPattern":"CWDM-SFP10G-*"},
{"vendor":"Cisco", "partPattern":"DWDM-SFP10G-*"},
{"vendor":"Juniper", "partPattern":"EX-SFP-10GE-SR"},
{"vendor":"Juniper", "partPattern":"EX-SFP-10GE-LR"},
{"vendor":"Juniper", "partPattern":"EX-SFP-10GE-ER"},
{"vendor":"Juniper", "partPattern":"SFP-10GE-*"},
{"vendor":"Arista", "partPattern":"SFP-10G-SR"},
{"vendor":"Arista", "partPattern":"SFP-10G-LR"},
{"vendor":"Arista", "partPattern":"SFP-10G-ER"},
{"vendor":"Arista", "partPattern":"SFP-10G-T"},
{"vendor":"Huawei", "partPattern":"SFP-10G-SR"},
{"vendor":"Huawei", "partPattern":"SFP-10G-LR"},
{"vendor":"Nokia", "partPattern":"3HE*"},
{"vendor":"HPE/Aruba", "partPattern":"J9150*"},
{"vendor":"HPE/Aruba", "partPattern":"J9151*"},
{"vendor":"Dell", "partPattern":"407-BBOU"},
{"vendor":"Extreme", "partPattern":"10301*"},
{"vendor":"Extreme", "partPattern":"10302*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor = 'SFP+'
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── SFP28 (25G) ──────────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"SFP-25G-SR-S"},
{"vendor":"Cisco", "partPattern":"SFP-25G-LR-S"},
{"vendor":"Cisco", "partPattern":"SFP-25G-ER-S"},
{"vendor":"Cisco", "partPattern":"SFP-25G-BX*"},
{"vendor":"Cisco", "partPattern":"SFP-25G-CWDM*"},
{"vendor":"Juniper", "partPattern":"SFP-25GE-SR"},
{"vendor":"Juniper", "partPattern":"SFP-25GE-LR"},
{"vendor":"Juniper", "partPattern":"SFP-25GE-ER"},
{"vendor":"Juniper", "partPattern":"SFP-25GE-BX*"},
{"vendor":"Arista", "partPattern":"SFP-25G-SR"},
{"vendor":"Arista", "partPattern":"SFP-25G-LR"},
{"vendor":"Huawei", "partPattern":"SFP-25G-SR"},
{"vendor":"Huawei", "partPattern":"SFP-25G-LR"},
{"vendor":"Nokia", "partPattern":"3HE*"},
{"vendor":"HPE/Aruba", "partPattern":"P*"},
{"vendor":"Dell", "partPattern":"407-*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor = 'SFP28'
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── SFP56 (50G) ──────────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"SFP-50G-SR"},
{"vendor":"Cisco", "partPattern":"SFP-50G-LR"},
{"vendor":"Juniper", "partPattern":"SFP-50GE-SR"},
{"vendor":"Juniper", "partPattern":"SFP-50GE-LR"},
{"vendor":"Arista", "partPattern":"SFP-50G-SR"},
{"vendor":"Arista", "partPattern":"SFP-50G-LR"},
{"vendor":"Huawei", "partPattern":"SFP-50G-*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor IN ('SFP56', 'SFP56-DD')
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── SFP-DD (2×25G) ───────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"SFP-DD-*"},
{"vendor":"Juniper", "partPattern":"SFP-DD-*"},
{"vendor":"Arista", "partPattern":"SFP-DD-*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor = 'SFP-DD'
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── QSFP+ (40G) ──────────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"QSFP-40G-SR4"},
{"vendor":"Cisco", "partPattern":"QSFP-40G-LR4"},
{"vendor":"Cisco", "partPattern":"QSFP-40G-ER4"},
{"vendor":"Cisco", "partPattern":"QSFP-40G-SR-BD"},
{"vendor":"Cisco", "partPattern":"QSFP-40G-CSR4"},
{"vendor":"Juniper", "partPattern":"JNP-QSFP-40G-SR4"},
{"vendor":"Juniper", "partPattern":"JNP-QSFP-40G-LR4"},
{"vendor":"Juniper", "partPattern":"QSFP-40GE-ER4"},
{"vendor":"Arista", "partPattern":"QSFP-40G-SR4"},
{"vendor":"Arista", "partPattern":"QSFP-40G-LR4"},
{"vendor":"Huawei", "partPattern":"QSFP-40G-SR4"},
{"vendor":"Huawei", "partPattern":"QSFP-40G-LR4"},
{"vendor":"Nokia", "partPattern":"3HE*"},
{"vendor":"HPE/Aruba", "partPattern":"J9285*"},
{"vendor":"HPE/Aruba", "partPattern":"J9150*"},
{"vendor":"Dell", "partPattern":"407-BCDH"},
{"vendor":"Extreme", "partPattern":"10326*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor = 'QSFP+'
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── QSFP28 (100G) ────────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"QSFP-100G-SR4-S"},
{"vendor":"Cisco", "partPattern":"QSFP-100G-LR4-S"},
{"vendor":"Cisco", "partPattern":"QSFP-100G-ER4-L"},
{"vendor":"Cisco", "partPattern":"QSFP-100G-CWDM4-S"},
{"vendor":"Cisco", "partPattern":"QSFP-100G-PSM4-S"},
{"vendor":"Cisco", "partPattern":"QSFP-100G-SR1.2"},
{"vendor":"Cisco", "partPattern":"QSFP-100G-AOC*"},
{"vendor":"Cisco", "partPattern":"QSFP-100G-CU*"},
{"vendor":"Juniper", "partPattern":"JNP-100G-SR4"},
{"vendor":"Juniper", "partPattern":"JNP-100G-LR4"},
{"vendor":"Juniper", "partPattern":"JNP-100G-CWDM4"},
{"vendor":"Juniper", "partPattern":"JNP-100G-PSM4"},
{"vendor":"Arista", "partPattern":"QSFP-100G-SR4"},
{"vendor":"Arista", "partPattern":"QSFP-100G-LR4"},
{"vendor":"Arista", "partPattern":"QSFP-100G-CWDM4"},
{"vendor":"Huawei", "partPattern":"QSFP-100G-SR4"},
{"vendor":"Huawei", "partPattern":"QSFP-100G-LR4"},
{"vendor":"Nokia", "partPattern":"3HE09828*"},
{"vendor":"Nokia", "partPattern":"3HE11013*"},
{"vendor":"HPE/Aruba", "partPattern":"845394*"},
{"vendor":"HPE/Aruba", "partPattern":"845396*"},
{"vendor":"Dell", "partPattern":"407-BCBN"},
{"vendor":"Extreme", "partPattern":"10412*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor = 'QSFP28'
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── QSFP56 (200G) ────────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"QSFP56-200G-*"},
{"vendor":"Juniper", "partPattern":"JNP-200G-*"},
{"vendor":"Arista", "partPattern":"QSFP56-200G-*"},
{"vendor":"Huawei", "partPattern":"QSFP56-200G-*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor = 'QSFP56'
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── QSFP-DD (400G) ───────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"QSFP-DD-400G-*"},
{"vendor":"Cisco", "partPattern":"QDD-400G-*"},
{"vendor":"Juniper", "partPattern":"JNP-QSFP-DD-400G-*"},
{"vendor":"Arista", "partPattern":"QSFP-DD-400G-*"},
{"vendor":"Huawei", "partPattern":"QSFP-DD-400G-*"},
{"vendor":"Nokia", "partPattern":"3HE*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor IN ('QSFP-DD', 'QSFP-DD800', 'QSFP112')
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── OSFP (400G / 800G) ───────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"OSFP-400G-*"},
{"vendor":"Cisco", "partPattern":"OSFP-800G-*"},
{"vendor":"Juniper", "partPattern":"JNP-OSFP-400G-*"},
{"vendor":"Arista", "partPattern":"OSFP-400G-*"},
{"vendor":"Arista", "partPattern":"OSFP-800G-*"},
{"vendor":"Huawei", "partPattern":"OSFP-400G-*"},
{"vendor":"Nokia", "partPattern":"3HE*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor IN ('OSFP', 'OSFP112', 'OSFP224')
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── XFP (10G legacy) ─────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"XFP-10G-MM-SR"},
{"vendor":"Cisco", "partPattern":"XFP-10GLR-OC192SR"},
{"vendor":"Cisco", "partPattern":"XFP-10GER-OC192IR"},
{"vendor":"Cisco", "partPattern":"XFP-10G-ZR"},
{"vendor":"Juniper", "partPattern":"XFP-10G-S"},
{"vendor":"Juniper", "partPattern":"XFP-10G-L-OC192-SR1"},
{"vendor":"Juniper", "partPattern":"XFP-10GE-ER"},
{"vendor":"Juniper", "partPattern":"XFP-10GE-ZR"}
]'::jsonb, updated_at = NOW()
WHERE form_factor = 'XFP'
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── CFP / CFP2 (100G+ coherent) ──────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"CFP-100G-*"},
{"vendor":"Juniper", "partPattern":"CFP-100G-*"},
{"vendor":"Huawei", "partPattern":"CFP-100G-*"},
{"vendor":"Nokia", "partPattern":"3HE*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor IN ('CFP', 'CFP2', 'CFP4')
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── GBIC (legacy 1G) ─────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"WS-G5484"},
{"vendor":"Cisco", "partPattern":"WS-G5486"},
{"vendor":"Cisco", "partPattern":"WS-G5487"},
{"vendor":"HPE/Aruba", "partPattern":"J4130*"},
{"vendor":"HPE/Aruba", "partPattern":"J4131*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor = 'GBIC'
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── CSFP (compact SFP) ───────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"CSFP-*"},
{"vendor":"Juniper", "partPattern":"CSFP-*"},
{"vendor":"Huawei", "partPattern":"CSFP-*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor = 'CSFP'
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── XENPAK / CXP ─────────────────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"XENPAK-10GB-*"},
{"vendor":"Cisco", "partPattern":"X2-10GB-*"},
{"vendor":"Juniper", "partPattern":"XENPAK-10GE-*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor IN ('XENPAK', 'CXP')
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── SFP112 / OSFP224 (800G+) ─────────────────────────────────────────────────
UPDATE transceivers
SET vendor_compat = '[
{"vendor":"Cisco", "partPattern":"SFP112-*"},
{"vendor":"Cisco", "partPattern":"800G-*"},
{"vendor":"Juniper", "partPattern":"JNP-SFP112-*"},
{"vendor":"Arista", "partPattern":"SFP112-*"}
]'::jsonb, updated_at = NOW()
WHERE form_factor IN ('SFP112', 'OSFP224')
AND (vendor_compat = '[]'::jsonb OR vendor_compat IS NULL);
-- ── Report ────────────────────────────────────────────────────────────────────
SELECT
form_factor,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE vendor_compat != '[]'::jsonb AND vendor_compat IS NOT NULL) AS has_compat
FROM transceivers
GROUP BY form_factor
ORDER BY total DESC;