import pkg from '@/package.json'; // OpenAPI 3.1 document for the app's public JSON API, kept in code so it can be // served machine-readable at /api/openapi (see AGENTS.md "## API"). Must be updated // whenever an endpoint changes. export const openApiSpec = { openapi: '3.1.0', info: { title: 'Flight Ops API', description: 'JSON API for Flight Ops — EU airport data for the map, the live game-state snapshot, player actions (accept / dispatch / buy / sell / refuel), and app metadata. Every action endpoint ticks the sim clock first and returns the full state snapshot on success.', // Sourced from package.json so it stays in sync with every version bump. version: pkg.version, }, // Empty url = current origin (the spec is served from the same host that runs the API). servers: [{ url: '' }], paths: { '/api/airports': { get: { summary: 'List EU airports', description: 'All fixed-wing EU airports (large, medium and seaplane bases) as a GeoJSON FeatureCollection.', operationId: 'listAirports', responses: { 200: { description: 'GeoJSON FeatureCollection of EU airports', content: { 'application/json': { schema: { $ref: '#/components/schemas/AirportFeatureCollection' }, }, }, }, }, }, }, '/api/state': { get: { summary: 'Game state snapshot', description: 'Full current game state for client polling — account, sim clock, planes and contracts. Tick-on-read: each call first advances the sim clock by (real time since last read) × timeScale (clamped per tick), so expiry/finalization/spawn are always applied before the snapshot is returned.', operationId: 'getState', responses: { 200: { description: 'Current game state', content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' }, }, }, }, }, }, }, '/api/sim': { post: { summary: 'Set sim time controls', description: 'Advance the sim clock (tick-on-read) and set the time scale / pause state. The clock is advanced first, so the returned simState already reflects current game time; contract expiry, arrival finalization and market spawn run inside that tick.', operationId: 'setSimControls', requestBody: { required: false, content: { 'application/json': { schema: { $ref: '#/components/schemas/SimControlsRequest' }, }, }, }, responses: { 200: { description: 'Updated sim state', content: { 'application/json': { schema: { $ref: '#/components/schemas/SimState' }, }, }, }, 400: { description: 'Invalid body (not an object, unknown field, or wrong types)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, '/api/version': { get: { summary: 'App version', description: 'The running app version, read from package.json.', operationId: 'getVersion', responses: { 200: { description: 'Current app version', content: { 'application/json': { schema: { $ref: '#/components/schemas/AppVersion' }, }, }, }, }, }, }, '/api/contracts/{id}/accept': { post: { summary: 'Accept a contract', description: 'Claim an open contract: status flips open → accepted (v0.8.0). The sim clock is ticked first, so the returned snapshot reflects current game time. No body.', operationId: 'acceptContract', parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Accepted — full state snapshot (contract now "accepted")', content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } }, }, 404: { description: 'Unknown contract id', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, 409: { description: 'Contract is not "open" (accepted / completed / expired)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, }, }, }, '/api/planes/{planeId}/dispatch': { post: { summary: 'Dispatch a plane on an accepted contract', description: 'Bind an ACCEPTED contract to a grounded plane at its origin (v0.8.0). The plane becomes "flying" with departureSimTime = current sim clock; the engine then flies it analytically and pays out reward + reputation on arrival. Validation: contract must be "accepted", plane "available" and located at the contract origin, with enough fuel for the leg (burnPerKm × great-circle km).', operationId: 'dispatchPlane', parameters: [ { name: 'planeId', in: 'path', required: true, schema: { type: 'string' } }, ], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/DispatchRequest' }, }, }, }, responses: { 200: { description: 'Dispatched — full state snapshot (plane "flying", departure set)', content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } }, }, 400: { description: 'Invalid body (not an object, unknown field, or missing/non-string contractId)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, 404: { description: 'Unknown plane id or contract id', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, 409: { description: 'Contract not "accepted", plane not "available", plane not at the origin, or insufficient fuel for the leg', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, }, }, }, '/api/market/buy-plane': { post: { summary: 'Buy a plane from the market', description: 'Buy an aircraft at its full catalog price (v0.8.0). The new plane arrives "available" with a full tank at the home base (EDDF). Only models in the in-code catalog are known.', operationId: 'buyPlane', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/BuyPlaneRequest' }, }, }, }, responses: { 200: { description: 'Purchased — full state snapshot (new plane, cash reduced)', content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } }, }, 400: { description: 'Invalid body or unknown catalog model', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, 409: { description: 'Insufficient funds', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, }, }, }, '/api/market/sell-plane': { post: { summary: 'Sell a plane on the market', description: 'Sell a grounded plane for 50% of its catalog price (v0.8.0). The row is deleted and the payout added to cash. A flying plane cannot be sold, nor can the last remaining plane.', operationId: 'sellPlane', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/SellPlaneRequest' }, }, }, }, responses: { 200: { description: 'Sold — full state snapshot (plane gone, cash increased by the payout)', content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } }, }, 400: { description: 'Invalid body (not an object, unknown field, or missing/non-string planeId)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, 404: { description: 'Unknown plane id', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, 409: { description: 'Plane is flying, or it is the last remaining plane', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, }, }, }, '/api/planes/{planeId}/refuel': { post: { summary: 'Refuel a grounded plane', description: 'Fill a grounded plane to maxFuel (v0.8.0). Cost = (maxFuel − fuel) × fuel price per liter, deducted from cash; no body.', operationId: 'refuelPlane', parameters: [ { name: 'planeId', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Refueled — full state snapshot (fuel at max, cash reduced by the cost)', content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } }, }, 404: { description: 'Unknown plane id', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, 409: { description: 'Plane is flying, or cash is insufficient for the fill-up', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, }, }, }, }, components: { schemas: { AppVersion: { type: 'object', properties: { version: { type: 'string', description: 'Semver of the running build' }, }, required: ['version'], }, AirportFeatureCollection: { type: 'object', properties: { type: { const: 'FeatureCollection' }, features: { type: 'array', items: { $ref: '#/components/schemas/AirportFeature' }, }, }, required: ['type', 'features'], }, AirportFeature: { type: 'object', properties: { type: { const: 'Feature' }, geometry: { $ref: '#/components/schemas/PointGeometry' }, properties: { $ref: '#/components/schemas/AirportProperties' }, }, required: ['type', 'geometry', 'properties'], }, PointGeometry: { type: 'object', properties: { type: { const: 'Point' }, coordinates: { type: 'array', description: '[longitude, latitude]', items: { type: 'number' }, minItems: 2, maxItems: 2, }, }, required: ['type', 'coordinates'], }, AirportProperties: { type: 'object', properties: { name: { type: 'string' }, icao: { type: 'string' }, lat: { type: 'number' }, lon: { type: 'number' }, }, required: ['name', 'icao', 'lat', 'lon'], }, StateSnapshot: { type: 'object', properties: { account: { $ref: '#/components/schemas/AccountState' }, simState: { $ref: '#/components/schemas/SimState' }, planes: { type: 'array', items: { $ref: '#/components/schemas/PlaneState' }, }, contracts: { type: 'array', items: { $ref: '#/components/schemas/ContractState' }, }, }, required: ['account', 'simState', 'planes', 'contracts'], }, AccountState: { type: 'object', properties: { cash: { type: 'number' }, reputation: { type: 'number' }, }, required: ['cash', 'reputation'], }, SimState: { type: 'object', properties: { clockMs: { type: 'integer', description: 'Sim-clock epoch ms (game time)' }, timeScale: { type: 'number', minimum: 1, maximum: 64 }, paused: { type: 'boolean' }, }, required: ['clockMs', 'timeScale', 'paused'], }, SimControlsRequest: { type: 'object', description: 'Partial update — omit a field to leave it unchanged.', properties: { timeScale: { type: 'number', minimum: 1, maximum: 64, description: 'Game-time multiplier; values outside [1, 64] are clamped. The UI offers the power-of-two ladder 1/2/4/8/16/32/64.', }, paused: { type: 'boolean' }, }, }, DispatchRequest: { type: 'object', properties: { contractId: { type: 'string', description: 'An ACCEPTED contract whose origin the plane is at.' }, }, required: ['contractId'], }, BuyPlaneRequest: { type: 'object', properties: { model: { type: 'string', description: 'Catalog model id (e.g. "atr72").' }, }, required: ['model'], }, SellPlaneRequest: { type: 'object', properties: { planeId: { type: 'string', description: 'A grounded ("available") plane that is not the last remaining one.' }, }, required: ['planeId'], }, Error: { type: 'object', properties: { error: { type: 'string' }, }, required: ['error'], }, PlaneState: { type: 'object', properties: { id: { type: 'string' }, model: { type: 'string' }, status: { enum: ['available', 'flying'] }, fuel: { type: 'number' }, maxFuel: { type: 'number' }, locationIcao: { type: 'string' }, locationName: { type: 'string' }, contractId: { type: ['string', 'null'] }, departureSimTime: { type: ['integer', 'null'], description: 'Sim-clock epoch ms of the current flight (set by dispatch); null when grounded.', }, lat: { type: 'number' }, lon: { type: 'number' }, }, required: [ 'id', 'model', 'status', 'fuel', 'maxFuel', 'locationIcao', 'locationName', 'contractId', 'departureSimTime', 'lat', 'lon', ], }, ContractState: { type: 'object', properties: { id: { type: 'string' }, originIcao: { type: 'string' }, originName: { type: 'string' }, destIcao: { type: 'string' }, destName: { type: 'string' }, reward: { type: 'number' }, cargoType: { type: 'string' }, cargoCount: { type: 'integer' }, deadlineSimTime: { type: 'integer', description: 'Sim-clock epoch ms (game time)' }, // 'expired' = an open contract whose deadline passed; accepted/in-flight contracts never expire. status: { enum: ['open', 'accepted', 'completed', 'expired'] }, planeId: { type: ['string', 'null'] }, createdSimTime: { type: 'integer' }, }, required: [ 'id', 'originIcao', 'originName', 'destIcao', 'destName', 'reward', 'cargoType', 'cargoCount', 'deadlineSimTime', 'status', 'planeId', 'createdSimTime', ], }, }, }, };