{"openapi":"3.1.0","info":{"title":"Award Flight Database","description":"Search award flight availability across 25+ loyalty programs","version":"2.0.0"},"paths":{"/api/auth/register":{"post":{"summary":"Auth Register","description":"Register a new user with email and password.","operationId":"auth_register_api_auth_register_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/login":{"post":{"summary":"Auth Login","description":"Login with email and password.","operationId":"auth_login_api_auth_login_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/google":{"post":{"summary":"Auth Google","description":"Login or register with Google ID token.","operationId":"auth_google_api_auth_google_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/me":{"get":{"summary":"Auth Me","description":"Get current authenticated user info.","operationId":"auth_me_api_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/change-password":{"post":{"summary":"Auth Change Password","description":"Change password for authenticated user.","operationId":"auth_change_password_api_auth_change_password_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/config":{"get":{"summary":"Auth Config","description":"Return public auth configuration (Google Client ID, etc.).","operationId":"auth_config_api_auth_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/user/preferences":{"get":{"summary":"Get User Preferences","description":"Get preferences for the authenticated user.","operationId":"get_user_preferences_api_user_preferences_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"put":{"summary":"Update User Preferences","description":"Update preferences for the authenticated user.","operationId":"update_user_preferences_api_user_preferences_put","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/user/profile":{"get":{"summary":"Get User Profile","description":"Get profile info for authenticated user.","operationId":"get_user_profile_api_user_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"put":{"summary":"Update User Profile","description":"Update profile for authenticated user.","operationId":"update_user_profile_api_user_profile_put","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/programs":{"get":{"tags":["search"],"summary":"List Programs","operationId":"list_programs_api_programs_get","parameters":[{"name":"bank","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bank"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/search":{"get":{"tags":["search"],"summary":"Search","operationId":"search_api_search_get","parameters":[{"name":"origin","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin"}},{"name":"destination","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination"}},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date From"}},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date To"}},{"name":"cabin","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"default":"J","title":"Cabin"}},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},{"name":"exclude_source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Exclude Source"}},{"name":"direct_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Direct Only"}},{"name":"max_miles","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Miles"}},{"name":"min_seats","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Seats"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"default":200,"title":"Limit"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"miles","title":"Sort"}},{"name":"airline","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Airline"}},{"name":"airlines","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Airlines"}},{"name":"max_price","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Price"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/search-connections":{"get":{"tags":["search"],"summary":"Search Connections","description":"Search for connecting itineraries through hub airports.\nmax_stops=0: direct only, max_stops=1: 1-stop connections, max_stops=2: also 2-stop connections.","operationId":"search_connections_api_search_connections_get","parameters":[{"name":"origin","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin"}},{"name":"destination","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination"}},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date From"}},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date To"}},{"name":"cabin","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"default":"J","title":"Cabin"}},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},{"name":"exclude_source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Exclude Source"}},{"name":"max_miles","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Miles"}},{"name":"min_seats","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Seats"}},{"name":"include_direct","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Include Direct"}},{"name":"max_stops","in":"query","required":false,"schema":{"type":"integer","maximum":2,"minimum":0,"default":1,"title":"Max Stops"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"miles","title":"Sort"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/airports/expand":{"get":{"tags":["search"],"summary":"Expand Airports","description":"Expand airport codes to all airports in the same country or continent.\nUsed by the frontend for open jaw search to resolve return airports.","operationId":"expand_airports_api_airports_expand_get","parameters":[{"name":"codes","in":"query","required":true,"schema":{"type":"string","description":"Comma-separated airport codes","title":"Codes"},"description":"Comma-separated airport codes"},{"name":"mode","in":"query","required":false,"schema":{"type":"string","description":"Expansion mode: 'country' or 'continent'","default":"country","title":"Mode"},"description":"Expansion mode: 'country' or 'continent'"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/alerts":{"get":{"summary":"Admin Alerts","operationId":"admin_alerts_api_admin_alerts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Admin Create Alert","description":"Admin alias for creating alerts — routes to the user-scoped endpoint.","operationId":"admin_create_alert_api_admin_alerts_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts/my":{"get":{"summary":"My Alerts","operationId":"my_alerts_api_alerts_my_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts/my-list":{"get":{"summary":"My Alerts List","operationId":"my_alerts_list_api_alerts_my_list_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/alerts-list":{"get":{"summary":"Admin Alerts List","operationId":"admin_alerts_list_api_admin_alerts_list_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts":{"post":{"summary":"Create Alert","operationId":"create_alert_api_alerts_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alerts/{alert_id}":{"put":{"summary":"Update Alert","operationId":"update_alert_api_alerts__alert_id__put","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Alert","operationId":"delete_alert_api_alerts__alert_id__delete","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alerts/{alert_id}/toggle":{"post":{"summary":"Toggle Alert","operationId":"toggle_alert_api_alerts__alert_id__toggle_post","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alerts/claim-orphans":{"post":{"summary":"Claim Orphan Alerts","operationId":"claim_orphan_alerts_api_alerts_claim_orphans_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/alerts/{alert_id}":{"put":{"summary":"Admin Update Alert","description":"Admin alias for updating alerts — routes to the user-scoped endpoint.","operationId":"admin_update_alert_api_admin_alerts__alert_id__put","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Admin Delete Alert","description":"Admin alias for deleting alerts — routes to the user-scoped endpoint.","operationId":"admin_delete_alert_api_admin_alerts__alert_id__delete","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/alerts/{alert_id}/toggle":{"post":{"summary":"Admin Toggle Alert","description":"Admin alias for toggling alerts — routes to the user-scoped endpoint.","operationId":"admin_toggle_alert_api_admin_alerts__alert_id__toggle_post","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/users":{"get":{"tags":["admin"],"summary":"Admin List Users","description":"List all users (admin only).","operationId":"admin_list_users_api_admin_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/users/{user_id}/role":{"put":{"tags":["admin"],"summary":"Admin Change Role","description":"Change a user's role (admin only). master_admin cannot be changed.","operationId":"admin_change_role_api_admin_users__user_id__role_put","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/users/{user_id}":{"delete":{"tags":["admin"],"summary":"Admin Delete User","description":"Delete a user (admin only). Master admin cannot be deleted.","operationId":"admin_delete_user_api_admin_users__user_id__delete","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/users/{user_id}/rate-limit":{"put":{"tags":["admin"],"summary":"Admin Set Rate Limit","description":"Set a per-user rate limit override (admin only).\nBody: { \"rate_limit\": \"120/minute\" } or { \"rate_limit\": null } to clear.\nFormat: \"<number>/<period>\" where period is second, minute, hour, or day.","operationId":"admin_set_rate_limit_api_admin_users__user_id__rate_limit_put","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/rate-limits":{"get":{"tags":["admin"],"summary":"Admin Get Rate Limits","description":"Get current rate limit configuration and all user overrides (admin only).","operationId":"admin_get_rate_limits_api_admin_rate_limits_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/history":{"get":{"tags":["admin"],"summary":"Admin History","operationId":"admin_history_api_admin_history_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/ingestion-coverage":{"get":{"tags":["admin"],"summary":"Admin Ingestion Coverage","operationId":"admin_ingestion_coverage_api_admin_ingestion_coverage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/freshness-heatmap":{"get":{"tags":["admin"],"summary":"Admin Freshness Heatmap","description":"Return per-program, per-date freshness data for the full heatmap.\nSingle query — groups by source and date, returns staleness hours and flight counts.\nAlso returns staleness priority scores for optimal scan prediction.","operationId":"admin_freshness_heatmap_api_admin_freshness_heatmap_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/ingestion-distribution/{source}":{"get":{"tags":["admin"],"summary":"Admin Ingestion Distribution","description":"Return daily flight counts + avg cabin pricing for a program over its release window.","operationId":"admin_ingestion_distribution_api_admin_ingestion_distribution__source__get","parameters":[{"name":"source","in":"path","required":true,"schema":{"type":"string","title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/program-radar":{"get":{"tags":["admin"],"summary":"Admin Program Radar","description":"Return radar chart metrics for all programs (normalized 0-100).","operationId":"admin_program_radar_api_admin_program_radar_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/stop-ingestion":{"post":{"tags":["admin"],"summary":"Admin Stop Ingestion","description":"Stop a running ingestion gracefully. Halts after current page/phase completes.","operationId":"admin_stop_ingestion_api_admin_stop_ingestion_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/trigger-ingestion":{"post":{"tags":["admin"],"summary":"Admin Trigger Ingestion","description":"Trigger ingestion — now runs natively on Railway (replaced GitHub Actions).","operationId":"admin_trigger_ingestion_api_admin_trigger_ingestion_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/trigger-gaps":{"post":{"tags":["admin"],"summary":"Admin Trigger Gaps","description":"Trigger gap filling — now runs natively on Railway (replaced GitHub Actions).","operationId":"admin_trigger_gaps_api_admin_trigger_gaps_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/recency-sweep":{"post":{"tags":["admin"],"summary":"Admin Recency Sweep","description":"Trigger a recency sweep — fetches all records updated in the last N days across selected programs.\nDoes NOT update saved cursors or state.\n\nJSON body:\n  sources:    list or comma-sep string of program slugs (default: all)\n  days_back:  how many days back to set the cursor (default: 1)\n  max_budget: max API calls (default: 50)","operationId":"admin_recency_sweep_api_admin_recency_sweep_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/frontier-sweep":{"post":{"tags":["admin"],"summary":"Admin Frontier Sweep","description":"Trigger a frontier sweep — fetches records from each program's max_departure_date - N days forward.\nDoes NOT update saved cursors or state.\n\nJSON body:\n  sources:      list or comma-sep string of program slugs (default: all)\n  days_overlap: how many days before max_departure_date to start (default: 1)\n  max_budget:   max API calls (default: 50)","operationId":"admin_frontier_sweep_api_admin_frontier_sweep_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/recover-from-prev1":{"post":{"tags":["admin"],"summary":"Recover From Prev1","description":"One-time, PER-PROGRAM recovery of rows lost to the non-atomic upsert\n(2026-06-14). Re-inserts rows that exist in the prev1 backup for the given\nsource(s) but are missing from the live DB. Scoped to the named sources\nonly (never touches other programs' freshly-ingested data), to current-window\ndates only (no reintroducing pruned past dates), and to missing IDs only via\na hash anti-join (bypasses the PK index). Idempotent.\n\nBody: {\"sources\": [\"qantas\", ...]}  or  {\"source\": \"qantas\"}","operationId":"recover_from_prev1_api_admin_recover_from_prev1_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/merge-availability":{"post":{"tags":["admin"],"summary":"Merge Availability Parquet","description":"Merge availability rows from an uploaded Parquet file.\nINSERT ... ON CONFLICT(id) DO NOTHING — only adds rows with new IDs.\nAdmin-only. Upload Parquet as raw binary body.","operationId":"merge_availability_parquet_api_admin_merge_availability_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/ingestion-state":{"get":{"tags":["admin"],"summary":"Admin Ingestion State","description":"Return the full ingestion_state.json for debugging cursor positions.","operationId":"admin_ingestion_state_api_admin_ingestion_state_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/ingestion-state/reset-cursor":{"put":{"tags":["admin"],"summary":"Admin Reset Cursor","description":"Reset cursor/skip for one or more programs so next ingestion starts fresh.","operationId":"admin_reset_cursor_api_admin_ingestion_state_reset_cursor_put","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/db-progress":{"get":{"tags":["admin"],"summary":"Admin Db Progress","description":"Return DB completion progress: per-program using per-airline release schedule days.","operationId":"admin_db_progress_api_admin_db_progress_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/high-scores":{"get":{"tags":["admin"],"summary":"Admin High Scores","operationId":"admin_high_scores_api_admin_high_scores_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/gaps":{"get":{"tags":["admin"],"summary":"Admin Gaps","description":"Detect date gaps across all programs — port of gap_filler.detect_all_gaps.","operationId":"admin_gaps_api_admin_gaps_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/bonuses":{"get":{"tags":["admin"],"summary":"Admin Bonuses","operationId":"admin_bonuses_api_admin_bonuses_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/bonuses/refresh":{"post":{"tags":["admin"],"summary":"Refresh Bonuses","description":"Manually trigger transfer bonus refresh (admin only).","operationId":"refresh_bonuses_api_admin_bonuses_refresh_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/cards":{"get":{"tags":["admin"],"summary":"Admin Cards","operationId":"admin_cards_api_admin_cards_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["admin"],"summary":"Save Cards","operationId":"save_cards_api_admin_cards_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/serpapi-usage":{"get":{"tags":["admin"],"summary":"Serpapi Usage","operationId":"serpapi_usage_api_admin_serpapi_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/flightapi-status":{"get":{"tags":["admin"],"summary":"Flightapi Status","description":"Return FlightAPI.io configuration status and cache stats.","operationId":"flightapi_status_api_admin_flightapi_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/equipment-cache":{"get":{"tags":["admin"],"summary":"Equipment Cache Debug","description":"Debug endpoint: dump equipment cache contents.","operationId":"equipment_cache_debug_api_admin_equipment_cache_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/seats-aero-quota":{"get":{"tags":["admin"],"summary":"Seats Aero Quota","description":"Check seats.aero API remaining daily quota without burning a meaningful call.\n\nMakes a lightweight call to the /availability endpoint with impossible params\nto trigger a response that includes the X-RateLimit-Remaining header.","operationId":"seats_aero_quota_api_admin_seats_aero_quota_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/test-seats-trips/{availability_id}":{"get":{"tags":["admin"],"summary":"Test Seats Trips","description":"Debug endpoint: test seats.aero Trips API for a specific availability ID.","operationId":"test_seats_trips_api_admin_test_seats_trips__availability_id__get","parameters":[{"name":"availability_id","in":"path","required":true,"schema":{"type":"string","title":"Availability Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/test-enrichment-pipeline":{"get":{"tags":["admin"],"summary":"Test Enrichment Pipeline","description":"Diagnostic endpoint: run full equipment enrichment pipeline on a small dataset.\n\nShows every step of the pipeline:\n1. Raw availability records from DB (with current equipment/airlines/flight_numbers)\n2. FlightAPI cash segments with marketing vs operating carrier\n3. Equipment cache state after cash enrichment\n4. Seats.aero Trips API results per availability\n5. Cross-program propagation results\n6. Final enriched state of each flight\n\nReturns a detailed trace of which flights got equipment from which source.","operationId":"test_enrichment_pipeline_api_admin_test_enrichment_pipeline_get","parameters":[{"name":"origin","in":"query","required":false,"schema":{"type":"string","default":"JFK","title":"Origin"}},{"name":"destination","in":"query","required":false,"schema":{"type":"string","default":"IST","title":"Destination"}},{"name":"cabin","in":"query","required":false,"schema":{"type":"string","default":"J","title":"Cabin"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/verify-enrichment":{"get":{"tags":["admin"],"summary":"Verify Enrichment","description":"Verification endpoint: compare pipeline enrichment vs seats.aero Trips ground truth.\n\nFor each availability on a route:\n1. Records the raw DB state (airlines, equipment, flight_numbers)\n2. Calls the Trips API to get the ACTUAL segments (ground truth)\n3. Runs our enrichment pipeline\n4. Compares enriched output vs ground truth\n5. Flags mismatches with detailed diff\n\nReturns a per-availability verdict (MATCH / MISMATCH / NO_TRIPS_DATA / SKIPPED)\nand an overall accuracy score.\n\nUse ?origin=JFK&destination=IST&cabin=J&limit=15&max_trips_calls=5","operationId":"verify_enrichment_api_admin_verify_enrichment_get","parameters":[{"name":"origin","in":"query","required":false,"schema":{"type":"string","default":"JFK","title":"Origin"}},{"name":"destination","in":"query","required":false,"schema":{"type":"string","default":"IST","title":"Destination"}},{"name":"cabin","in":"query","required":false,"schema":{"type":"string","default":"J","title":"Cabin"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":15,"title":"Limit"}},{"name":"max_trips_calls","in":"query","required":false,"schema":{"type":"integer","default":5,"title":"Max Trips Calls"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cash-price":{"get":{"tags":["admin"],"summary":"Get Cash Price","description":"Fetch live cash prices from FlightAPI.io for a given route/date/cabin.","operationId":"get_cash_price_api_cash_price_get","parameters":[{"name":"origin","in":"query","required":true,"schema":{"type":"string","title":"Origin"}},{"name":"destination","in":"query","required":true,"schema":{"type":"string","title":"Destination"}},{"name":"date","in":"query","required":true,"schema":{"type":"string","title":"Date"}},{"name":"cabin","in":"query","required":false,"schema":{"type":"string","default":"Y","title":"Cabin"}},{"name":"currency","in":"query","required":false,"schema":{"type":"string","default":"USD","title":"Currency"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/trip-value":{"get":{"tags":["admin"],"summary":"Get Trip Value","description":"Compare award vs cash value for a route. Optionally split via a connection airport.\n\nExamples:\n  /api/trip-value?origin=MEM&destination=LHR&date=2026-04-15&cabin=J&source=virginatlantic\n  /api/trip-value?origin=MEM&destination=LHR&date=2026-04-15&cabin=J&via=JFK","operationId":"get_trip_value_api_trip_value_get","parameters":[{"name":"origin","in":"query","required":true,"schema":{"type":"string","title":"Origin"}},{"name":"destination","in":"query","required":true,"schema":{"type":"string","title":"Destination"}},{"name":"date","in":"query","required":true,"schema":{"type":"string","title":"Date"}},{"name":"cabin","in":"query","required":false,"schema":{"type":"string","default":"J","title":"Cabin"}},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},{"name":"via","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Via"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/email-preview":{"get":{"tags":["admin"],"summary":"Admin Email Preview","description":"Full email preview with RT pairing, grouped cards, transfer costs, and open jaw support.\n\ntrip_type: 'one_way', 'round_trip', or 'open_jaw'\nopen_jaw: 'same' (strict RT), 'country', 'continent' — for RT/OJ, how to expand return airports\nbanks: comma-separated bank keys (chase,amex,etc.) for transfer cost display\nairlines: comma-separated IATA airline codes (NH,JL,CX) to filter operating carrier\nmax_stops: 0=direct only, 1=include 1-stop connections, 2=include 2-stop connections\nenrich_cash: if true, fetch live cash prices from FlightAPI.io for CPP comparison","operationId":"admin_email_preview_api_admin_email_preview_get","parameters":[{"name":"alert_name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Alert Name"}},{"name":"origin","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin"}},{"name":"destination","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination"}},{"name":"cabin","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"default":"J","title":"Cabin"}},{"name":"max_miles","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Miles"}},{"name":"trip_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"default":"round_trip","title":"Trip Type"}},{"name":"sources","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sources"}},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date From"}},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date To"}},{"name":"home_airport","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Home Airport"}},{"name":"min_days","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Days"}},{"name":"max_days","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Days"}},{"name":"return_origin","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Return Origin"}},{"name":"return_destination","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Return Destination"}},{"name":"airlines","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Airlines"}},{"name":"open_jaw","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"default":"same","title":"Open Jaw"}},{"name":"banks","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banks"}},{"name":"max_stops","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":0,"title":"Max Stops"}},{"name":"enrich_cash","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":false,"title":"Enrich Cash"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/flush-wal":{"post":{"tags":["admin"],"summary":"Flush Wal","description":"Spawn a checkpoint subprocess to merge the WAL into the main DB file.\n\nWhy a subprocess instead of `con.execute(\"FORCE CHECKPOINT\")` in-process:\non the production 13 GB DuckDB file the in-process call hung for 20+\nminutes at the first attempt during ingestion (verified 2026-04-30) —\nFastAPI's `_db_conn` keeps fielding admin polls, FORCE CHECKPOINT waits\nto abort those transactions, and the wait never resolves. The\nsubprocess pattern (used by the periodic CHECKPOINT cron, the\npost-ingestion CHECKPOINT, and the mid-ingestion CHECKPOINT) bypasses\nthis by running in a separate process: it acquires the DuckDB file-\nlevel writer lock cleanly and core.get_db() returns clean 503s\n(via /data/archive.lock presence) for the duration.\n\nReturns immediately. The subprocess removes /data/archive.lock when\nfinished. Watch Railway logs for \"Manual CHECKPOINT subprocess\nspawned\" and a few minutes later \"Removed archive lock file\".","operationId":"flush_wal_api_admin_flush_wal_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/db-info":{"get":{"tags":["admin"],"summary":"Get Db Info","operationId":"get_db_info_api_admin_db_info_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/settings":{"get":{"tags":["admin"],"summary":"Get Settings","operationId":"get_settings_api_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"put":{"tags":["admin"],"summary":"Update Settings","operationId":"update_settings_api_settings_put","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/analytics/event":{"post":{"tags":["admin"],"summary":"Log Analytics Event","description":"Fire-and-forget analytics event logger. Never errors to client.","operationId":"log_analytics_event_api_analytics_event_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/analytics":{"get":{"tags":["admin"],"summary":"Admin Analytics","description":"Return analytics dashboard data for admin panel.","operationId":"admin_analytics_api_admin_analytics_get","parameters":[{"name":"period","in":"query","required":false,"schema":{"type":"string","default":"week","title":"Period"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/scraper-coverage":{"get":{"tags":["admin"],"summary":"Admin Scraper Coverage","description":"Per-source scraper coverage stats — mirrors ingestion-coverage pattern.","operationId":"admin_scraper_coverage_api_admin_scraper_coverage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/scraper-distribution/{source}":{"get":{"tags":["admin"],"summary":"Admin Scraper Distribution","description":"Return daily flight counts + avg cabin pricing for a scraper source over 365 days.","operationId":"admin_scraper_distribution_api_admin_scraper_distribution__source__get","parameters":[{"name":"source","in":"path","required":true,"schema":{"type":"string","title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/scraper-stats":{"get":{"tags":["admin"],"summary":"Admin Scraper Stats","description":"Aggregate stats for animated counters: total flights, biz/econ averages.","operationId":"admin_scraper_stats_api_admin_scraper_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/scraper-runs":{"get":{"tags":["admin"],"summary":"Admin Scraper Runs","description":"Recent scrape runs with stats.","operationId":"admin_scraper_runs_api_admin_scraper_runs_get","parameters":[{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/scraper-gaps":{"get":{"tags":["admin"],"summary":"Admin Scraper Gaps","description":"Detect date gaps in scraper coverage — mirrors /api/admin/gaps pattern.","operationId":"admin_scraper_gaps_api_admin_scraper_gaps_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/trigger-scraper-gaps":{"post":{"tags":["admin"],"summary":"Admin Trigger Scraper Gaps","description":"Trigger scraper gap fill for selected sources with date range control.\nBody: { sources: [\"jal\",\"iberia\"], fill_mode: \"day|days|month|months|year\",\n        start_date: \"2026-04-01\", end_date: \"2026-04-30\", resume: true }","operationId":"admin_trigger_scraper_gaps_api_admin_trigger_scraper_gaps_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/scraper-state":{"get":{"tags":["admin"],"summary":"Admin Scraper State","description":"Return scraper state including gap fill request status and resume position.","operationId":"admin_scraper_state_api_admin_scraper_state_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"put":{"tags":["admin"],"summary":"Admin Update Scraper State","description":"Update scraper state (e.g., mark gap fill as in-progress, update cursor).","operationId":"admin_update_scraper_state_api_admin_scraper_state_put","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/scraper-backup":{"post":{"tags":["admin"],"summary":"Admin Scraper Backup","description":"Create a backup of the scraper DuckDB database.","operationId":"admin_scraper_backup_api_admin_scraper_backup_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/scraper-backup-status":{"get":{"tags":["admin"],"summary":"Admin Scraper Backup Status","description":"Check scraper DB backup status — sizes and dates.","operationId":"admin_scraper_backup_status_api_admin_scraper_backup_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/scraper-config":{"get":{"tags":["admin"],"summary":"Admin Scraper Config","description":"Return scraper configuration — routes, sources, release days.","operationId":"admin_scraper_config_api_admin_scraper_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/trigger-scraper":{"post":{"tags":["admin"],"summary":"Admin Trigger Scraper","description":"Trigger a scraper run as a background thread.\nBody: { source: \"iberia\"|\"jal\", days_ahead: 90, routes: [optional subset] }","operationId":"admin_trigger_scraper_api_admin_trigger_scraper_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/scraper-status":{"get":{"tags":["admin"],"summary":"Admin Scraper Status","description":"Get current scraper run status.","operationId":"admin_scraper_status_api_admin_scraper_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/stop-scraper":{"post":{"tags":["admin"],"summary":"Admin Stop Scraper","description":"Stop the currently running scraper.","operationId":"admin_stop_scraper_api_admin_stop_scraper_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/podcast-topics":{"get":{"tags":["podcast"],"summary":"Admin List Topics","operationId":"admin_list_topics_api_admin_podcast_topics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["podcast"],"summary":"Admin Create Topic","operationId":"admin_create_topic_api_admin_podcast_topics_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/podcast-topics/{topic_id}":{"put":{"tags":["podcast"],"summary":"Admin Update Topic","operationId":"admin_update_topic_api_admin_podcast_topics__topic_id__put","parameters":[{"name":"topic_id","in":"path","required":true,"schema":{"type":"integer","title":"Topic Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["podcast"],"summary":"Admin Delete Topic","operationId":"admin_delete_topic_api_admin_podcast_topics__topic_id__delete","parameters":[{"name":"topic_id","in":"path","required":true,"schema":{"type":"integer","title":"Topic Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/podcast-qa-reports":{"get":{"tags":["podcast"],"summary":"Admin List Qa","operationId":"admin_list_qa_api_admin_podcast_qa_reports_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["podcast"],"summary":"Admin Create Qa","operationId":"admin_create_qa_api_admin_podcast_qa_reports_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/podcast-qa-reports/{report_id}":{"delete":{"tags":["podcast"],"summary":"Admin Delete Qa","operationId":"admin_delete_qa_api_admin_podcast_qa_reports__report_id__delete","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"integer","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/podcast-research-briefs":{"get":{"tags":["podcast"],"summary":"Admin List Briefs","operationId":"admin_list_briefs_api_admin_podcast_research_briefs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["podcast"],"summary":"Admin Create Brief","operationId":"admin_create_brief_api_admin_podcast_research_briefs_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/podcast-research-briefs/{brief_id}":{"delete":{"tags":["podcast"],"summary":"Admin Delete Brief","operationId":"admin_delete_brief_api_admin_podcast_research_briefs__brief_id__delete","parameters":[{"name":"brief_id","in":"path","required":true,"schema":{"type":"integer","title":"Brief Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/podcast":{"get":{"tags":["admin"],"summary":"Admin Podcast Tab","description":"Admin tab for podcast management.","operationId":"admin_podcast_tab_api_admin_podcast_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/upload-scraper-data":{"post":{"tags":["admin"],"summary":"Upload Scraper Data","description":"Receive scraped award flight data from local or remote scrapers.\nExpects JSON: {\"source\": \"iberia\", \"flights\": [...]}\nEach flight: origin, destination, date, flight_numbers, airlines,\ndirect, stops, departure_time, arrival_time, duration_minutes, equipment,\ny/w/j/f _available, _mileage, _taxes fields.","operationId":"upload_scraper_data_api_admin_upload_scraper_data_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/backfill-transfer-ratios":{"post":{"tags":["admin"],"summary":"Admin Backfill Transfer Ratios","description":"Sync TRANSFER_RATIOS dict → transfer_cache DB rows (ratio + transfer_speed).\n\nThe bonus scraper only writes bonus columns, never ratio/speed. This\nendpoint backfills (and updates) ratio/speed for every (bank, program)\npair in TRANSFER_RATIOS. Safe to call any time; idempotent.","operationId":"admin_backfill_transfer_ratios_api_admin_backfill_transfer_ratios_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/create-checkout-session":{"post":{"summary":"Create Checkout Session","description":"Create a Stripe Checkout session for subscription upgrade.","operationId":"create_checkout_session_api_create_checkout_session_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/webhooks/stripe":{"post":{"summary":"Stripe Webhook","description":"Handle Stripe webhook events for subscription lifecycle.","operationId":"stripe_webhook_api_webhooks_stripe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/subscription/status":{"get":{"summary":"Subscription Status","description":"Get current user subscription status.","operationId":"subscription_status_api_subscription_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/create-portal-session":{"post":{"summary":"Create Portal Session","description":"Create a Stripe Billing Portal session for self-service subscription management.","operationId":"create_portal_session_api_create_portal_session_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/mcp/payment-success":{"get":{"summary":"Mcp Payment Success","description":"Landing page after successful MCP micropayment checkout.","operationId":"mcp_payment_success_mcp_payment_success_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/mcp/payment-cancel":{"get":{"summary":"Mcp Payment Cancel","description":"Landing page after canceled MCP micropayment checkout.","operationId":"mcp_payment_cancel_mcp_payment_cancel_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/podcast":{"get":{"tags":["podcast"],"summary":"Podcast Landing","description":"Public podcast landing page.","operationId":"podcast_landing_podcast_head","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"tags":["podcast"],"summary":"Podcast Landing","description":"Public podcast landing page.","operationId":"podcast_landing_podcast_head","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/podcast/rss":{"get":{"tags":["podcast"],"summary":"Podcast Rss","description":"Generate RSS feed for the podcast.","operationId":"podcast_rss_podcast_rss_head","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"tags":["podcast"],"summary":"Podcast Rss","description":"Generate RSS feed for the podcast.","operationId":"podcast_rss_podcast_rss_head","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/podcast/audio/{filename}":{"head":{"tags":["podcast"],"summary":"Serve Podcast Audio","description":"Serve podcast audio files.","operationId":"serve_podcast_audio_podcast_audio__filename__head","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["podcast"],"summary":"Serve Podcast Audio","description":"Serve podcast audio files.","operationId":"serve_podcast_audio_podcast_audio__filename__head","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/podcast/episodes":{"get":{"tags":["podcast"],"summary":"List Podcast Episodes","description":"List all podcast episodes.","operationId":"list_podcast_episodes_api_podcast_episodes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["podcast"],"summary":"Create Podcast Episode","description":"Create a new podcast episode.","operationId":"create_podcast_episode_api_podcast_episodes_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/podcast/episodes/{episode_id}":{"get":{"tags":["podcast"],"summary":"Get Podcast Episode","description":"Get a specific podcast episode.","operationId":"get_podcast_episode_api_podcast_episodes__episode_id__get","parameters":[{"name":"episode_id","in":"path","required":true,"schema":{"type":"string","title":"Episode Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["podcast"],"summary":"Update Podcast Episode","description":"Update a podcast episode.","operationId":"update_podcast_episode_api_podcast_episodes__episode_id__put","parameters":[{"name":"episode_id","in":"path","required":true,"schema":{"type":"string","title":"Episode Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["podcast"],"summary":"Delete Podcast Episode","description":"Delete a podcast episode.","operationId":"delete_podcast_episode_api_podcast_episodes__episode_id__delete","parameters":[{"name":"episode_id","in":"path","required":true,"schema":{"type":"string","title":"Episode Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/podcast/episodes/{episode_id}/upload":{"post":{"tags":["podcast"],"summary":"Upload Podcast Audio","description":"Upload audio file for a podcast episode (binary body or multipart form).","operationId":"upload_podcast_audio_api_podcast_episodes__episode_id__upload_post","parameters":[{"name":"episode_id","in":"path","required":true,"schema":{"type":"string","title":"Episode Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/podcast/topics":{"get":{"tags":["podcast"],"summary":"List Podcast Topics","description":"List all podcast topic suggestions.","operationId":"list_podcast_topics_api_podcast_topics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["podcast"],"summary":"Suggest Podcast Topic","description":"Submit a podcast topic suggestion.","operationId":"suggest_podcast_topic_api_podcast_topics_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/podcast/pending-topics":{"get":{"tags":["podcast"],"summary":"Get Pending Topics","description":"List pending topic suggestions (used by podcast_pipeline.py).","operationId":"get_pending_topics_api_podcast_pending_topics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/podcast/topics/{topic_id}/mark-used":{"put":{"tags":["podcast"],"summary":"Mark Topic Used","description":"Mark a topic as used (called by podcast_pipeline.py).","operationId":"mark_topic_used_api_podcast_topics__topic_id__mark_used_put","parameters":[{"name":"topic_id","in":"path","required":true,"schema":{"type":"integer","title":"Topic Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/podcast/qa-reports":{"get":{"tags":["podcast"],"summary":"List Qa Reports","description":"List all podcast QA reports.","operationId":"list_qa_reports_api_podcast_qa_reports_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["podcast"],"summary":"Create Qa Report","description":"Create a new QA report.","operationId":"create_qa_report_api_podcast_qa_reports_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/podcast/research-briefs":{"get":{"tags":["podcast"],"summary":"List Research Briefs","description":"List all podcast research briefs.","operationId":"list_research_briefs_api_podcast_research_briefs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["podcast"],"summary":"Create Research Brief","description":"Create a new research brief.","operationId":"create_research_brief_api_podcast_research_briefs_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/health":{"get":{"summary":"Health","description":"Lightweight liveness probe — Docker HEALTHCHECK pings this every 60s\nwith a 10s timeout. Must NOT touch the DB: when a write (e.g. the\nnightly prune) holds the DuckDB file lock, the SELECT here used to\nblock, the probe used to time out, Docker would declare the container\nunhealthy, and Railway would kill it mid-write. That's the \"container\nkeeps restarting during prune\" symptom. Task #120.","operationId":"health_api_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/health/deep":{"get":{"summary":"Health Deep","description":"Optional deeper check that DOES touch the DB — for dashboards and\nmanual diagnostics, NOT for the Docker HEALTHCHECK.","operationId":"health_deep_api_health_deep_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/stats":{"get":{"summary":"Stats","operationId":"stats_api_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/flight-options":{"get":{"summary":"Flight Options","description":"Return ALL programs with availability for a specific route + date, across all cabin classes.","operationId":"flight_options_api_flight_options_get","parameters":[{"name":"origin","in":"query","required":true,"schema":{"type":"string","title":"Origin"}},{"name":"destination","in":"query","required":true,"schema":{"type":"string","title":"Destination"}},{"name":"date","in":"query","required":true,"schema":{"type":"string","title":"Date"}},{"name":"cabin","in":"query","required":false,"schema":{"type":"string","default":"J","title":"Cabin"}},{"name":"trip_type","in":"query","required":false,"schema":{"type":"string","default":"one_way","title":"Trip Type"}},{"name":"return_date","in":"query","required":false,"schema":{"type":"string","default":"","title":"Return Date"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/trips/{availability_id}":{"get":{"summary":"Get Trip Details","description":"Fetch detailed trip information from Seats.aero Trips API with caching.\n\nReturns flight-level data: departure/arrival times, duration, stops, segments,\nfare classes, taxes, booking links — everything needed for a Seats.aero-style detail view.\n\nCache-first: checks in-memory → DB → calls API if miss.\nRate-limited per user per day based on role.","operationId":"get_trip_details_api_trips__availability_id__get","parameters":[{"name":"availability_id","in":"path","required":true,"schema":{"type":"string","title":"Availability Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/routes":{"get":{"summary":"List Routes","operationId":"list_routes_api_routes_get","parameters":[{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},{"name":"origin","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/resolve-location":{"get":{"summary":"Resolve Location","operationId":"resolve_location_api_resolve_location_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/airports":{"get":{"summary":"List Airports","operationId":"list_airports_api_airports_get","parameters":[{"name":"continent","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Continent"}},{"name":"country","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"}},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Active Only"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/airports/map":{"get":{"summary":"Airports Map","description":"Return airports with coordinates and flight counts for the map visualization.","operationId":"airports_map_api_airports_map_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/transfer-partners":{"get":{"summary":"Get Transfer Partners","description":"Build transfer partner data from PROGRAMS + BANK_PROGRAMS + TRANSFER_RATIOS.","operationId":"get_transfer_partners_api_transfer_partners_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/transfer-bonuses":{"get":{"summary":"Get Transfer Bonuses","operationId":"get_transfer_bonuses_api_transfer_bonuses_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/google742a9ff9960965ac.html":{"get":{"summary":"Google Verification","description":"Google Search Console verification file.","operationId":"google_verification_google742a9ff9960965ac_html_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/google59c018b920b70b8c.html":{"get":{"summary":"Google Verification Www","description":"Google Search Console verification file (www property).","operationId":"google_verification_www_google59c018b920b70b8c_html_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/robots.txt":{"get":{"summary":"Robots Txt","description":"Serve robots.txt for search engine crawlers.","operationId":"robots_txt_robots_txt_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/sitemap.xml":{"get":{"summary":"Sitemap Xml","description":"Dynamic sitemap with main pages and podcast episodes.","operationId":"sitemap_xml_sitemap_xml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/version":{"get":{"summary":"Get Version","operationId":"get_version_api_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/dashboard/route-density":{"get":{"summary":"Route Density","description":"Return top routes with flight counts + airport coords for density map.\n\nOptional date_from / date_to (YYYY-MM-DD) restrict which availability rows\ncount toward the route's flights / best-J / best-Y aggregates.","operationId":"route_density_api_dashboard_route_density_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":800,"title":"Limit"}},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date From"}},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date To"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/dashboard/route-detail":{"get":{"summary":"Route Detail","description":"Return detailed availability for a specific route — all cabins, airlines, dates.\nOptional date_from / date_to (YYYY-MM-DD) restrict the dates returned.","operationId":"route_detail_api_dashboard_route_detail_get","parameters":[{"name":"origin","in":"query","required":true,"schema":{"type":"string","title":"Origin"}},{"name":"destination","in":"query","required":true,"schema":{"type":"string","title":"Destination"}},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date From"}},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date To"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/dashboard/sweet-spots":{"get":{"summary":"Sweet Spots","description":"Return cheapest mileage per program x month grid for heatmap.","operationId":"sweet_spots_api_dashboard_sweet_spots_get","parameters":[{"name":"origin","in":"query","required":false,"schema":{"type":"string","title":"Origin"}},{"name":"destination","in":"query","required":false,"schema":{"type":"string","title":"Destination"}},{"name":"origin_type","in":"query","required":false,"schema":{"type":"string","default":"iata","title":"Origin Type"}},{"name":"dest_type","in":"query","required":false,"schema":{"type":"string","default":"iata","title":"Dest Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/dashboard/calendar-availability":{"get":{"summary":"Calendar Availability","description":"Return daily cabin-class availability for a calendar view.","operationId":"calendar_availability_api_dashboard_calendar_availability_get","parameters":[{"name":"origin","in":"query","required":false,"schema":{"type":"string","title":"Origin"}},{"name":"destination","in":"query","required":false,"schema":{"type":"string","title":"Destination"}},{"name":"origin_type","in":"query","required":false,"schema":{"type":"string","default":"iata","title":"Origin Type"}},{"name":"dest_type","in":"query","required":false,"schema":{"type":"string","default":"iata","title":"Dest Type"}},{"name":"month","in":"query","required":false,"schema":{"type":"string","title":"Month"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mobile":{"get":{"summary":"Serve Mobile","description":"Serve the mobile-optimized PWA frontend.","operationId":"serve_mobile_mobile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/":{"get":{"summary":"Serve Frontend","operationId":"serve_frontend__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/pathfinder/hubs":{"get":{"summary":"Get Hubs","description":"Suggest hub airports for a hub-and-spoke route between origin and destination.\n\nNEW LOGIC (2026-04-14):\n- If origin IS a top-50 hub → it can serve as its own hub (no connection needed).\n  Still check award availability from origin to destination.\n- If origin is NOT a top-50 hub → use OpenFlights airline route data to find\n  which top-50 hubs the origin has direct airline service to. Then verify which\n  of those hubs have award availability to the destination.\n- Same logic applies in reverse for return hubs (destination side).\n\nThis replaces the old approach of querying the Seats.aero availability table\nfor origin→hub connections, which only had international routes and returned\nzero results for domestic-only airports like MEM.","operationId":"get_hubs_api_pathfinder_hubs_get","parameters":[{"name":"origin","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":3,"title":"Origin"}},{"name":"destination","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":3,"title":"Destination"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/pathfinder/search":{"post":{"summary":"Pathfinder Search","description":"Hub-and-spoke award flight search.\n\nRequest body:\n{\n    \"origin\": \"MEM\",\n    \"destination\": \"ATH\",\n    \"outbound_date\": \"2026-05-15\",\n    \"return_date\": \"2026-05-22\",       // optional\n    \"outbound_hubs\": [\"JFK\", \"ORD\"],   // hubs to route through outbound\n    \"return_hubs\": [\"FRA\", \"CDG\"],     // hubs for return (can differ)\n    \"connection_cabin\": \"Y\",\n    \"longhaul_cabin\": \"J\"\n}\n\nFor each hub, builds route: origin → hub → destination.\nEach leg is classified as connection (<3000mi) or longhaul (>3000mi).\nConnection legs get connection_cabin, longhaul legs get longhaul_cabin.\nBoth award availability AND cash prices are fetched for every leg.","operationId":"pathfinder_search_api_pathfinder_search_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/pathfinder/price":{"get":{"summary":"Get Price","description":"Fetch cash prices for a single leg using FlightAPI.io.\n\nReturns: { cheapest: float, currency: str, options: [...] }","operationId":"get_price_api_pathfinder_price_get","parameters":[{"name":"origin","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":3,"title":"Origin"}},{"name":"destination","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":3,"title":"Destination"}},{"name":"date","in":"query","required":true,"schema":{"type":"string","title":"Date"}},{"name":"cabin","in":"query","required":false,"schema":{"type":"string","pattern":"^[YWJF]$","default":"Y","title":"Cabin"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/pathfinder/airports/search":{"get":{"summary":"Search Airports","description":"Autocomplete search for airports by IATA code, city, or name.\nReturns top 10 matches.","operationId":"search_airports_api_pathfinder_airports_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/travel-agent/init":{"post":{"summary":"Init Tables","description":"Initialize travel agent tables.","operationId":"init_tables_api_admin_travel_agent_init_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/travel-agent/build-trip":{"post":{"summary":"Build Trip","description":"Build a complete multi-city trip.\nBody: {\n    \"home_airport\": \"MEM\",\n    \"destinations\": [\n        {\"city\": \"Tokyo\", \"airport\": \"NRT\", \"arrival_date\": \"2026-05-01\", \"nights\": 3},\n        {\"city\": \"Bangkok\", \"airport\": \"BKK\", \"arrival_date\": \"2026-05-04\", \"nights\": 4},\n    ],\n    \"cabin\": \"J\",\n    \"travelers\": 1\n}","operationId":"build_trip_api_admin_travel_agent_build_trip_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/travel-agent/itinerary":{"post":{"summary":"Get Itinerary","description":"Generate itinerary for a single city (for testing/preview).","operationId":"get_itinerary_api_admin_travel_agent_itinerary_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/travel-agent/cache-stats":{"get":{"summary":"Cache Stats","description":"Show cached itineraries.","operationId":"cache_stats_api_admin_travel_agent_cache_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/travel-agent/cache":{"delete":{"summary":"Clear Cache","description":"Clear itinerary cache.","operationId":"clear_cache_api_admin_travel_agent_cache_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/travel-agent/search-flights":{"post":{"summary":"Search Flights For Leg","description":"Search award + cash flights for a single leg.","operationId":"search_flights_for_leg_api_admin_travel_agent_search_flights_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/travel-agent-v2/init":{"post":{"summary":"Init V2","operationId":"init_v2_api_admin_travel_agent_v2_init_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/travel-agent-v2/build-trip":{"post":{"summary":"Build Trip V2","description":"Body:\n{\n  \"home_airport\": \"MEM\",\n  \"start_date\": \"2026-07-01\",\n  \"travelers\": 1,\n  \"return_cabin\": \"J\",\n  \"legs\": [\n    {\"airport\":\"CHI\",\"city\":\"Chicago\",\"type\":\"connection\",\"nights\":0,\"cabin\":\"Y\"},\n    {\"airport\":\"DUB\",\"city\":\"Dublin\",\"type\":\"stay\",\"nights\":3,\"cabin\":\"J\"},\n    {\"airport\":\"LHR\",\"city\":\"London\",\"type\":\"on_my_own\",\"nights\":2,\"cabin\":\"J\"}\n  ]\n}","operationId":"build_trip_v2_api_admin_travel_agent_v2_build_trip_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/mcp/stats":{"get":{"summary":"Mcp Stats","description":"Get MCP usage statistics for the dashboard.","operationId":"mcp_stats_api_admin_mcp_stats_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":7,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/mcp/health":{"get":{"summary":"Mcp Health","description":"MCP server health check.","operationId":"mcp_health_api_admin_mcp_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/mcp/generate-key":{"post":{"summary":"Generate Key","description":"Generate a new MCP API key.","operationId":"generate_key_api_admin_mcp_generate_key_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/mcp/balance":{"get":{"summary":"Mcp Balance","description":"Check credit balance for an MCP API key.\n\nQuery params: api_key (required)\nReturns: balance_cents, total_deposited, total_spent, queries_remaining","operationId":"mcp_balance_api_mcp_balance_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/mcp/purchase-credits":{"post":{"summary":"Mcp Purchase Credits","description":"Create a Stripe checkout session to purchase MCP credits.\n\nBody: { \"api_key\": \"afd_...\", \"package\": \"starter|standard|pro\" }\nReturns: { \"checkout_url\": \"https://checkout.stripe.com/...\" }","operationId":"mcp_purchase_credits_api_mcp_purchase_credits_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/webhooks/mcp-stripe":{"post":{"summary":"Mcp Stripe Webhook","description":"Stripe webhook to fulfill MCP credit purchases.\n\nListens for checkout.session.completed events where metadata.type == 'mcp_credits'.\nAdds credits to the agent's balance.","operationId":"mcp_stripe_webhook_api_webhooks_mcp_stripe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/mcp/add-credits":{"post":{"summary":"Admin Add Credits","description":"Admin endpoint to manually add credits to an agent.\n\nBody: { \"agent_id\": \"agent_xxx\", \"amount_cents\": 1000, \"notes\": \"manual top-up\" }","operationId":"admin_add_credits_api_admin_mcp_add_credits_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/mcp/revenue":{"get":{"summary":"Mcp Revenue","description":"Revenue analytics for MCP credits.","operationId":"mcp_revenue_api_admin_mcp_revenue_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/mcp/packages":{"get":{"summary":"Mcp Packages","description":"List available MCP credit packages (public endpoint).","operationId":"mcp_packages_api_mcp_packages_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/partner/interest":{"post":{"summary":"Submit Partner Interest","description":"Handle airline partner interest submissions.\n\nStores partner inquiry for manual review and sends acknowledgment.","operationId":"submit_partner_interest_api_partner_interest_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/partner/status":{"get":{"summary":"Get Partner Status","description":"Get partner network status (public endpoint).\nReturns aggregated stats about the partner network.","operationId":"get_partner_status_api_partner_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/partner/register":{"post":{"summary":"Register Partner","description":"Register a new airline partner for direct data submission.\n\nAdmin-only endpoint. Creates entry in airline_partners table and generates\na unique API key for subsequent submissions.\n\nInput JSON:\n  - airline_name (str, required): Full airline name\n  - contact_email (str, required): Partner contact email\n  - program_slug (str, required): Loyalty program slug (must match one of 26 programs)\n  - website_url (str, optional): Airline's website\n\nReturns:\n  - api_key (str): afd_partner_<32 hex chars> — store securely\n  - partner_id (str): UUID for this partner\n  - program_slug (str): Normalized program slug\n  - created_at (str): ISO timestamp","operationId":"register_partner_api_partner_register_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/partner/list":{"get":{"summary":"List Partners","description":"List all registered airline partners with submission statistics.\nAdmin-only endpoint.\n\nReturns array of partners with:\n  - partner_id, airline_name, program_slug\n  - api_key (masked)\n  - total_submissions, last_submission\n  - active status","operationId":"list_partners_api_partner_list_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/partner/submit-availability":{"post":{"summary":"Submit Availability","description":"Submit award flight availability data to AFD.\n\nAuth: X-Partner-Key header (partner API key)\n\nInput JSON array of availability records:\n  [\n    {\n      \"origin\": \"JFK\",           # 3-letter IATA, required\n      \"destination\": \"LHR\",      # 3-letter IATA, required\n      \"date\": \"2026-05-15\",      # YYYY-MM-DD, required\n      \"cabin\": \"J\",              # Y/W/J/F, required\n      \"miles_required\": 100000,  # integer, required\n      \"seats_available\": 2,      # integer, default 1\n      \"direct\": true,            # boolean, default false\n      \"aircraft\": \"77W\",         # string, optional\n      \"expires_at\": \"2026-04-01\" # YYYY-MM-DD, optional (when this avail expires)\n    },\n    ...\n  ]\n\nReturns:\n  - records_accepted (int): number of records inserted\n  - submission_id (str): UUID for this submission batch\n  - errors (list): any validation errors (if any record fails, entire batch rejected)","operationId":"submit_availability_api_partner_submit_availability_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/partner/submit-bulk":{"post":{"summary":"Submit Bulk","description":"Bulk CSV upload of award flight availability.\n\nAuth: X-Partner-Key header\n\nCSV columns (header required):\n  origin,destination,date,cabin,miles_required,seats_available,direct,aircraft,expires_at\n\nExample:\n  origin,destination,date,cabin,miles_required,seats_available,direct,aircraft,expires_at\n  JFK,LHR,2026-05-15,J,100000,2,true,77W,2026-04-01\n  JFK,CDG,2026-05-20,Y,50000,5,false,,\n\nProcesses in batches of 1000 rows.\n\nReturns:\n  - submission_id (str): batch identifier\n  - total_processed (int): rows processed\n  - rows_inserted (int): rows successfully inserted\n  - errors (list): any parse/validation errors encountered","operationId":"submit_bulk_api_partner_submit_bulk_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/partner/stats":{"get":{"summary":"Partner Stats","description":"View partner's submission statistics.\n\nAuth: X-Partner-Key header\n\nReturns:\n  - total_records_submitted (int): cumulative records from all submissions\n  - active_records (int): non-expired records currently in availability table\n  - last_submission (str or null): ISO timestamp of last submission\n  - total_submissions (int): number of batches submitted\n  - queries_served (int): estimated queries that hit this partner's data","operationId":"partner_stats_api_partner_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/partner/health":{"get":{"summary":"Health Check","description":"Public health check for partner integration.\nNo authentication required.\n\nReturns:\n  - status (str): 'healthy' or 'degraded'\n  - api_version (str): current API version\n  - supported_programs (list): 26 loyalty programs accepting submissions","operationId":"health_check_api_partner_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/mcp/docs":{"get":{"summary":"Mcp Docs","description":"MCP documentation page - how to use the Award Flight Daily MCP.","operationId":"mcp_docs_mcp_docs_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/mcp/partners":{"get":{"summary":"Mcp Partners","description":"MCP partners page - airline loyalty program partnership information.","operationId":"mcp_partners_mcp_partners_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/partner-interest":{"get":{"summary":"Partner Interest Page","description":"Partner interest landing page. Was a dead link (404) from both CTA\nbuttons on /mcp/partners — flagged by Search Console as a 4xx.","operationId":"partner_interest_page_partner_interest_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/signup":{"get":{"summary":"Signup Redirect","description":"Signup is a modal in the React SPA — redirect users to the homepage\nwhere the 'Sign In' / 'Sign Up Free' button opens the auth modal.","operationId":"signup_redirect_signup_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/privacy":{"get":{"summary":"Privacy Page","description":"Privacy Policy page.","operationId":"privacy_page_privacy_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/terms":{"get":{"summary":"Terms Page","description":"Terms of Service page.","operationId":"terms_page_terms_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/admin/mcp/keys":{"get":{"summary":"Mcp Keys","description":"Get all MCP API keys from the mcp_api_keys table.\n\nQuery params:\n    active_only (bool, optional): If true, return only active keys\n\nReturns:\n    {\n        \"keys\": [\n            {\n                \"id\": integer,\n                \"key\": \"key_value\",\n                \"agent_id\": \"agent_xxx\",\n                \"tier\": \"free|pro|enterprise\",\n                \"active\": boolean,\n                \"created_at\": \"2024-01-01T00:00:00Z\"\n            },\n            ...\n        ]\n    }","operationId":"mcp_keys_api_admin_mcp_keys_get","parameters":[{"name":"active_only","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Active Only"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/mcp/keys/revoke":{"post":{"summary":"Mcp Keys Revoke","description":"Revoke (deactivate) an MCP API key.\n\nBody: { \"key\": \"key_id_or_value\" }\n\nReturns:\n    { \"success\": true, \"key\": \"...\", \"message\": \"Key revoked\" }","operationId":"mcp_keys_revoke_api_admin_mcp_keys_revoke_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/mcp/agents":{"get":{"summary":"Mcp Agents","description":"Get aggregated MCP agent statistics.\n\nReturns:\n    {\n        \"agents\": [\n            {\n                \"agent_id\": \"agent_xxx\",\n                \"tier\": \"free|pro|enterprise\",\n                \"total_queries\": integer,\n                \"revenue_cents\": integer,\n                \"first_seen\": \"2024-01-01T00:00:00Z\",\n                \"last_seen\": \"2024-01-01T00:00:00Z\",\n                \"favorite_tool\": \"tool_name\"\n            },\n            ...\n        ]\n    }","operationId":"mcp_agents_api_admin_mcp_agents_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/mcp/partners":{"get":{"summary":"Mcp Partners","description":"Get MCP partner information (API partner submissions, integrations, etc).\n\nNote: Partners table may not exist yet. Returns empty data gracefully.\n\nReturns:\n    {\n        \"recent_submissions\": [...],\n        \"active_partners\": [...],\n        \"stats\": {\n            \"total_submissions\": integer,\n            \"accepted\": integer,\n            \"pending\": integer\n        }\n    }","operationId":"mcp_partners_api_admin_mcp_partners_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/mcp/live":{"get":{"summary":"Mcp Live","description":"Get the most recent MCP events (live activity feed).\n\nQuery params:\n    limit (int, optional): Number of recent events to return (default 50, max 500)\n\nReturns:\n    {\n        \"events\": [\n            {\n                \"timestamp\": \"2024-01-01T00:00:00Z\",\n                \"agent_id\": \"agent_xxx\",\n                \"tool_name\": \"search_flights\",\n                \"response_time_ms\": 150,\n                \"tier\": \"pro\",\n                \"payment_cents\": 10,\n                \"success\": true\n            },\n            ...\n        ]\n    }","operationId":"mcp_live_api_admin_mcp_live_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/mcp/data-quality":{"get":{"summary":"Mcp Data Quality","description":"Get data quality metrics for award flight availability data.\n\nReturns:\n    {\n        \"summary\": {\n            \"total_records\": integer,\n            \"avg_freshness_score\": float,\n            \"total_sources\": integer\n        },\n        \"programs\": [\n            {\n                \"source\": \"airline_code\",\n                \"record_count\": integer,\n                \"last_updated\": \"2024-01-01T00:00:00Z\",\n                \"api_last_updated\": \"2024-01-01T00:00:00Z\",\n                \"earliest_date\": \"2024-01-01\",\n                \"latest_date\": \"2024-12-31\",\n                \"freshness_score\": integer (0-100)\n            },\n            ...\n        ]\n    }","operationId":"mcp_data_quality_api_admin_mcp_data_quality_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/mcp/query-details":{"get":{"summary":"Mcp Query Details","description":"Get detailed search query analytics from the main database.\n\nQuery params:\n    days (int, optional): Number of days to analyze (default 7)\n\nReturns:\n    {\n        \"total_searches\": integer,\n        \"direct_only_pct\": float (0-100),\n        \"period_days\": integer,\n        \"top_origins\": [{\"code\": \"JFK\", \"count\": 100}, ...],\n        \"top_destinations\": [{\"code\": \"LHR\", \"count\": 100}, ...],\n        \"top_routes\": [{\"route\": \"JFK-LHR\", \"count\": 50}, ...],\n        \"cabin_distribution\": {\"Y\": 200, \"W\": 50, \"J\": 30, \"F\": 10},\n        \"top_programs\": [{\"program\": \"united\", \"count\": 150}, ...],\n        \"searches_by_hour\": [{\"hour\": 0, \"count\": 10}, {\"hour\": 1, \"count\": 15}, ...]\n    }","operationId":"mcp_query_details_api_admin_mcp_query_details_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":7,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/mcp/health-v2":{"get":{"summary":"Mcp Health V2","description":"MCP server health check with corrected response shape.\n\nReturns:\n    {\n        \"status\": \"healthy\" | \"degraded\" | \"error\",\n        \"version\": \"1.0.0\",\n        \"db_size\": integer (bytes),\n        \"record_count\": integer,\n        \"timestamp\": \"2024-01-01T00:00:00Z\"\n    }","operationId":"mcp_health_v2_api_admin_mcp_health_v2_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/favorites":{"get":{"summary":"List Favorites","operationId":"list_favorites_api_favorites_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Create Favorite","operationId":"create_favorite_api_favorites_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/favorites/{fav_id}":{"put":{"summary":"Update Favorite","operationId":"update_favorite_api_favorites__fav_id__put","parameters":[{"name":"fav_id","in":"path","required":true,"schema":{"type":"string","title":"Fav Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Favorite","operationId":"delete_favorite_api_favorites__fav_id__delete","parameters":[{"name":"fav_id","in":"path","required":true,"schema":{"type":"string","title":"Fav Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/run-ingestion":{"post":{"summary":"Trigger Ingestion","description":"Trigger an ingestion run in the background.\nUses existing require_admin() from app.py for auth.\n\nJSON body (all optional):\n  sources:      list of program slugs (default: all programs)\n  max_budget:   max API calls (default: 900)\n  max_pages:    max pages per program (default: auto)\n  skip_gaps:    skip gap fill phase (default: false)\n  skip_refresh: skip rolling refresh phase (default: false)\n  start_date:   filter results from this date, e.g. \"2026-05-24\" (default: auto)\n  end_date:     filter results up to this date, e.g. \"2026-07-21\" (default: auto)\n  take:         rows per API page (default: 1000)\n\nReturns immediately with status. Poll /api/admin/ingestion-status for progress.","operationId":"trigger_ingestion_api_admin_run_ingestion_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/auto-ingestion":{"get":{"summary":"Auto Ingestion State","description":"Return whether automatic (scheduled) ingestion is paused.","operationId":"auto_ingestion_state_api_admin_auto_ingestion_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Set Auto Ingestion","description":"Pause or resume automatic (scheduled) ingestion. Body: {\"paused\": true|false}.\nPersists via a flag file on /data. Manual ingestion is unaffected.","operationId":"set_auto_ingestion_api_admin_auto_ingestion_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/ingestion-status":{"get":{"summary":"Ingestion Status","description":"Get current ingestion status and last result.","operationId":"ingestion_status_api_admin_ingestion_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/ingestion-runs":{"get":{"summary":"Ingestion Runs","description":"Get run history (newest first). Optional ?limit=N and ?type=<run_type>.\n\nWhen `type` is given (e.g. ingestion / alerts / prune) the runs are read by\ntype from admin.db, so the last N of that type are always available\nregardless of how many other runs happened in between (e.g. last 30 purges).","operationId":"ingestion_runs_api_admin_ingestion_runs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/ingestion-runs/{run_number}":{"get":{"summary":"Ingestion Run Detail","description":"Get full detail for a specific run including all steps and event log.","operationId":"ingestion_run_detail_api_admin_ingestion_runs__run_number__get","parameters":[{"name":"run_number","in":"path","required":true,"schema":{"type":"integer","title":"Run Number"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/db-status":{"get":{"summary":"Db Status","description":"Comprehensive database status: total rows, programs, dates, per-program breakdown.\nSimilar to `python -m backend.ingestion.run --status` output.","operationId":"db_status_api_admin_db_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/date-index/diagnose":{"post":{"summary":"Date Index Diagnose","description":"Read-only: past-date histogram + index-blindness probe. Safe anytime.","operationId":"date_index_diagnose_api_admin_date_index_diagnose_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/date-index/rebuild":{"post":{"summary":"Date Index Rebuild","description":"DROP + CREATE idx_avail_date, then CHECKPOINT — in a background thread,\nholding core._db_lock for the duration (site returns 503s briefly; run in\na quiet window). JSON body: {\"recreate\": true|false} (default true).\nPoll GET /api/admin/date-index/status.","operationId":"date_index_rebuild_api_admin_date_index_rebuild_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/date-index/status":{"get":{"summary":"Date Index Status","operationId":"date_index_status_api_admin_date_index_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/reindex-availability":{"post":{"summary":"Reindex Availability","description":"DROP + CREATE every recoverable secondary index on availability, with\nCHECKPOINTs between, in a background thread under core._db_lock. Durable fix\nfor the recurring checkpoint-FATAL index corruption. Site returns brief 503s\nwhile it runs (~1-2 min). Poll GET /api/admin/reindex-availability/status.","operationId":"reindex_availability_api_admin_reindex_availability_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/reindex-availability/status":{"get":{"summary":"Reindex Availability Status","operationId":"reindex_availability_status_api_admin_reindex_availability_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/full-refresh/status":{"get":{"summary":"Full Refresh Status","description":"Rotation progress: cycle #, cursor, units done/total, last night summary.","operationId":"full_refresh_status_api_admin_full_refresh_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/full-refresh/run-now":{"post":{"summary":"Full Refresh Run Now","description":"Manually kick one night of the rotation (same as the 01:00 UTC job).\nJSON body (optional): {\"budget\": N} to override the nightly budget —\nhandy for smoke tests without burning the day's quota.","operationId":"full_refresh_run_now_api_admin_full_refresh_run_now_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/full-refresh/config":{"post":{"summary":"Full Refresh Config","description":"Persist the nightly budget used by the 01:00 UTC rotation.\nJSON body: {\"nightly_budget\": 2000} — sticks until changed again\n(stored in the state file, survives restarts/deploys).\n{\"nightly_budget\": null} clears the override back to the env default.","operationId":"full_refresh_config_api_admin_full_refresh_config_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/full-refresh/replan":{"post":{"summary":"Full Refresh Replan","description":"Discard the current cycle plan; the next run replans from fresh row\ncounts + staleness (need-order). Use after big schema/coverage changes.","operationId":"full_refresh_replan_api_admin_full_refresh_replan_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/run-archive":{"post":{"summary":"Trigger Archive","description":"Trigger weekly archive: move past-date and expired rows to historical DB.\n\nJSON body (all optional):\n  cutoff_date:    archive rows before this date (default: today)\n  rotate_backups: rotate historical.duckdb backups first (default: true)\n\nReturns immediately. Poll /api/admin/archive-status for progress.","operationId":"trigger_archive_api_admin_run_archive_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/archive-status":{"get":{"summary":"Archive Status","description":"Get current archive status and last result.","operationId":"archive_status_api_admin_archive_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/historical-stats":{"get":{"summary":"Historical Stats","description":"Get summary stats from the historical DB.","operationId":"historical_stats_api_admin_historical_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/archive/run-nightly":{"post":{"summary":"Trigger Nightly Prune","description":"Trigger the nightly prune: DELETE rows where date < today OR expired_at IS NOT NULL.\n\nJSON body (all optional):\n  cutoff_date: prune rows with date < this. Default: today (UTC).\n  max_rows:    hard cap on rows deleted in this run. Default: 5,000,000.\n\nReturns immediately; runs in a background thread.\nPoll /api/admin/archive/status for progress.","operationId":"trigger_nightly_prune_api_admin_archive_run_nightly_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/archive/status":{"get":{"summary":"Nightly Prune Status","description":"Current nightly-prune status + last result.","operationId":"nightly_prune_status_api_admin_archive_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/archive/catchup":{"post":{"summary":"Trigger Catchup Prune","description":"Catch-up prune for when nightly missed several days (e.g. after a restart).\nSame as run-nightly but max_rows defaults higher so the whole backlog gets\ncleared in one call. Use sparingly — this can briefly elevate write lock\ncontention if the backlog is large.\n\nJSON body (all optional):\n  cutoff_date: default today (UTC)\n  max_rows:    default 10,000,000 (twice the normal cap)","operationId":"trigger_catchup_prune_api_admin_archive_catchup_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/backup-db":{"post":{"summary":"Backup Db","description":"Manual backup rotation for the live DB.\nRotates: .prev4 ← .prev3 ← .prev2 ← .prev1 ← current (copy)\nKeeps 5 copies total. Runs automatically before each ingestion.","operationId":"backup_db_api_admin_backup_db_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/refresh-db-connection":{"post":{"summary":"Refresh Db Conn","description":"Force the app's shared DB connection to reconnect, picking up writes\nmade by the ingestion worker's separate connection.","operationId":"refresh_db_conn_api_admin_refresh_db_connection_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/backup-copy-prev1":{"post":{"summary":"Backup Copy Prev1","description":"Copy current DB to .prev1 only — no rotation of other backups.","operationId":"backup_copy_prev1_api_admin_backup_copy_prev1_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/backup-wal":{"post":{"summary":"Backup Wal","description":"Snapshot the live WAL to a timestamped copy — fast (tens of MB), no big copy.\n\nWAL-ONLY by design (ops choice 2026-06-13): preserves recent un-checkpointed\nupdates before a risky op or restart. IMPORTANT caveat surfaced in the\nresponse: a WAL snapshot is only restorable PAIRED with the main DB file as\nit stood at snapshot time, so we record the main file's size + mtime as the\npairing reference. If the main file is later checkpointed/changed, the\nsnapshot may no longer apply cleanly — for a fully self-sufficient backup use\n/api/admin/backup-db (full rotation) instead.","operationId":"backup_wal_api_admin_backup_wal_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/backup-status":{"get":{"summary":"Backup Status","description":"Show backup file status for both live and historical DBs, including flight counts.","operationId":"backup_status_api_admin_backup_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/run-alerts":{"post":{"summary":"Trigger Alerts","description":"Trigger alerts + transfer bonus scrape in the background.\nRuns the same pipeline as the old v2-alerts.yml GitHub Action.\n\nJSON body (all optional):\n  skip_bonuses:  skip transfer bonus scraping (default: false)\n  dry_run:       search but don't send emails (default: false)\n\nReturns immediately. Poll /api/admin/alerts-status for progress.","operationId":"trigger_alerts_api_admin_run_alerts_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/alerts-status":{"get":{"summary":"Alerts Status","description":"Get current alerts run status and last result.","operationId":"alerts_status_api_admin_alerts_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/export-availability-delta":{"post":{"summary":"Export Availability Delta","description":"Compare live availability against the prev1 backup; dump the delta to parquet.","operationId":"export_availability_delta_api_admin_export_availability_delta_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/restore-availability-from-parquet":{"post":{"summary":"Restore Availability From Parquet","description":"Re-insert availability rows from a parquet file. INSERT OR IGNORE handles dups via PK.","operationId":"restore_availability_from_parquet_api_admin_restore_availability_from_parquet_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/move-wal-aside":{"post":{"summary":"Move Wal Aside","description":"One-shot helper: rename live WAL out of the way. Restart container after.","operationId":"move_wal_aside_api_admin_move_wal_aside_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}