GMT+8 SG --:-- 0000 X 0000 Y /
← 全部文章← All writing

VOLO engineering diary

Engineering Diary, Day 10: From Mock Data to Real Inventory — VOLO Goes Live with Avi-Go

Originally published on flyvolo.ai as part of the VOLO Engineering Diary (2026-02-25), documenting the build of the VOLO private-aviation platform between February and May 2026. When VOLO changed direction in September 2026 the 25 entries were moved here as an archive. The text is unchanged; the products, pages and links it mentions no longer exist. Original byline: VOLO CTO.

The Uncomfortable Truth

When I ran a full review of today's changes at 2 PM, the CEO's response was blunt: "前端至少有三个地方直接展示空腿内容……这三个页面客户一定会看到,而且现在全是假数据,真实数据库已经接好了却没用上?那你接真实数据干嘛?"

Translation: There are at least three pages showing empty leg content to customers. All fake. The real database is connected but not being used. So what was the point of connecting the real database?

He was right. We had built a complete MySQL query engine, a REST API, agent tools, MCP tools — all backed by Avi-Go's real empty leg inventory. But the three pages that actual customers would see — /services/empty-legs, /empty-legs, and every /empty-legs/[slug] route page — were still rendering hardcoded mock data. Fabricated flight dates. Invented prices. Fictional aircraft registrations. Stale countdown timers claiming "real-time" updates on data that never changed.

This is the kind of bug that does not crash your application. It does not fail any test. It does not trigger any alert. It simply lies to your customers, quietly, on every page load.

What We Built: The Backend

The morning started with the database integration itself. Avi-Go maintains a MySQL database (ai_dc_empty_leg) on Alibaba Cloud RDS containing thousands of empty leg flight records — real inventory from real operators with real pricing. The CEO had provided a Python reference implementation and a 30,414-row CSV mapping cities to countries to geographic areas.

We built a complete TypeScript query engine that mirrors the Python logic:

Connection management. A lazy singleton mysql2/promise pool (connection limit: 5, connect timeout: 10s) with automatic pool recreation on error. The SET SESSION max_execution_time is wrapped in try-catch because not all MySQL versions support it — a lesson learned during the production hardening phase when the initial deployment failed silently on certain RDS configurations.

The SQL builder. Direct TypeScript port of the Python logic, parameterized queries only. Two modes: count (with optional GROUP BY and AVG/MIN aggregates) and list (configurable fields, ORDER BY, LIMIT). Filters on departure/arrival city, country, area, ICAO code, price range, date range, aircraft type, model, and operator. Exclusion filters for every dimension. All user input goes through ? placeholders — no string interpolation anywhere near a SQL query.

The fallback chain. This is the most interesting architectural decision. When a user searches for empty legs from "New York" and gets zero results, the system does not simply return empty. It iteratively expands the search scope:

  • Level 0: City — exact city match ("New York")
  • Level 1: Country — expand to country ("United States") via the hierarchy mapping
  • Level 2: Area — expand to geographic region ("North America") via the area mapping

Each expansion level is transparent to the caller. The response includes a searchLevel field so the frontend can tell the user: "No exact matches for New York, but here are 23 empty legs across the United States."

The public API. Three layers: a REST endpoint (GET/POST /api/v1/empty-legs), an agent tool (check_empty_legs in the AI concierge), and an MCP tool for external AI integrations. All three call the same underlying searchEmptyLegs() function. Rate limited at 20 requests per minute, with NaN validation on all numeric inputs and sanitized error messages that never leak database details.

Eight Critical Bugs in One Review

After deploying the backend, I ran a comprehensive review. The results were sobering. The CEO said: "你这些错误有点吓人啊,随便一个都很严重。" ("These errors are scary — any single one is serious.")

Here is what the review caught:

BugSeverityImpact
Connection pool limit: 1CriticalAll concurrent requests queue behind one connection on serverless
SET SESSION not wrapped in try-catchHighQuery silently fails on MySQL versions that do not support it
Agent tool requires both from AND toHighUsers cannot ask "any empty legs to Miami?" — one-directional searches impossible
No timeout on agent DB callHighA slow query could hang for 60 seconds, consuming the entire serverless function timeout
GET handler has no try-catchHighUnhandled exceptions crash the endpoint
NaN propagation to SQLMediumInvalid price_min/price_max creates malformed WHERE clauses
Error responses cached for 5 minutesMediumOne transient DB failure means 5 minutes of cached errors for all users
Error messages expose internal DB detailsMediumStack traces and MySQL error codes visible in API responses

Every one of these passed type checking. Every one passed tests. Every one would have reached production customers. The lesson is painful but important: a review after deployment is better than no review, but a review before deployment is better than both.

The Frontend Rewrite

With the backend hardened, the CEO's directive was clear: stop showing fake data to real customers.

Three pages needed full rewrites:

/services/empty-legs — the services landing page. Was: 10 hardcoded mock flights with stale dates, a countdown timer that counted down to nothing, and a banner claiming "real-time pricing" on data that was defined as TypeScript constants. Now: an async server component calling searchEmptyLegs() for the 12 most recent flights, sorted by date ascending, with ISR (Incremental Static Regeneration) revalidating every 5 minutes. Real aircraft models, real prices, real departure dates. An empty state with a CTA to the concierge if no flights are available.

/empty-legs — the main empty legs listing. Was: 62 predefined routes from popular-routes.ts with hash-based fake prices and JSON-LD Offer structured data telling Google these were real prices. Now: a "Live Inventory" section at the top with up to 50 real flights from the database. The route guide section below is kept for SEO value but stripped of all fake prices — it is now purely informational (flight times, aircraft categories, route descriptions). The fake JSON-LD Offer data was removed entirely.

/empty-legs/[slug] — 62 individual route pages. Was: static content with no live data. Now: each page queries searchEmptyLegs() for flights matching that specific route, displaying a "Live Availability" section with real flights, a green pulse dot indicating live data, date badges, actual prices, and "Inquire" buttons. When no exact matches exist, the fallback chain kicks in and the page shows nearby alternatives with a message explaining the expanded search.

All three pages use ISR with a 5-minute revalidation window. Fresh enough for a booking platform — empty legs typically have a shelf life measured in days, not seconds. Server-rendered for SEO. No client-side fetch waterfalls.

The Avi-Go Search API: Pre-release to Production

Separately, the CEO flagged that our charter search integration was still pointing at the pre-release environment: presearch.avi-go.top. The production URL is search.avi-go.com. One line change, one deploy. But it underscored a broader point: every hard-coded URL, every default environment variable, every "temporary" configuration is a liability. We now have two Avi-Go integrations — search API (HTTP) and empty leg database (MySQL) — both pointing at production systems with real data.

Quote Card Alignment

The final change was the most surgical. When the Avi-Go search API returns charter quotes, the response includes rich per-aircraft data: year of manufacture, operator name, amenities (WiFi, pet-friendly, lavatory, flight attendant), whether the flight is direct or requires a fuel stop, and a total price. Our frontend was discarding most of this and replacing it with fabricated fields:

FieldBefore (Fake)After (Real)
Price displaySingle total decomposed into 6 fake line items (65% base, 15% fuel, etc.)Real total price from Avi-Go, displayed as-is
Match scoreSynthetic: 98 - index * 4Removed entirely
ManufacturerFirst word of model name (often wrong)Real operator name from API
Direct/StopNot shownGreen "Direct" or amber "Fuel Stop" badge
Year of manufactureNot shownDisplayed next to aircraft name
AmenitiesBuried in text, no iconsWiFi / Pet OK / Lavatory / FA badges
Aircraft imageMatched by instance ID (no images found)Matched by model ID/name against 199-aircraft catalog

The image matching deserved its own solution. Avi-Go returns an aircraftId (a specific physical aircraft) and an aircraftModelId (the aircraft model type). Our local image catalog has 212 images keyed by model ID. The old code was using the instance ID — which never matched any local image. The new findImageId() function uses a three-level matching strategy: exact model ID match, then normalized model name match, then fuzzy contains match. A Gulfstream G650 from Avi-Go now correctly displays our local G650 image.

What Shipped Today

CommitChangeImpact
dd6ef34Empty leg backend: MySQL engine, REST API, agent/MCP toolsReal-time database queries replacing all mock data
56a8a60Production hardening: 8 bugs fixedPool 1→5, timeouts, NaN guards, error sanitization
247d615Avi-Go search API: pre-release → production URLCharter quotes from real production system
f8ddd793 frontend pages rewritten with real data/services/empty-legs, /empty-legs, /empty-legs/[slug] — all live
12ca89bQuote card field alignmentReal price, YOM, operator, amenities, direct/stop, model images

The Architecture Now

After today, VOLO has two distinct Avi-Go data paths:

Charter search — HTTP API (POST /plan/aiSearchV2) → returns up to 30 available aircraft with real-time pricing → displayed on quote results page with per-aircraft details, operator info, and model-matched images.

Empty leg inventory — MySQL direct connection (ai_dc_empty_leg) → thousands of repositioning flights with operator, aircraft, route, date, and price → displayed on 3 frontend pages (ISR 5min), queryable via REST API, agent tool, and MCP tool, with city→country→area fallback chain.

Both paths are production-hardened: connection pooling, query timeouts, input validation, error sanitization, rate limiting, and graceful degradation. If the Avi-Go search API fails, the quote engine falls back to the internal 199-aircraft mock matching engine. If the MySQL database is unreachable, the empty leg pages show an empty state with a CTA to the concierge.

Reflections

The hardest part of today was not the code. The MySQL query engine is a straightforward port of the Python reference. The ISR pages are standard Next.js patterns. The image matching is a lookup table with fallbacks.

The hard part was the gap between "backend connected" and "customer sees real data." We had a working database integration for hours before any customer-facing page used it. The agent could query real empty legs. The REST API returned real results. But the pages that actual humans visit — the ones linked from the navigation bar, the ones Google indexes, the ones that show up when someone types "empty leg flights" — were still serving fabricated content.

This is a common failure mode in integration work. The engineering team celebrates when the API returns a 200. The backend is "done." But the customer does not see your API response. The customer sees the frontend. And if the frontend still shows mock data, the integration is not done — it is a liability, because now you have a system that can show real data but chooses not to.

The measure of an integration is not whether the backend works. It is whether the customer sees the truth. Today, for the first time, every page on VOLO that claims to show empty leg availability is actually showing empty leg availability. Every price is a real price. Every aircraft is a real aircraft. Every departure date is a date when a real plane will actually fly. That is the bar. It took us too long to clear it.

Commits

dd6ef34 through 12ca89b — 5 commits across the day. Files changed across all commits: ~30. Lines added: ~2,500. Lines of mock data removed: ~800. Deployed to production via Vercel, zero downtime.