July 4, 2026

NCL, Back From Six Silent Weeks

The Failure Mode That Doesn't Alarm

NCL redesigned their site in mid-May, replacing cruise detail pages with a SPA shell. Our HTML parser matched nothing against it -- but every request returned HTTP 200, so scraper runs kept completing successfully with 0 sailings and 0 prices from May 19 onward. The per-line health checks only watched run completion, and the global insert check stayed green because every other line was inserting fine. NCL prices silently flatlined for six and a half weeks.

The Fix: Vacation Builder API

The scrape now uses the JSON API behind NCL's new Vacation Builder (/api/vacations/sailings/{code}), which is cleaner than the old HTML in every way. The API's packageId equals the legacy voyageId, so all existing sailing IDs stay continuous -- price history is unbroken.

A bonus correction fell out: the old parser rendered NCL's UTC-midnight timestamps in LA time, which stored every NCL start date one day early. The API's sail date is authoritative, and existing dates are reconciled in place as sailings re-scrape -- self-healing. A silent-failure trap in the new path (missing pricing data) logs an error instead of dying quietly, and the dead HTML-era parsing code is gone.

The Alert That Would Have Caught It

The scheduler gained a per-line zero-insert check: if a line's runs keep completing but no prices have been inserted for its threshold window, a warning fires. Thresholds were tuned against 45 days of run history -- the worst gap between productive runs on any healthy line is ~4 hours, so the default threshold is 24 hours (unambiguous breakage), with a 4-hour override for Royal Caribbean where hourly resolution is the whole point.

This is the alert that would have caught the NCL breakage within hours instead of weeks.

CI Green Again

Both CI workflows had been red since June 30: the orphan-cleanup test needed updating for the new age guard, and composer audit flagged CVEs in guzzle, psr7, and a symfony polyfill -- all bumped, suite green.