Add flight comparator web app with full scan pipeline
Full-stack flight price scanner built on fast-flights v3 (SOCS cookie bypass): Backend (FastAPI + SQLite): - REST API with rate limiting, Pydantic v2 validation, paginated responses - Scan pipeline: resolves airports, queries every day in the window, saves individual flights + aggregate route stats to SQLite - Background async scan processor with real-time progress tracking - Airport search endpoint backed by OpenFlights dataset - Daily scan window (all dates, not monthly samples) Frontend (React 19 + TypeScript + Tailwind CSS v4): - Dashboard with live scan status and recent scans - Create scan form: country mode or specific airports (searchable dropdown) - Scan detail page with expandable route rows showing individual flights (date, airline, departure, arrival, price) loaded on demand - AirportSearch component with debounced live search and multi-select Database: - scans → routes → flights schema with FK cascade and auto-update triggers - Migrations for schema evolution (relaxed country constraint) Tests: - 74 tests: unit + integration, isolated per-test SQLite DB - Confirmed flight fixtures in tests/confirmed_flights.json (50 real flights, BDS→FMM Ryanair + BDS→DUS Eurowings, scraped Feb 2026) - Integration tests parametrized from confirmed routes Docker: - Multi-stage builds, Compose orchestration, Nginx reverse proxy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
145
flight-comparator/frontend/src/api.ts
Normal file
145
flight-comparator/frontend/src/api.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Types
|
||||
export interface Scan {
|
||||
id: number;
|
||||
origin: string;
|
||||
country: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
total_routes: number;
|
||||
routes_scanned: number;
|
||||
total_flights: number;
|
||||
error_message?: string;
|
||||
seat_class: string;
|
||||
adults: number;
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
id: number;
|
||||
scan_id: number;
|
||||
destination: string;
|
||||
destination_name: string;
|
||||
destination_city?: string;
|
||||
flight_count: number;
|
||||
airlines: string[];
|
||||
min_price?: number;
|
||||
max_price?: number;
|
||||
avg_price?: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Flight {
|
||||
id: number;
|
||||
scan_id: number;
|
||||
destination: string;
|
||||
date: string;
|
||||
airline?: string;
|
||||
departure_time?: string;
|
||||
arrival_time?: string;
|
||||
price?: number;
|
||||
stops: number;
|
||||
}
|
||||
|
||||
export interface Airport {
|
||||
iata: string;
|
||||
name: string;
|
||||
city: string;
|
||||
country: string;
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
message: string;
|
||||
module?: string;
|
||||
function?: string;
|
||||
line?: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateScanRequest {
|
||||
origin: string;
|
||||
country?: string; // Optional: provide either country or destinations
|
||||
destinations?: string[]; // Optional: provide either country or destinations
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
window_months?: number;
|
||||
seat_class?: 'economy' | 'premium' | 'business' | 'first';
|
||||
adults?: number;
|
||||
}
|
||||
|
||||
export interface CreateScanResponse {
|
||||
status: string;
|
||||
id: number;
|
||||
scan: Scan;
|
||||
}
|
||||
|
||||
// API functions
|
||||
export const scanApi = {
|
||||
list: (page = 1, limit = 20, status?: string) => {
|
||||
const params: any = { page, limit };
|
||||
if (status) params.status = status;
|
||||
return api.get<PaginatedResponse<Scan>>('/scans', { params });
|
||||
},
|
||||
|
||||
get: (id: number) => {
|
||||
return api.get<Scan>(`/scans/${id}`);
|
||||
},
|
||||
|
||||
create: (data: CreateScanRequest) => {
|
||||
return api.post<CreateScanResponse>('/scans', data);
|
||||
},
|
||||
|
||||
getRoutes: (id: number, page = 1, limit = 20) => {
|
||||
return api.get<PaginatedResponse<Route>>(`/scans/${id}/routes`, {
|
||||
params: { page, limit }
|
||||
});
|
||||
},
|
||||
|
||||
getFlights: (id: number, destination?: string, page = 1, limit = 50) => {
|
||||
const params: Record<string, unknown> = { page, limit };
|
||||
if (destination) params.destination = destination;
|
||||
return api.get<PaginatedResponse<Flight>>(`/scans/${id}/flights`, { params });
|
||||
},
|
||||
};
|
||||
|
||||
export const airportApi = {
|
||||
search: (query: string, page = 1, limit = 20) => {
|
||||
return api.get<PaginatedResponse<Airport>>('/airports', {
|
||||
params: { q: query, page, limit }
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const logApi = {
|
||||
list: (page = 1, limit = 50, level?: string, search?: string) => {
|
||||
const params: any = { page, limit };
|
||||
if (level) params.level = level;
|
||||
if (search) params.search = search;
|
||||
return api.get<PaginatedResponse<LogEntry>>('/logs', { params });
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
Reference in New Issue
Block a user