Engineering Diary, Day 10: From Mock Data to Real Inventory — VOLO Goes Live with Avi-Go
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:
| Bug | Severity | Impact |
|---|---|---|
| Connection pool limit: 1 | Critical | All concurrent requests queue behind one connection on serverless |
SET SESSION not wrapped in try-catch | High | Query silently fails on MySQL versions that do not support it |
| Agent tool requires both from AND to | High | Users cannot ask "any empty legs to Miami?" — one-directional searches impossible |
| No timeout on agent DB call | High | A slow query could hang for 60 seconds, consuming the entire serverless function timeout |
| GET handler has no try-catch | High | Unhandled exceptions crash the endpoint |
| NaN propagation to SQL | Medium | Invalid price_min/price_max creates malformed WHERE clauses |
| Error responses cached for 5 minutes | Medium | One transient DB failure means 5 minutes of cached errors for all users |
| Error messages expose internal DB details | Medium | Stack 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:
| Field | Before (Fake) | After (Real) |
|---|---|---|
| Price display | Single total decomposed into 6 fake line items (65% base, 15% fuel, etc.) | Real total price from Avi-Go, displayed as-is |
| Match score | Synthetic: 98 - index * 4 | Removed entirely |
| Manufacturer | First word of model name (often wrong) | Real operator name from API |
| Direct/Stop | Not shown | Green "Direct" or amber "Fuel Stop" badge |
| Year of manufacture | Not shown | Displayed next to aircraft name |
| Amenities | Buried in text, no icons | WiFi / Pet OK / Lavatory / FA badges |
| Aircraft image | Matched 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
| Commit | Change | Impact |
|---|---|---|
dd6ef34 | Empty leg backend: MySQL engine, REST API, agent/MCP tools | Real-time database queries replacing all mock data |
56a8a60 | Production hardening: 8 bugs fixed | Pool 1→5, timeouts, NaN guards, error sanitization |
247d615 | Avi-Go search API: pre-release → production URL | Charter quotes from real production system |
f8ddd79 | 3 frontend pages rewritten with real data | /services/empty-legs, /empty-legs, /empty-legs/[slug] — all live |
12ca89b | Quote card field alignment | Real 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.
Stay Informed
Empty leg deals, new routes, and aviation insights — delivered to your inbox.