274 lines
8.4 KiB
TypeScript
274 lines
8.4 KiB
TypeScript
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, and app metadata.',
|
||
// 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' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
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: 60 },
|
||
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: 60,
|
||
description: 'Game-time multiplier; values outside [1, 60] are clamped.',
|
||
},
|
||
paused: { type: 'boolean' },
|
||
},
|
||
},
|
||
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'] },
|
||
lat: { type: 'number' },
|
||
lon: { type: 'number' },
|
||
},
|
||
required: [
|
||
'id',
|
||
'model',
|
||
'status',
|
||
'fuel',
|
||
'maxFuel',
|
||
'locationIcao',
|
||
'locationName',
|
||
'contractId',
|
||
'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',
|
||
],
|
||
},
|
||
},
|
||
},
|
||
};
|