openapi: 3.1.0
info:
  title: Scintilla Locate API
  version: 1.0.0
  description: >
    REST service that periodically fetches mixed (multi-GNSS) RINEX NAV files,
    parses broadcast ephemerides for GPS (G), Galileo (E), BeiDou (C), and QZSS (J),
    and serves the "best ephemeris per PRN" for a requested epoch.

    Selection method: for each PRN, choose the record with maximal Toe such that
    Toe <= requested time (t) and (t - Toe) <= window_sec, where all times are in
    the constellation's native system week/time-of-week.
servers:
  - url: http://localhost:8080
tags:
  - name: Health
  - name: Ephemerides
paths:
  /v1/health:
    get:
      tags: [Health]
      summary: Service health and current cache status
      operationId: getHealth
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
  /v1/ephemerides:
    get:
      tags: [Ephemerides]
      summary: Best ephemeris per PRN (optionally filtered by constellations)
      operationId: getEphemerides
      parameters:
        - $ref: "#/components/parameters/TParam"
        - $ref: "#/components/parameters/ConstellationsParam"
        - $ref: "#/components/parameters/WindowSecParam"
      responses:
        "200":
          description: OK
          headers:
            ETag:
              description: Strong ETag for (source checksum + selection inputs).
              schema: { type: string }
            Cache-Control:
              description: Cache hints for clients.
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EphemerisSetResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "503":
          $ref: "#/components/responses/NoData"
  /v1/ephemerides/{prn}:
    get:
      tags: [Ephemerides]
      summary: Best ephemeris for a specific PRN
      operationId: getEphemerisByPrn
      parameters:
        - $ref: "#/components/parameters/PrnPath"
        - $ref: "#/components/parameters/TParam"
        - $ref: "#/components/parameters/WindowSecParam"
      responses:
        "200":
          description: OK
          headers:
            ETag:
              description: Strong ETag for (source checksum + selection inputs).
              schema: { type: string }
            Cache-Control:
              description: Cache hints for clients.
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SingleEphemerisResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          description: No valid ephemeris within window for this PRN
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                no_valid_ephemeris:
                  value:
                    error: NO_VALID_EPHEMERIS
                    message: no ephemeris within selection window
        "503":
          $ref: "#/components/responses/NoData"
  /v1/constellations/{constellation}/ephemerides:
    get:
      tags: [Ephemerides]
      summary: Best ephemeris per PRN for one constellation
      operationId: getEphemeridesByConstellation
      parameters:
        - $ref: "#/components/parameters/ConstellationPath"
        - $ref: "#/components/parameters/TParam"
        - $ref: "#/components/parameters/WindowSecParam"
      responses:
        "200":
          description: OK
          headers:
            ETag:
              description: Strong ETag for (source checksum + selection inputs).
              schema: { type: string }
            Cache-Control:
              description: Cache hints for clients.
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EphemerisSetResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "503":
          $ref: "#/components/responses/NoData"
components:
  parameters:
    PrnPath:
      name: prn
      in: path
      required: true
      description: Satellite PRN in RINEX form (e.g. G01, E11, C19, J02).
      schema:
        type: string
        pattern: "^[GECJ][0-9]{2}$"
    ConstellationPath:
      name: constellation
      in: path
      required: true
      description: Constellation identifier.
      schema:
        type: string
        enum: ["G", "E", "C", "J"]
    TParam:
      name: t
      in: query
      required: false
      description: Target epoch in UTC (RFC3339). Defaults to server current time.
      schema:
        type: string
        format: date-time
      example: "2025-12-14T00:00:00Z"
    ConstellationsParam:
      name: constellations
      in: query
      required: false
      description: Comma-separated list of constellation letters (e.g. G,E,C,J). Defaults to server configuration.
      schema:
        type: string
      example: "G,E"
    WindowSecParam:
      name: window_sec
      in: query
      required: false
      description: Selection window in seconds. Records with (t - Toe) > window_sec are ignored. Defaults to server configuration.
      schema:
        type: integer
        minimum: 0
      example: 14400
  responses:
    BadRequest:
      description: Invalid request parameters.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            bad_request:
              value:
                error: BAD_REQUEST
                message: invalid window_sec
    NoData:
      description: Cache not yet populated.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            no_data:
              value:
                error: NO_DATA
                message: ephemeris cache not yet populated
  schemas:
    ErrorResponse:
      type: object
      additionalProperties: false
      required: [error, message]
      properties:
        error: { type: string, description: Machine-readable error code. }
        message: { type: string, description: Human-readable error message. }
        details:
          type: object
          additionalProperties: true
          description: Optional structured details.
    SourceInfo:
      type: object
      additionalProperties: false
      required: [uri, fetched_at_utc, checksum_sha256]
      properties:
        uri: { type: string }
        fetched_at_utc: { type: string, format: date-time }
        checksum_sha256:
          type: string
          pattern: "^[a-f0-9]{64}$"
    Warning:
      type: object
      additionalProperties: false
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }
    SelectionStats:
      type: object
      additionalProperties: false
      required: [records_returned, unique_prns, dropped_prns]
      properties:
        records_returned: { type: integer, minimum: 0 }
        unique_prns: { type: integer, minimum: 0 }
        dropped_prns: { type: integer, minimum: 0 }
    Satellite:
      type: object
      additionalProperties: false
      required:
        [prn, week, toe, toc_week, toc, af0, af1, af2, delta_n, sqrt_a, e, m0, omega0, omega, i0, omega_dot, idot, cuc, cus, crc, crs, cic, cis, health]
      properties:
        prn: { type: string, pattern: "^[GECJ][0-9]{2}$" }
        week: { type: integer }
        toe: { type: number }
        toc_week: { type: integer }
        toc: { type: number }
        af0: { type: number }
        af1: { type: number }
        af2: { type: number }
        delta_n: { type: number }
        sqrt_a: { type: number }
        e: { type: number }
        m0: { type: number }
        omega0: { type: number }
        omega: { type: number }
        i0: { type: number }
        omega_dot: { type: number }
        idot: { type: number }
        cuc: { type: number }
        cus: { type: number }
        crc: { type: number }
        crs: { type: number }
        cic: { type: number }
        cis: { type: number }
        health: { type: number }
    HealthResponse:
      type: object
      additionalProperties: false
      required: [status, time_utc, version, source]
      properties:
        status: { type: string, enum: ["ok"] }
        time_utc: { type: string, format: date-time }
        version: { type: string }
        source: { $ref: "#/components/schemas/SourceInfo" }
    EphemerisSetResponse:
      type: object
      additionalProperties: false
      required: [epoch_utc, source, constellations, selection, ephemerides, stats]
      properties:
        epoch_utc: { type: string, format: date-time }
        source: { $ref: "#/components/schemas/SourceInfo" }
        constellations:
          type: array
          items: { type: string, enum: ["G","E","C","J"] }
        selection:
          type: object
          additionalProperties: true
        ephemerides:
          type: array
          items: { $ref: "#/components/schemas/Satellite" }
        stats: { $ref: "#/components/schemas/SelectionStats" }
        warnings:
          type: array
          items: { $ref: "#/components/schemas/Warning" }
    SingleEphemerisResponse:
      type: object
      additionalProperties: false
      required: [epoch_utc, prn, ephemeris]
      properties:
        epoch_utc: { type: string, format: date-time }
        prn: { type: string, pattern: "^[GECJ][0-9]{2}$" }
        ephemeris: { $ref: "#/components/schemas/Satellite" }
