openapi: 3.1.0
info:
  title: Veðrið API
  version: 2.0.2-release
  summary: Contract-first API for Icelandic weather stations, observations, operator visibility,
    and day-one mobile platform resources.
  description: 'OpenAPI 3.1 contract for the Veðrið backend.


    ## Scope


    - `/v2` is the forward-looking public contract.

    - Public weather data is read-only and does not require authentication.

    - Day-one platform resources use bearer authentication after bootstrap.

    - The backend is designed for a Go + Postgres architecture.

    - Latitude and longitude are mandatory for user-owned location resources so the
    app can stay location-native even when a station is resolved later.


    ## Implementation notes


    This backend is intentionally `/v2`-only.

    Any legacy path translation belongs outside this project.

    Health endpoints `/healthz` and `/readyz` use the standard `{ data: ... }`
    response envelope so operational probes and `/v2` resources share one JSON
    shape.

    JSON, YAML, HTML, and plain-text responses honor `Accept-Encoding: gzip`
    when clients request it so mobile and polling surfaces can save bandwidth
    without changing response semantics.

    This document defines the contract and schema quality bar for the service implemented
    here.

    '
  x-llms-txt: /llms.txt
servers:
- url: http://localhost:8080
  description: Local development
- url: https://beta-api.heima.oli.is
  description: Development / beta server
- url: https://api.vedrid.is
  description: Production
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
tags:
- name: Health
- name: Seifur
- name: Stations
- name: Stats
- name: Alerts
- name: Observations
- name: ErrorLogs
- name: Auth
- name: Users
- name: Subscriptions
- name: Devices
- name: Widgets
- name: Notifications
- name: Usage
- name: Roads
- name: Sources
paths:
  /llms.txt:
    get:
      tags:
      - Health
      summary: AI usage documentation
      operationId: getLLMsTxt
      responses:
        '200':
          description: Human and agent friendly API summary in plain text.
          content:
            text/plain:
              schema:
                type: string

  /healthz:
    get:
      tags:
      - Health
      summary: Liveness probe
      operationId: getHealthz
      responses:
        '200':
          description: Service is alive.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthStatusResponse'
  /readyz:
    get:
      tags:
      - Health
      summary: Readiness probe
      operationId: getReadyz
      responses:
        '200':
          description: Service is ready to receive traffic.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadinessStatusResponse'
        '503':
          description: Service is not ready to receive traffic.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadinessStatusResponse'
  /v2/version:
    get:
      tags:
      - Health
      summary: Build and version metadata
      description: Returns the backend build/version metadata embedded in the running binary so operators and clients can see which revision is currently deployed.
      operationId: getVersionV2
      responses:
        '200':
          description: Running backend version metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VersionResponse'
  /v2/seifur:
    get:
      tags:
      - Seifur
      summary: Seifur namespace overview
      description: Returns the current high-level Seifur state and snapshot metadata. Current beta target is real `wind` + nested `gust` + `temp`; `rain` is intentionally left as metadata-only and treated as a later follow-up rather than part of the served beta slice.
      operationId: getSeifurOverviewV2
      responses:
        '200':
          description: Current Seifur overview state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeifurOverviewResponse'
  /v2/seifur/health:
    get:
      tags:
      - Seifur
      summary: Seifur route health probe
      description: Lightweight probe for the additive Seifur namespace route wiring.
      operationId: getSeifurHealthV2
      responses:
        '200':
          description: Seifur namespace is wired and responding.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthStatusResponse'
  /v2/seifur/field/now:
    get:
      tags:
      - Seifur
      summary: Seifur current field slice for the requested viewport
      description: Returns the current snapshot-backed Seifur field slice for the requested viewport. Real interpolation currently exposes sustained wind vectors and air temperature. Wind cells blend current station observations over a preferred model-backed Open-Meteo GFS background field, with station influence tapering over roughly 150 km before the background dominates farther offshore. The runtime fails fast to a synthetic background fallback when the external model source is slow or unavailable so `u/v` coverage stays non-null across the supported extent. The wind payload may also include partial gust intensity metadata derived from current non-stale gust-reporting stations. Gust is a wind-band modifier, not a separate flow field, and gust cells may be null where sustained wind cells are available. Rain remains metadata-only and is intentionally left out of the served beta field slice for now. Response coverage metadata distinguishes requested, available, served, and per-band null-heavy behavior for this specific field response.
      operationId: getSeifurFieldNowV2
      parameters:
      - in: query
        name: bbox
        required: true
        description: Bounding box as minLon,minLat,maxLon,maxLat.
        schema:
          type: string
          pattern: ^\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*$
      - in: query
        name: zoom
        required: true
        description: Requested map zoom used for future field density selection.
        schema:
          type: integer
          minimum: 0
          maximum: 22
      - in: query
        name: width
        required: false
        description: Requested output width. Defaults to 64. Width and height are jointly bounded to at most 4096 cells, except for the canonical wide 128x72 national shape.
        schema:
          type: integer
          minimum: 1
          maximum: 128
          default: 64
      - in: query
        name: height
        required: false
        description: Requested output height. Defaults to 64. Width and height are jointly bounded to at most 4096 cells, except for the canonical wide 128x72 national shape.
        schema:
          type: integer
          minimum: 1
          maximum: 128
          default: 64
      - in: query
        name: bands
        required: false
        description: Optional comma-separated requested bands. Defaults to wind,temp,rain.
        schema:
          type: string
          example: wind,temp,rain
      responses:
        '200':
          description: Snapshot-backed current field slice for the requested viewport. Real interpolation currently exposes sustained wind vectors and air temperature. Wind may include partial gust intensity metadata with narrower availability than sustained wind; requested `rain` remains metadata-only and intentionally unserved in the current beta slice.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeifurFieldNowResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          description: Seifur snapshot metadata is currently unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v2/seifur/wind/texture:
    get:
      tags:
      - Seifur
      summary: PNG wind texture for the requested viewport
      description: Returns an image/png texture derived from the current Seifur wind field. Texture encoding uses R=u normalized, G=v normalized, and B=speed normalized. Row 0 is the northernmost row in the requested bbox. The texture is generated from the same runtime wind blend used by `/v2/seifur/field/now`, including the preferred Open-Meteo GFS background path and its fast synthetic fallback. Wide national-like requests are generated from the canonical `128x72` backend wind field and then resampled back to the requested PNG size so callers keep a stable smaller texture contract without falling back to an excessively coarse wide-area raster. Response headers expose the normalization ranges and snapshot timestamp used for the texture.
      operationId: getSeifurWindTextureV2
      parameters:
      - in: query
        name: bbox
        required: true
        description: Bounding box as minLon,minLat,maxLon,maxLat.
        schema:
          type: string
          pattern: ^\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*$
      - in: query
        name: width
        required: true
        description: Requested output width. Width and height are jointly bounded to at most 4096 cells, except for the canonical wide 128x72 national shape.
        schema:
          type: integer
          minimum: 1
          maximum: 128
      - in: query
        name: height
        required: true
        description: Requested output height. Width and height are jointly bounded to at most 4096 cells, except for the canonical wide 128x72 national shape.
        schema:
          type: integer
          minimum: 1
          maximum: 128
      responses:
        '200':
          description: PNG wind texture for the requested viewport.
          headers:
            X-Wind-U-Range:
              schema:
                type: string
            X-Wind-V-Range:
              schema:
                type: string
            X-Wind-Speed-Range:
              schema:
                type: string
            X-Bbox:
              schema:
                type: string
            X-Timestamp:
              schema:
                type: string
                format: date-time
          content:
            image/png:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          description: Seifur wind field is currently unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v2/seifur/debug/influence:
    get:
      tags:
      - Seifur
      summary: Seifur debug influence metadata for one point
      description: Returns bounded current-snapshot selector metadata for one Seifur point. For currently served bands (`wind`, `temp`), selected contributors reuse the exact field interpolation selector; stale latest readings still appear only as explicit exclusions. Rain stays on the metadata-only side of the contract and is not treated as a served debug/interpolation band. This route intentionally does not expose interpolated values or point-level station weights.
      operationId: getSeifurDebugInfluenceV2
      parameters:
      - in: query
        name: lat
        required: true
        description: Latitude of the point to explain.
        schema:
          $ref: '#/components/schemas/Latitude'
      - in: query
        name: lon
        required: true
        description: Longitude of the point to explain.
        schema:
          $ref: '#/components/schemas/Longitude'
      - in: query
        name: bands
        required: false
        description: Optional comma-separated requested bands. Defaults to wind,temp,rain.
        schema:
          type: string
          example: wind,temp,rain
      responses:
        '200':
          description: Bounded current-snapshot selector metadata for Seifur influence explainability. For currently served bands (`wind`, `temp`), selected contributors match the exact field interpolation selector; point-level weights are not included.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeifurDebugInfluenceResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          description: Seifur snapshot metadata is currently unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v2/seifur/stations:
    get:
      tags:
      - Seifur
      summary: Seifur visible stations in a bbox
      description: |
        Discovery surface only.
        Seifur station discovery uses the same 24-hour usable-station rule as station catalog/nearest discovery: the canonical latest reading must include wind speed, wind direction degrees, and temperature.
      operationId: listSeifurStationsV2
      parameters:
      - in: query
        name: bbox
        required: true
        description: Bounding box as minLon,minLat,maxLon,maxLat.
        schema:
          type: string
          pattern: ^\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*$
      - in: query
        name: sources
        required: false
        description: Optional comma-separated source filter.
        schema:
          type: string
          example: vegagerdin,vedur.is,ambientweather
      responses:
        '200':
          description: Active stations inside the bbox with latest reading subset when available.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeifurStationListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
  /v2/stations:
    get:
      tags:
      - Stations
      summary: List stations
      description: |
        Discovery surface only.
        This endpoint hides stations whose canonical latest reading is older than 24 hours or missing any of: wind speed, wind direction degrees, or temperature.
        Explicit station detail/latest endpoints remain reachable and are not filtered by this discovery visibility rule.
      operationId: listStationsV2
      parameters:
      - $ref: '#/components/parameters/LimitParam'
      - $ref: '#/components/parameters/CursorParam'
      - in: query
        name: region
        description: Optional region filter.
        schema:
          type: string
          minLength: 1
          maxLength: 80
      - in: query
        name: source
        description: Optional upstream source identifier for filtering the canonical merged station directory.
        schema:
          type: string
          minLength: 1
          maxLength: 64
      - in: query
        name: isActive
        description: Filter by active/inactive station state.
        schema:
          type: boolean
      - in: query
        name: lat
        description: Optional latitude for distance-aware sorting. Must be sent with
          `lon`. Without coordinates, results default to Icelandic station-name ordering.
        schema:
          $ref: '#/components/schemas/Latitude'
      - in: query
        name: lon
        description: Optional longitude for distance-aware sorting. Must be sent with
          `lat`. Without coordinates, results default to Icelandic station-name ordering.
        schema:
          $ref: '#/components/schemas/Longitude'
      responses:
        '200':
          description: Paginated station list ordered by Icelandic station-name collation by default, or by nearest distance when `lat` + `lon` are supplied.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StationListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
  /v2/stats/stations:
    get:
      tags:
      - Stats
      summary: Get station summary statistics
      description: Returns aggregate counts for the station catalog currently exposed on the public API, including freshness buckets and per-source totals.
      operationId: getStationStatsV2
      responses:
        '200':
          description: Aggregate public station freshness, source counts, and recent-update windows.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StationStatsResponse'
  /v2/roads/report:
    get:
      tags:
      - Roads
      summary: Resolve one active road status report
      description: Resolves a user road query against the current persisted road-status inventory, preferring matched active road-condition alerts and falling back to direct Vegagerðin segment status when alert matching is sparse. The best match is enriched with linked station weather summaries plus compact same-corridor adjacent road context and nearby connected-road status. This is still a compact persisted report surface built on structured data, not a full road-catalog search or route engine.
      operationId: getRoadReportV2
      parameters:
      - in: query
        name: q
        required: false
        schema:
          type: string
          minLength: 1
          maxLength: 200
      - in: query
        name: query
        required: false
        deprecated: true
        description: Legacy alias for `q`. If both are provided, `q` wins.
        schema:
          type: string
          minLength: 1
          maxLength: 200
      responses:
        '200':
          description: Best active road report match for the supplied query.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoadReportResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
  /v2/drive/context:
    post:
      tags:
      - Roads
      summary: Compute guest-safe drive context
      description: Returns a first-cut guest-safe driving context derived from the caller's live coordinates plus the current road-aware station, alert, and road-status inventory. This is an honest nearest-station/nearby-context slice, not a full route engine. The response now includes a dedicated `windRisk` semantic derived from the nearest current-road weather station evidence plus the effective gust threshold and vehicle profile, while `roadStatus`, `alerts`, and `liveStatus` remain direct/local road truth and bounded nearby context. `guestModeAllowed` and `subscriptionTierRequired` make the current free baseline and future route-feature gating explicit without requiring clients to infer policy from missing features.
      operationId: postDriveContextV2
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DriveContextRequest'
      responses:
        '200':
          description: Computed guest-safe drive context.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DriveContextResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
  /v2/drive/route-context:
    post:
      tags:
      - Roads
      summary: Compute subscriber-gated route context
      description: 'Returns the current bounded Ferð route-context response for subscribed users. This endpoint is intentionally honest: it is authenticated and subscription-gated, and it treats the client polyline strictly as geometry input. The backend decodes route geometry, matches it against local Vegagerðin road geometry when possible, ranks along-route alerts with explicit source trust ordering, and falls back to origin-local context when real route matching is unavailable.'
      operationId: postRouteContextV2
      security:
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RouteContextRequest'
      responses:
        '200':
          description: Computed subscription-gated route-context response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouteContextResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /v2/alerts:
    get:
      tags:
      - Alerts
      summary: List active public alerts
      description: Returns the current active public alert feed backed by direct Vegagerðin WFS road-status rows, fresh matched `umferdin.is` road-condition alerts already persisted by the ingestion layer, and active `vedur.is` CAP weather warnings. Expired direct-status rows and CAP warnings are excluded. When Vegagerðin and `umferdin.is` both represent the same matched road segment, Vegagerðin wins and the duplicate `umferdin.is` alert is suppressed. CAP warnings attach to covered stations via polygon coverage and can then inherit conservative road/segment hints from those stations. Paging uses a zero-based offset cursor returned in `meta.nextCursor`. By default, alerts are ordered by freshest observed update first. `sort=nearest` orders by ascending distance from the requested coordinate pair to the nearest linked or covered station. `sort=conditionCategory` orders by backend severity priority (`closure`, `restriction`, `warning`, `info`) and then freshness. `sort=source` orders by backend source priority (`vegagerdin`, then `umferdin.is`, then `vedur.is`) and then freshness. Alerts that have enough station/road context also include up to five nearby or covered weather stations with compact latest-weather summaries.
      operationId: listAlertsV2
      parameters:
      - $ref: '#/components/parameters/LimitParam'
      - $ref: '#/components/parameters/CursorParam'
      - in: query
        name: sort
        description: Optional alert ordering mode. Defaults to `freshness`. Use `nearest` only together with `lat` and `lon`.
        schema:
          type: string
          enum:
          - freshness
          - nearest
          - conditionCategory
          - source
          default: freshness
      - in: query
        name: source
        description: Optional source filter. Repeat the parameter to include multiple values. Valid values are `vegagerdin`, `umferdin.is`, and `vedur.is`.
        schema:
          type: array
          items:
            type: string
            enum:
            - vegagerdin
            - umferdin.is
            - vedur.is
        style: form
        explode: true
      - in: query
        name: conditionCategory
        description: Optional condition-category filter. Repeat the parameter to include multiple values. Accepted values are `closure` (alias `closed`), `restriction`, `warning`, and `info`.
        schema:
          type: array
          items:
            type: string
            enum:
            - closure
            - closed
            - restriction
            - warning
            - info
        style: form
        explode: true
      - in: query
        name: lat
        description: Optional latitude for nearest-alert sorting. Must be sent with `lon` and requires `sort=nearest`.
        schema:
          $ref: '#/components/schemas/Latitude'
      - in: query
        name: lon
        description: Optional longitude for nearest-alert sorting. Must be sent with `lat` and requires `sort=nearest`.
        schema:
          $ref: '#/components/schemas/Longitude'
      responses:
        '200':
          description: Active public alert feed backed by direct Vegagerðin road-status rows, fresh matched `umferdin.is` alerts, and active `vedur.is` CAP warnings, ordered by freshness by default, or by nearest linked-or-covered station distance / condition category / source when requested.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AlertListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
  /v2/alerts/{alertId}:
    get:
      tags:
      - Alerts
      summary: Get one active public alert
      operationId: getAlertV2
      parameters:
      - in: path
        name: alertId
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 128
          pattern: ^[A-Za-z0-9._:-]+$
      responses:
        '200':
          description: Fresh public alert detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AlertResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
  /v2/stations/{stationId}:
    get:
      tags:
      - Stations
      summary: Get station details
      operationId: getStationV2
      parameters:
      - $ref: '#/components/parameters/StationIdParam'
      responses:
        '200':
          description: Station details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
  /v2/stations/{stationId}/latest:
    get:
      tags:
      - Observations
      summary: Get the latest observation for one station
      operationId: getLatestObservationByStationV2
      parameters:
      - $ref: '#/components/parameters/StationIdParam'
      responses:
        '200':
          description: Latest observation for the station.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ObservationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
  /v2/stations/nearest:
    get:
      tags:
      - Stations
      summary: Find the nearest station to a required coordinate pair
      description: |
        Discovery surface only.
        Nearest-station selection ignores stations whose canonical latest reading is older than 24 hours or missing any of: wind speed, wind direction degrees, or temperature.
        Explicit station detail/latest endpoints remain reachable and are not filtered by this discovery visibility rule.
      operationId: findNearestStationV2
      parameters:
      - in: query
        name: lat
        required: true
        schema:
          $ref: '#/components/schemas/Latitude'
      - in: query
        name: lon
        required: true
        schema:
          $ref: '#/components/schemas/Longitude'
      responses:
        '200':
          description: Nearest station resolution.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NearestStationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
  /v2/sources/health:
    get:
      tags:
      - Sources
      summary: List source refresh health snapshots
      operationId: listSourceHealthV2
      parameters:
      - in: query
        name: source
        description: Optional source filter.
        schema:
          type: string
          minLength: 1
          maxLength: 64
      - in: query
        name: operatorState
        description: Optional derived operator-state filter.
        schema:
          type: string
          enum:
          - waiting
          - due_now
          - running
          - stale_running
      - in: query
        name: lastAttemptStatus
        description: Optional scheduler-status filter such as idle, running, succeeded, failed, or timed_out.
        schema:
          type: string
          minLength: 1
          maxLength: 64
      - $ref: '#/components/parameters/LimitParam'
      responses:
        '200':
          description: Source refresh state shaped for operator triage without direct database access.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SourceHealthListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
  /v2/errorlogs:
    get:
      tags:
      - ErrorLogs
      summary: List upstream ingestion and source issues
      operationId: listUpstreamErrorLogsV2
      parameters:
      - in: query
        name: source
        description: Optional upstream source filter.
        schema:
          type: string
          minLength: 1
          maxLength: 64
      - in: query
        name: category
        description: Optional issue category filter. `rate_limit` is accepted as a legacy alias and normalized to `rate_limited`.
        schema:
          type: string
          enum:
          - apply_failure
          - bad_response
          - fetch_failure
          - parse_failure
          - rate_limit
          - rate_limited
          - scheduler_timeout
          - schema_change
      - in: query
        name: severity
        description: Optional severity filter.
        schema:
          type: string
          enum:
          - info
          - warning
          - error
          - critical
      - in: query
        name: operation
        description: Optional ingest operation filter. Current operator-facing values include Ambient Weather `discovery`, `refresh`, `refresh_recovery`, and `snapshot_sanitize`; umferdin.is `directory_refresh`/`measurement_refresh`/`road_condition_refresh`; plus apply-time `snapshot_apply` and `road_inventory_apply` for persisted operator triage.
        schema:
          type: string
          enum:
          - directory_refresh
          - discovery
          - measurement_refresh
          - refresh
          - refresh_recovery
          - road_condition_refresh
          - road_inventory_apply
          - snapshot_apply
          - snapshot_sanitize
      - in: query
        name: stationId
        description: Optional station filter for station-scoped ingest failures.
        schema:
          type: string
          minLength: 1
          maxLength: 64
      - in: query
        name: statusCode
        description: Optional upstream HTTP status filter for isolating rate limits and 5xx failures.
        schema:
          type: integer
          minimum: 100
          maximum: 599
      - in: query
        name: since
        description: Optional inclusive lower bound for `occurredAt` in RFC3339 format.
        schema:
          type: string
          format: date-time
      - in: query
        name: until
        description: Optional inclusive upper bound for `occurredAt` in RFC3339 format.
        schema:
          type: string
          format: date-time
      - $ref: '#/components/parameters/LimitParam'
      responses:
        '200':
          description: Upstream issue log entries, with repeated matching incidents coalesced into occurrence windows.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorLogListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
  /v2/auth/bootstrap:
    post:
      tags:
      - Auth
      summary: Create or resume a device-scoped user session
      description: 'Reusing the same `externalKey` resumes the existing user instead
        of creating

        a duplicate account. The backend refreshes profile fields, rotates the bearer

        token, invalidates the prior token, and preserves user-owned rows such as

        subscriptions, device tokens, widgets, and notification subscriptions.

        '
      operationId: bootstrapUserV2
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserBootstrapRequest'
      responses:
        '201':
          description: User session bootstrap result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserBootstrapResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
  /v2/me:
    get:
      tags:
      - Users
      summary: Get the authenticated user
      description: "Returns the profile resolved from the bearer token used on this request. After bootstrap resumes with the same `externalKey`, clients must switch to the rotated token before calling `/v2/me` again."
      operationId: getCurrentUserV2
      security:
      - bearerAuth: []
      responses:
        '200':
          description: Current user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v2/me/subscriptions:
    get:
      tags:
      - Subscriptions
      summary: List subscriptions for the authenticated user
      description: 'Subscriptions remain attached to the same user row after bootstrap
        resume.

        Clients should re-auth with the rotated token and continue using the same

        resource ids through `/v2/me`.

        '
      operationId: listMySubscriptionsV2
      security:
      - bearerAuth: []
      responses:
        '200':
          description: Subscription list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - Subscriptions
      summary: Create a subscription record for the authenticated user
      description: |
        Create uses the bearer token on this request. If bootstrap has just resumed,
        clients must discard the stale token first so new subscription writes stay on
        the resumed account instead of failing closed.
      operationId: createMySubscriptionV2
      security:
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSubscriptionRequest'
      responses:
        '201':
          description: Subscription created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v2/me/device-tokens:
    get:
      tags:
      - Devices
      summary: List push device tokens for the authenticated user
      description: 'Device-token rows stay on the same user when bootstrap resumes
        with the

        same `externalKey`. Re-registering the same `{platform, token}` updates the

        existing row instead of creating duplicates.

        '
      operationId: listMyDeviceTokensV2
      security:
      - bearerAuth: []
      responses:
        '200':
          description: Device token list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceTokenListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - Devices
      summary: Register a push device token for the authenticated user
      description: |
        Device registration writes against the current bearer token. After bootstrap
        resume rotates credentials, clients must re-auth before registering or
        mutating device rows.
      operationId: createMyDeviceTokenV2
      security:
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDeviceTokenRequest'
      responses:
        '201':
          description: Device token created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceTokenResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v2/me/widgets/{widgetId}:
    get:
      tags:
      - Widgets
      summary: Get a widget configuration
      operationId: getMyWidgetConfigV2
      security:
      - bearerAuth: []
      parameters:
      - $ref: '#/components/parameters/WidgetIdParam'
      responses:
        '200':
          description: Widget configuration.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetConfigResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - Widgets
      summary: Create or replace a widget configuration
      description: |
        Widget ids remain stable across bootstrap resume, but widget writes are still
        authorized by the current bearer token. Clients should re-auth with the
        rotated token before updating the existing widget row.
      operationId: upsertMyWidgetConfigV2
      security:
      - bearerAuth: []
      parameters:
      - $ref: '#/components/parameters/WidgetIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertWidgetConfigRequest'
      responses:
        '200':
          description: Widget configuration upserted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetConfigResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v2/me/notification-subscriptions:
    get:
      tags:
      - Notifications
      summary: List notification subscriptions for the authenticated user
      description: 'Notification rules remain attached to the same user row after bootstrap
        resume.

        Clients should re-auth with the rotated token and continue using the same

        notification-subscription ids through `/v2/me` mutation flows.

        '
      operationId: listMyNotificationSubscriptionsV2
      security:
      - bearerAuth: []
      responses:
        '200':
          description: Notification subscription list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationSubscriptionListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - Notifications
      summary: Create a notification subscription
      description: 'Create uses the currently authorized user. If bootstrap has just resumed,
        stale tokens should be discarded before creating new rules so writes stay on
        the resumed account.

        '
      operationId: createMyNotificationSubscriptionV2
      security:
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateNotificationSubscriptionRequest'
      responses:
        '201':
          description: Notification subscription created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationSubscriptionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v2/usage:
    get:
      tags:
      - Usage
      summary: Get aggregate platform usage stats
      operationId: getUsageStatsV2
      security:
      - bearerAuth: []
      responses:
        '200':
          description: Aggregate usage stats.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageStatsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v2/me/usage:
    get:
      tags:
      - Usage
      summary: Get usage stats scoped to the authenticated user
      operationId: getMyUsageStatsV2
      security:
      - bearerAuth: []
      responses:
        '200':
          description: User-scoped usage stats.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageStatsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v2/me/subscriptions/{subscriptionId}:
    put:
      tags:
      - Subscriptions
      summary: Update a subscription record for the authenticated user
      description: |
        Updates keep targeting the same subscription id after bootstrap resume, but
        the old token is invalidated immediately. Clients must re-auth before
        continuing subscription mutations.
      operationId: updateMySubscriptionV2
      security: &id001
      - bearerAuth: []
      parameters:
      - $ref: '#/components/parameters/SubscriptionIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSubscriptionRequest'
      responses:
        '200':
          description: Subscription updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
      - Subscriptions
      summary: Delete a subscription record for the authenticated user
      description: |
        Deletes use the same carried subscription id after bootstrap resume, but the
        request must present the rotated bearer token once the old one has been
        invalidated.
      operationId: deleteMySubscriptionV2
      security: *id001
      parameters:
      - $ref: '#/components/parameters/SubscriptionIdParam'
      responses:
        '204':
          description: Subscription deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v2/me/device-tokens/{deviceTokenId}:
    put:
      tags:
      - Devices
      summary: Update a push device token for the authenticated user
      description: |
        Device-token ids stay attached to the same user through bootstrap resume,
        but updates must switch to the rotated bearer token before continuing.
      operationId: updateMyDeviceTokenV2
      security: *id001
      parameters:
      - $ref: '#/components/parameters/DeviceTokenIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDeviceTokenRequest'
      responses:
        '200':
          description: Device token updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceTokenResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
      - Devices
      summary: Delete a push device token for the authenticated user
      description: |
        Deletes keep using the same device-token id after bootstrap resume, but the
        stale token will fail closed. Clients must re-auth with the rotated token.
      operationId: deleteMyDeviceTokenV2
      security: *id001
      parameters:
      - $ref: '#/components/parameters/DeviceTokenIdParam'
      responses:
        '204':
          description: Device token deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v2/me/notification-subscriptions/{notificationSubscriptionId}:
    put:
      tags:
      - Notifications
      summary: Update a notification subscription for the authenticated user
      description: 'Updates target the same notification-subscription id before and after
        bootstrap resume, but clients must send the rotated bearer token once the
        old token has been invalidated.

        Switching a carried rule to `alert` intentionally clears `thresholdValue`
        and any omitted quiet-hours window on that same row instead of forcing a
        delete/recreate cycle.

        '
      operationId: updateMyNotificationSubscriptionV2
      security: *id001
      parameters:
      - $ref: '#/components/parameters/NotificationSubscriptionIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateNotificationSubscriptionRequest'
      responses:
        '200':
          description: Notification subscription updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationSubscriptionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
      - Notifications
      summary: Delete a notification subscription for the authenticated user
      description: |
        Deletes keep using the same notification-subscription id after bootstrap
        resume, but the request must present the rotated bearer token once the old
        token has been invalidated.
      operationId: deleteMyNotificationSubscriptionV2
      security: *id001
      parameters:
      - $ref: '#/components/parameters/NotificationSubscriptionIdParam'
      responses:
        '204':
          description: Notification subscription deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v2/observations/latest:
    get:
      tags:
      - Observations
      summary: Get latest observations for multiple stations
      operationId: listLatestObservationsV2
      parameters:
      - in: query
        name: stationIds
        required: true
        description: Station ids to load in request order. Repeat the parameter to
          request multiple stations.
        schema:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
            minLength: 1
            maxLength: 64
        style: form
        explode: true
        examples:
          sample:
            value:
            - '1'
            - '422'
            - '777'
      responses:
        '200':
          description: Latest observations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ObservationListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
  parameters:
    StationIdParam:
      in: path
      name: stationId
      required: true
      schema:
        $ref: '#/components/schemas/StationId'
    WidgetIdParam:
      in: path
      name: widgetId
      required: true
      schema:
        $ref: '#/components/schemas/WidgetId'
    LimitParam:
      in: query
      name: limit
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 200
    CursorParam:
      in: query
      name: cursor
      description: Zero-based offset cursor returned by `meta.nextCursor`. Reuse it verbatim to fetch the next page of the same filtered result set.
      schema:
        type: integer
        minimum: 0
    UserIdParam:
      in: path
      name: userId
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 128
    SubscriptionIdParam:
      in: path
      name: subscriptionId
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 128
    DeviceTokenIdParam:
      in: path
      name: deviceTokenId
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 128
    NotificationSubscriptionIdParam:
      in: path
      name: notificationSubscriptionId
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 128
  responses:
    BadRequest:
      description: The request was invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Authentication is required or invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: The authenticated user is not entitled to this operation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: The requested resource was not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    HealthStatus:
      type: object
      required:
      - status
      properties:
        status:
          type: string
          examples:
          - ok
          - ready
      additionalProperties: false
    HealthStatusResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/HealthStatus'
      additionalProperties: false
    ReadinessStatus:
      type: object
      required:
      - status
      - checks
      properties:
        status:
          type: string
          enum:
          - ready
          - not_ready
        checks:
          type: object
          required:
          - database
          properties:
            database:
              type: string
              enum:
              - ok
              - error
          additionalProperties: false
      additionalProperties: false
    ReadinessStatusResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/ReadinessStatus'
      additionalProperties: false
    VersionInfo:
      type: object
      required:
      - version
      - commit
      - builtAt
      properties:
        version:
          type: string
          minLength: 1
          description: Human-readable build version string, typically a tag or git describe value.
        commit:
          type: string
          minLength: 1
          description: Full git commit hash embedded into the running binary when available.
        builtAt:
          type:
          - string
          - 'null'
          format: date-time
          description: UTC build timestamp when the binary was stamped during image build; falls back to VCS time when available.
      additionalProperties: false
    VersionResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/VersionInfo'
      additionalProperties: false
    SeifurOverview:
      type: object
      required:
      - name
      - slug
      - status
      - phase
      - summary
      - capabilities
      - nextStep
      properties:
        name:
          type: string
          minLength: 1
        slug:
          type: string
          minLength: 1
        status:
          type: string
          minLength: 1
        phase:
          type: string
          minLength: 1
        summary:
          type: string
          minLength: 1
        capabilities:
          type: array
          items:
            type: string
            minLength: 1
        nextStep:
          type: string
          minLength: 1
        snapshot:
          oneOf:
          - $ref: '#/components/schemas/SeifurSnapshotMetadata'
          - type: 'null'
      additionalProperties: false
    SeifurSnapshotMetadata:
      type: object
      required:
      - snapshotId
      - generatedAt
      - validAt
      - totalActiveStations
      - reportingStationCount
      - stationCountsBySource
      - reportingCountsByBand
      - availableBands
      - freshnessSeconds
      - searchRadiusKm
      - searchRadiiKm
      properties:
        snapshotId:
          type: string
          minLength: 1
        generatedAt:
          type: string
          format: date-time
        validAt:
          type:
          - string
          - 'null'
          format: date-time
        totalActiveStations:
          type: integer
          minimum: 0
        reportingStationCount:
          type: integer
          minimum: 0
        stationCountsBySource:
          type: array
          items:
            $ref: '#/components/schemas/SeifurSnapshotSourceCount'
        reportingCountsByBand:
          type: array
          items:
            $ref: '#/components/schemas/SeifurSnapshotBandCount'
        availableBands:
          type: array
          items:
            type: string
            enum:
            - wind
            - temp
            - rain
        freshnessSeconds:
          type:
          - integer
          - 'null'
          minimum: 0
        searchRadiusKm:
          type: number
          minimum: 0
        searchRadiiKm:
          type: array
          minItems: 1
          items:
            type: number
            minimum: 0
      additionalProperties: false
    SeifurSnapshotSourceCount:
      type: object
      required:
      - source
      - count
      properties:
        source:
          type: string
          minLength: 1
        count:
          type: integer
          minimum: 0
      additionalProperties: false
    SeifurSnapshotBandCount:
      type: object
      required:
      - band
      - count
      properties:
        band:
          type: string
          enum:
          - wind
          - temp
          - rain
        count:
          type: integer
          minimum: 0
      additionalProperties: false
    SeifurOverviewResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/SeifurOverview'

    SeifurStationListResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SeifurStation'
    SeifurStation:
      type: object
      required:
      - id
      - name
      - slug
      - region
      - lat
      - lon
      - source
      - isActive
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        region:
          type: string
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        source:
          type: string
        elevationM:
          type:
          - number
          - 'null'
        roadNumber:
          type:
          - string
          - 'null'
        isActive:
          type: boolean
        statusReason:
          type:
          - string
          - 'null'
        statusObservedAt:
          type:
          - string
          - 'null'
          format: date-time
        lastObservationAt:
          type:
          - string
          - 'null'
          format: date-time
        observationSource:
          type:
          - string
          - 'null'
        latest:
          oneOf:
          - $ref: '#/components/schemas/SeifurStationLatest'
          - type: 'null'
    SeifurStationLatest:
      type: object
      properties:
        observedAt:
          type:
          - string
          - 'null'
          format: date-time
        source:
          type:
          - string
          - 'null'
        stale:
          type:
          - boolean
          - 'null'
        airTemperatureC:
          type:
          - number
          - 'null'
        roadTemperatureC:
          type:
          - number
          - 'null'
        windSpeedMs:
          type:
          - number
          - 'null'
        gustSpeedMs:
          type:
          - number
          - 'null'
        windDirectionDeg:
          type:
          - integer
          - 'null'
        windDirectionText:
          type:
          - string
          - 'null'
        relativeHumidityPct:
          type:
          - number
          - 'null'
        precipitationMm:
          type:
          - number
          - 'null'
        visibilityM:
          type:
          - integer
          - 'null'
        conditionCode:
          type:
          - string
          - 'null'
      additionalProperties: false
    SeifurFieldNow:
      type: object
      required:
      - status
      - summary
      - request
      - coverage
      properties:
        status:
          type: string
          enum:
          - snapshot_only
          - partial_interpolated_field
        summary:
          type: string
          minLength: 1
        request:
          $ref: '#/components/schemas/SeifurFieldNowRequest'
        snapshot:
          oneOf:
          - $ref: '#/components/schemas/SeifurSnapshotMetadata'
          - type: 'null'
        coverage:
          $ref: '#/components/schemas/SeifurFieldNowCoverage'
        grid:
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowGrid'
          - type: 'null'
        bands:
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowBands'
          - type: 'null'
      additionalProperties: false
    SeifurFieldNowRequest:
      type: object
      required:
      - bbox
      - zoom
      - width
      - height
      - bands
      properties:
        bbox:
          type: array
          minItems: 4
          maxItems: 4
          items:
            type: number
        zoom:
          type: integer
          minimum: 0
          maximum: 22
        width:
          type: integer
          minimum: 1
          maximum: 1024
        height:
          type: integer
          minimum: 1
          maximum: 1024
        bands:
          type: array
          items:
            type: string
            enum:
            - wind
            - temp
            - rain
      additionalProperties: false
    SeifurFieldNowCoverage:
      type: object
      required:
      - totalActiveStations
      - reportingStationCount
      - availableBands
      - requestedBands
      - servedBands
      properties:
        totalActiveStations:
          type: integer
          minimum: 0
        reportingStationCount:
          type: integer
          minimum: 0
        availableBands:
          type: array
          items:
            type: string
            enum:
            - wind
            - temp
            - rain
        requestedBands:
          type: array
          items:
            type: string
            enum:
            - wind
            - temp
            - rain
        servedBands:
          description: Top-level interpolated field bands actually served in this response. This list does not include nested wind `gustMs` metadata.
          type: array
          items:
            type: string
            enum:
            - wind
            - temp
            - rain
        bands:
          description: Response-specific per-band coverage metadata. `servedBands` remains limited to top-level interpolated field bands currently exposed on this route. Nested wind `gustMs` availability is described under `coverage.bands.wind.gustMs` and does not make gust a separate served band.
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowCoverageBands'
          - type: 'null'
      additionalProperties: false
    SeifurFieldNowCoverageBands:
      type: object
      properties:
        wind:
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowWindBandCoverage'
          - type: 'null'
        temp:
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowBandCoverage'
          - type: 'null'
        rain:
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowBandCoverage'
          - type: 'null'
      additionalProperties: false
    SeifurFieldNowBandCoverage:
      type: object
      required:
      - requested
      - available
      - served
      - status
      properties:
        requested:
          type: boolean
        available:
          type: boolean
        served:
          type: boolean
        status:
          type: string
          enum:
          - not_requested
          - unavailable
          - metadata_only
          - all_null
          - partially_populated
          - fully_populated
        summary:
          type:
          - string
          - 'null'
        basis:
          type:
          - string
          - 'null'
        cellCount:
          type:
          - integer
          - 'null'
          minimum: 0
        nonNullCellCount:
          type:
          - integer
          - 'null'
          minimum: 0
        nullCellCount:
          type:
          - integer
          - 'null'
          minimum: 0
        nonNullFraction:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1
        confidence:
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowBandConfidence'
          - type: 'null'
      additionalProperties: false
    SeifurFieldNowBandConfidence:
      type: object
      required:
      - level
      - summary
      - populationFraction
      properties:
        level:
          type: string
          enum:
          - low
          - moderate
          - high
        summary:
          type: string
        minContributorsPerNonNull:
          type:
          - integer
          - 'null'
          minimum: 1
        maxContributorsPerNonNull:
          type:
          - integer
          - 'null'
          minimum: 1
        populationFraction:
          type: number
          minimum: 0
          maximum: 1
      additionalProperties: false
    SeifurFieldNowWindBandCoverage:
      type: object
      required:
      - requested
      - available
      - served
      - status
      properties:
        requested:
          type: boolean
        available:
          type: boolean
        served:
          type: boolean
        status:
          type: string
          enum:
          - not_requested
          - unavailable
          - metadata_only
          - all_null
          - partially_populated
          - fully_populated
        summary:
          type:
          - string
          - 'null'
        basis:
          type:
          - string
          - 'null'
        cellCount:
          type:
          - integer
          - 'null'
          minimum: 0
        nonNullCellCount:
          type:
          - integer
          - 'null'
          minimum: 0
        nullCellCount:
          type:
          - integer
          - 'null'
          minimum: 0
        nonNullFraction:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1
        confidence:
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowBandConfidence'
          - type: 'null'
        gustMs:
          description: Coverage metadata for nested gust intensity inside the served top-level wind band. `gustMs` does not indicate a separate top-level gust field.
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowNestedCoverage'
          - type: 'null'
      additionalProperties: false
    SeifurFieldNowNestedCoverage:
      type: object
      required:
      - available
      - status
      properties:
        available:
          type: boolean
        status:
          type: string
          enum:
          - unavailable
          - all_null
          - partially_populated
          - fully_populated
        summary:
          type:
          - string
          - 'null'
        cellCount:
          type:
          - integer
          - 'null'
          minimum: 0
        nonNullCellCount:
          type:
          - integer
          - 'null'
          minimum: 0
        nullCellCount:
          type:
          - integer
          - 'null'
          minimum: 0
        nonNullFraction:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1
        confidence:
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowBandConfidence'
          - type: 'null'
      additionalProperties: false
    SeifurFieldNowGrid:
      type: object
      required:
      - nx
      - ny
      properties:
        nx:
          type: integer
          minimum: 1
        ny:
          type: integer
          minimum: 1
      additionalProperties: false
    SeifurFieldNowBands:
      type: object
      properties:
        wind:
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowWindBand'
          - type: 'null'
        temp:
          oneOf:
          - $ref: '#/components/schemas/SeifurFieldNowScalarBand'
          - type: 'null'
      additionalProperties: false
    SeifurFieldNowWindBand:
      type: object
      description: Sustained wind vector field derived from nearby current non-stale reporting stations. This object may also include partial gust intensity metadata where current gust-reporting contributors exist. Gust values modify wind intensity semantics only and do not represent a separate vector field. Response-specific gust coverage/null behavior is described separately under `coverage.bands.wind.gustMs`.
      required:
      - status
      - summary
      - contributorRadiusKm
      - maxContributorsPerCell
      - u
      - v
      - speedMs
      - gustMs
      properties:
        status:
          type: string
          enum:
          - interpolated
        summary:
          type: string
          minLength: 1
        contributorRadiusKm:
          type: number
          minimum: 0
        maxContributorsPerCell:
          type: integer
          minimum: 1
        u:
          type: array
          items:
            type:
            - number
            - 'null'
        v:
          type: array
          items:
            type:
            - number
            - 'null'
        speedMs:
          type: array
          items:
            type:
            - number
            - 'null'
        gustMs:
          description: Partial gust intensity field in meters per second, derived from nearby current non-stale stations that report gusts. Cells may be null when no honest nearby gust contributors exist, including cells where sustained wind values are available.
          type: array
          items:
            type:
            - number
            - 'null'
      additionalProperties: false
    SeifurFieldNowScalarBand:
      type: object
      required:
      - status
      - summary
      - contributorRadiusKm
      - maxContributorsPerCell
      - values
      properties:
        status:
          type: string
          enum:
          - interpolated
        summary:
          type: string
          minLength: 1
        contributorRadiusKm:
          type: number
          minimum: 0
        maxContributorsPerCell:
          type: integer
          minimum: 1
        values:
          type: array
          items:
            type:
            - number
            - 'null'
      additionalProperties: false
    SeifurFieldNowResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/SeifurFieldNow'
      additionalProperties: false
    SeifurDebugInfluence:
      type: object
      required:
      - status
      - summary
      - request
      - bands
      properties:
        status:
          type: string
          enum:
          - current_snapshot_metadata_only
        summary:
          type: string
          minLength: 1
        request:
          $ref: '#/components/schemas/SeifurDebugInfluenceRequest'
        snapshot:
          oneOf:
          - $ref: '#/components/schemas/SeifurSnapshotMetadata'
          - type: 'null'
        bands:
          type: array
          items:
            $ref: '#/components/schemas/SeifurDebugInfluenceBandCoverage'
      additionalProperties: false
    SeifurDebugInfluenceRequest:
      type: object
      required:
      - lat
      - lon
      - bands
      properties:
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        bands:
          type: array
          items:
            type: string
            enum:
            - wind
            - temp
            - rain
      additionalProperties: false
    SeifurDebugInfluenceContributor:
      type: object
      required:
      - stationId
      - name
      - source
      - lat
      - lon
      - distanceKm
      - observedAt
      - stale
      - roadNumber
      - valueStatus
      properties:
        stationId:
          type: string
        name:
          type: string
        source:
          type: string
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        distanceKm:
          type: number
          minimum: 0
        observedAt:
          type:
          - string
          - 'null'
          format: date-time
        stale:
          type:
          - boolean
          - 'null'
        roadNumber:
          type:
          - string
          - 'null'
        valueStatus:
          type: string
          enum:
          - current
          - stale
          - missing
      additionalProperties: false
    SeifurDebugInfluenceExcludedCandidate:
      type: object
      required:
      - stationId
      - name
      - source
      - lat
      - lon
      - distanceKm
      - observedAt
      - stale
      - roadNumber
      - exclusionReason
      - requestedBand
      - valueStatus
      properties:
        stationId:
          type: string
        name:
          type: string
        source:
          type: string
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        distanceKm:
          type: number
          minimum: 0
        observedAt:
          type:
          - string
          - 'null'
          format: date-time
        stale:
          type:
          - boolean
          - 'null'
        roadNumber:
          type:
          - string
          - 'null'
        exclusionReason:
          type: string
          enum:
          - missing_latest
          - missing_wind_speed
          - missing_wind_direction
          - missing_wind_speed_and_direction
          - missing_temperature
          - missing_precipitation
          - stale_latest
          - selected_limit_reached
          - band_unavailable
        requestedBand:
          type: string
          enum:
          - wind
          - temp
          - rain
        valueStatus:
          type: string
          enum:
          - current
          - stale
          - missing
      additionalProperties: false
    SeifurDebugInfluenceBandCoverage:
      type: object
      required:
      - band
      - requested
      - available
      - reportingStationCount
      - searchRadiusKm
      - searchRadiiKm
      - maxNearbyCandidates
      - maxSelectedContributors
      - nearbyCandidateCount
      - eligibleCandidateCount
      - selectionBasis
      - selectedContributors
      - excludedCandidates
      - status
      - summary
      properties:
        band:
          type: string
          enum:
          - wind
          - temp
          - rain
        requested:
          type: boolean
        available:
          type: boolean
        reportingStationCount:
          type: integer
          minimum: 0
        searchRadiusKm:
          type: number
          minimum: 0
        searchRadiiKm:
          type: array
          minItems: 1
          items:
            type: number
            minimum: 0
        maxNearbyCandidates:
          type: integer
          minimum: 1
        maxSelectedContributors:
          type: integer
          minimum: 1
        nearbyCandidateCount:
          type: integer
          minimum: 0
        eligibleCandidateCount:
          type: integer
          minimum: 0
        selectionBasis:
          type: string
          enum:
          - current_non_stale_station_snapshot
        selectedContributors:
          type: array
          items:
            $ref: '#/components/schemas/SeifurDebugInfluenceContributor'
        excludedCandidates:
          type: array
          items:
            $ref: '#/components/schemas/SeifurDebugInfluenceExcludedCandidate'
        status:
          type: string
          enum:
          - metadata_only_selected_contributors
          - metadata_only_unavailable
        summary:
          type: string
          minLength: 1
      additionalProperties: false
    SeifurDebugInfluenceResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/SeifurDebugInfluence'
      additionalProperties: false
    ErrorResponse:
      type: object
      required:
      - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorDetail'
      additionalProperties: false
    ErrorDetail:
      type: object
      required:
      - code
      - message
      - status
      properties:
        code:
          type: string
          examples:
          - bad_request
          - unauthorized
          - not_found
        message:
          type: string
        details:
          type: object
          additionalProperties: true
        status:
          type: integer
          minimum: 100
          maximum: 599
      additionalProperties: false
    PaginationMeta:
      type: object
      required:
      - limit
      properties:
        limit:
          type: integer
          minimum: 1
        nextCursor:
          type:
          - string
          - 'null'
      additionalProperties: false
    Latitude:
      type: number
      minimum: -90
      maximum: 90
      examples:
      - 64.9631
    Longitude:
      type: number
      minimum: -180
      maximum: 180
      examples:
      - -19.0208
    StationId:
      type: string
      minLength: 1
      maxLength: 64
      pattern: ^[A-Za-z0-9._:-]+$
      examples:
      - '1'
    WidgetId:
      type: string
      minLength: 1
      maxLength: 64
      pattern: ^[A-Za-z0-9._:-]+$
    Station:
      type: object
      required:
      - id
      - name
      - slug
      - region
      - lat
      - lon
      - source
      - roadNumber
      - road
      - alerts
      - updatedAt
      - isActive
      - statusReason
      - statusObservedAt
      - lastObservationAt
      - observationSource
      properties:
        id: &id002
          $ref: '#/components/schemas/StationId'
        name: &id003
          type: string
          minLength: 1
          maxLength: 160
        slug: &id004
          type: string
          minLength: 1
          maxLength: 160
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        region: &id005
          type: string
          minLength: 1
          maxLength: 80
        lat: &id006
          $ref: '#/components/schemas/Latitude'
        lon: &id007
          $ref: '#/components/schemas/Longitude'
        source: &id008
          type: string
          minLength: 1
          maxLength: 64
          description: Upstream source attribution for the canonical station identity currently
            served to clients after Veðrið's source-precedence merge rules are applied.
        elevationM: &id009
          type:
          - number
          - 'null'
        roadNumber: &id010
          type:
          - string
          - 'null'
          maxLength: 32
        road: &id017
          anyOf:
          - $ref: '#/components/schemas/StationRoadRef'
          - type: 'null'
        alerts: &id018
          type: array
          description: Active public alerts currently related to this station via linked alert stations, bounded same-road corridor context, or later area-alert coverage rules.
          items:
            $ref: '#/components/schemas/Alert'
        distance:
          anyOf:
          - $ref: '#/components/schemas/StationDistance'
          - type: 'null'
          description: Present when the list was requested with a coordinate pair
            for distance-aware sorting.
        isActive: &id011
          type: boolean
        statusReason: &id012
          type:
          - string
          - 'null'
          maxLength: 128
        statusObservedAt: &id013
          type:
          - string
          - 'null'
          format: date-time
        lastObservationAt: &id014
          type:
          - string
          - 'null'
          format: date-time
        observationSource: &id015
          type:
          - string
          - 'null'
          maxLength: 64
          description: Upstream source attribution for the most recent observation known for
            this station. It may differ from `source` when the canonical station record
            came from one source and the freshest reading came from another after merge
            precedence is applied.
        updatedAt: &id016
          type: string
          format: date-time
      additionalProperties: false
    StationRoadRef:
      type: object
      description: Compact road context attached to station and observation surfaces. The object shape stays stable even when some road-enrichment fields are unknown, so clients can safely read `road.number` first and treat the other fields as nullable enrichments. For some Ambient Weather and bare `vedur.is` stations the backend may now backfill this object conservatively from nearby persisted road geometry or nearby road-aware anchors; that is useful travel context, but not the same provenance level as a direct source-native segment join. Browse/list surfaces may intentionally stop at road basics (`number`/`label`) while detail/latest/nearest surfaces can still backfill a safe approximate segment hint.
      required:
      - number
      - label
      - segmentLabel
      - status
      - incidents
      properties:
        number:
          type:
          - string
          - 'null'
          maxLength: 32
          description: Canonical public road number when the station has been mapped to a road corridor.
        label:
          type:
          - string
          - 'null'
          maxLength: 160
          description: Human-readable road label for the mapped corridor when known.
        segmentLabel:
          type:
          - string
          - 'null'
          maxLength: 160
          description: Human-readable segment or pass label for the mapped road section when known.
        status:
          anyOf:
          - $ref: '#/components/schemas/RoadStatus'
          - type: 'null'
          description: Effective live road-status summary for this station's mapped road segment. Direct Vegagerðin segment status stays canonical when available; otherwise the backend may project a fresh matched `umferdin.is` road-condition summary as additive travel context. Null means no active closure, restriction, or warning is currently associated.
        incidents:
          anyOf:
          - type: array
            items:
              $ref: '#/components/schemas/RoadIncident'
          - type: 'null'
          description: Compact list of fresh matched `umferdin.is` road-condition incidents on the same linked road segment(s). This is additive travel context and must not outrank the canonical effective `road.status` summary.
      additionalProperties: false
    RoadStatus:
      type: object
      required:
      - code
      - label
      - severity
      - description
      - note
      - updatedAt
      - validUntil
      - source
      - sourceType
      - matchedRoadConditionId
      - matchStrategy
      - matchConfidence
      properties:
        code:
          type:
          - string
          - 'null'
          maxLength: 64
          description: Stable upstream status code, for example `ALLUR_AKSTUR_BANN`.
        label:
          type:
          - string
          - 'null'
          maxLength: 240
          description: Short upstream label used in public road-condition feeds.
        severity:
          type:
          - string
          - 'null'
          maxLength: 32
          description: Derived Veðrið severity bucket for the effective status, such as `closure`, `restriction`, or `warning`.
        description:
          type:
          - string
          - 'null'
          maxLength: 240
          description: Human-readable primary label for the current road status.
        note:
          type:
          - string
          - 'null'
          maxLength: 2000
          description: Optional operator note or qualifier from the upstream status feed.
        updatedAt:
          type:
          - string
          - 'null'
          format: date-time
          description: Timestamp when the current status record was last registered or published upstream.
        validUntil:
          type:
          - string
          - 'null'
          format: date-time
          description: Upstream validity cutoff for the current status record when provided.
        source:
          type:
          - string
          - 'null'
          maxLength: 64
          description: Upstream source that supplied this effective status, for example `vegagerdin` or `umferdin.is`.
        sourceType:
          type:
          - string
          - 'null'
          maxLength: 64
          description: How Veðrið arrived at this status, such as a direct segment status or a matched persisted road condition.
        matchedRoadConditionId:
          type:
          - string
          - 'null'
          maxLength: 128
          description: Persisted `umferdin.is` road-condition id when the status was projected from a matched road-condition record rather than a direct Vegagerðin status point.
        matchStrategy:
          type:
          - string
          - 'null'
          maxLength: 128
          description: Match strategy recorded in `road_condition_segment_matches` when the status came from a matched road condition.
        matchConfidence:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1
          description: Confidence score recorded in `road_condition_segment_matches` when the status came from a matched road condition.
      additionalProperties: false
    RoadIncident:
      type: object
      required:
      - roadConditionId
      - code
      - label
      - severity
      - description
      - note
      - updatedAt
      - source
      - sourceType
      - matchStrategy
      - matchConfidence
      properties:
        roadConditionId:
          type: string
          minLength: 1
          maxLength: 128
        code:
          type:
          - string
          - 'null'
          maxLength: 64
        label:
          type:
          - string
          - 'null'
          maxLength: 240
        severity:
          type:
          - string
          - 'null'
          maxLength: 32
          description: Derived Veðrið severity bucket when available. Null means the matched enrichment has not yet been promoted into the same severity model as direct road statuses.
        description:
          type:
          - string
          - 'null'
          maxLength: 240
        note:
          type:
          - string
          - 'null'
          maxLength: 2000
        updatedAt:
          type:
          - string
          - 'null'
          format: date-time
        source:
          type: string
          minLength: 1
          maxLength: 64
        sourceType:
          type: string
          minLength: 1
          maxLength: 64
        matchStrategy:
          type:
          - string
          - 'null'
          maxLength: 128
        matchConfidence:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1
      additionalProperties: false
    StationDistance:
      type: object
      required:
      - km
      properties:
        km:
          type: number
          minimum: 0
      additionalProperties: false
    StationSourceStats:
      type: object
      required:
      - source
      - count
      properties:
        source:
          type: string
          minLength: 1
          maxLength: 64
        count:
          type: integer
          minimum: 0
      additionalProperties: false
    StationStats:
      type: object
      required:
      - totalStations
      - stationsPerSource
      - totalStale
      - updatedLast60Minutes
      - updatedLast10Minutes
      - generatedAt
      properties:
        totalStations:
          type: integer
          minimum: 0
        stationsPerSource:
          type: array
          items:
            $ref: '#/components/schemas/StationSourceStats'
        totalStale:
          type: integer
          minimum: 0
        updatedLast60Minutes:
          type: integer
          minimum: 0
        updatedLast10Minutes:
          type: integer
          minimum: 0
        generatedAt:
          type: string
          format: date-time
      additionalProperties: false
    Observation:
      type: object
      required:
      - stationId
      - observedAt
      - source
      - stale
      - airTemperatureC
      - roadTemperatureC
      - dewPoint
      - traffic
      - trafficFromMidnight
      - relativeHumidityPct
      - windSpeedMs
      - gustSpeedMs
      - windDirectionDeg
      - windDirectionText
      - precipitationMm
      - visibilityM
      - conditionCode
      - roadNumber
      - road
      - alerts
      - stationIsActive
      - stationStatusReason
      - stationStatusUpdatedAt
      properties:
        stationId:
          $ref: '#/components/schemas/StationId'
        observedAt:
          type: string
          format: date-time
        source:
          type: string
          minLength: 1
          maxLength: 64
          description: Upstream source attribution for the observation payload returned to
            the client after merge precedence is applied.
        stale:
          type: boolean
        airTemperatureC:
          type:
          - number
          - 'null'
        roadTemperatureC:
          type:
          - number
          - 'null'
        dewPoint:
          type:
          - number
          - 'null'
        traffic:
          type:
          - integer
          - 'null'
          minimum: 0
        trafficFromMidnight:
          type:
          - integer
          - 'null'
          minimum: 0
        relativeHumidityPct:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 100
        windSpeedMs:
          type:
          - number
          - 'null'
          minimum: 0
        gustSpeedMs:
          type:
          - number
          - 'null'
          minimum: 0
        windDirectionDeg:
          type:
          - integer
          - 'null'
          minimum: 0
          maximum: 360
        windDirectionText:
          type:
          - string
          - 'null'
          maxLength: 8
        precipitationMm:
          description: Source-native recent precipitation amount in millimeters; Ambient Weather currently maps this from `hourlyrainin` (last hour), not a daily total.
          type:
          - number
          - 'null'
          minimum: 0
        visibilityM:
          type:
          - integer
          - 'null'
          minimum: 0
        conditionCode:
          type:
          - string
          - 'null'
          maxLength: 64
        roadNumber: *id010
        road: *id017
        alerts:
          type: array
          items:
            $ref: '#/components/schemas/ObservationAlert'
        stationIsActive:
          type: boolean
        stationStatusReason:
          type:
          - string
          - 'null'
          maxLength: 128
        stationStatusUpdatedAt:
          type:
          - string
          - 'null'
          format: date-time
      additionalProperties: false
    NearestStationResult:
      type: object
      required:
      - station
      - requested
      - distanceKm
      properties:
        station:
          $ref: '#/components/schemas/Station'
        requested:
          type: object
          required:
          - lat
          - lon
          properties:
            lat:
              $ref: '#/components/schemas/Latitude'
            lon:
              $ref: '#/components/schemas/Longitude'
          additionalProperties: false
        distanceKm:
          type: number
          minimum: 0
      additionalProperties: false
    User:
      type: object
      required:
      - id
      - displayName
      - locale
      - timezone
      - createdAt
      - updatedAt
      properties:
        id:
          type: string
        externalKey:
          type: string
          maxLength: 128
        displayName:
          type: string
          minLength: 1
          maxLength: 120
        locale:
          type: string
          minLength: 2
          maxLength: 16
        timezone:
          type: string
          minLength: 1
          maxLength: 64
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      additionalProperties: false
    UserBootstrapRequest:
      type: object
      required:
      - displayName
      - locale
      - timezone
      properties:
        externalKey:
          type:
          - string
          - 'null'
          maxLength: 128
        displayName:
          type: string
          minLength: 1
          maxLength: 120
        locale:
          type: string
          minLength: 2
          maxLength: 16
        timezone:
          type: string
          minLength: 1
          maxLength: 64
      additionalProperties: false
    Subscription:
      description: Subscriptions are currently represented by `expiresAt` without a separate `endedAt` field.
      type: object
      required:
      - id
      - userId
      - plan
      - status
      - startedAt
      - createdAt
      - updatedAt
      properties:
        id:
          type: string
        userId:
          type: string
        plan:
          type: string
          enum:
          - free
          - plus
          - pro
          - lifetime
        status:
          type: string
          enum:
          - active
          - trialing
          - paused
          - canceled
          - expired
        startedAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      additionalProperties: false
    CreateSubscriptionRequest:
      description: This request does not include an `endedAt` field; use `expiresAt` for planned end dates.
      type: object
      required:
      - plan
      - status
      - startedAt
      properties:
        plan:
          type: string
          enum:
          - free
          - plus
          - pro
          - lifetime
        status:
          type: string
          enum:
          - active
          - trialing
          - paused
          - canceled
          - expired
        startedAt:
          type: string
          format: date-time
        expiresAt:
          type:
          - string
          - 'null'
          format: date-time
      additionalProperties: false
    DeviceToken:
      type: object
      required:
      - id
      - userId
      - platform
      - token
      - environment
      - createdAt
      - updatedAt
      properties:
        id:
          type: string
        userId:
          type: string
        platform:
          type: string
          enum:
          - ios
          - android
          - web
        token:
          type: string
          minLength: 1
          maxLength: 4096
        environment:
          type: string
          enum:
          - development
          - production
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      additionalProperties: false
    CreateDeviceTokenRequest:
      type: object
      required:
      - platform
      - token
      - environment
      properties:
        platform:
          type: string
          enum:
          - ios
          - android
          - web
        token:
          type: string
          minLength: 1
          maxLength: 4096
        environment:
          type: string
          enum:
          - development
          - production
      additionalProperties: false
    WidgetConfig:
      type: object
      required:
      - id
      - userId
      - name
      - lat
      - lon
      - theme
      - temperatureC
      - windSpeedMs
      - updatedAt
      - createdAt
      properties:
        id:
          $ref: '#/components/schemas/WidgetId'
        userId:
          type: string
        name:
          type: string
          minLength: 1
          maxLength: 120
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        stationId:
          $ref: '#/components/schemas/StationId'
        theme:
          type: string
          description: |
            `night` stays in read responses because older clients already persist
            it, and fail-closed response validation should not reject their saved
            widget state on resume.
          enum:
          - road-sign
          - night
          - system-dark
          - system-light
        temperatureC:
          type: boolean
        windSpeedMs:
          type: boolean
        updatedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
      additionalProperties: false
    UpsertWidgetConfigRequest:
      type: object
      required:
      - name
      - lat
      - lon
      - theme
      - temperatureC
      - windSpeedMs
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        stationId:
          anyOf:
          - $ref: '#/components/schemas/StationId'
          - type: 'null'
          description: Optional override. If omitted, the backend may resolve the
            nearest station from lat/lon.
        theme:
          type: string
          description: |
            `night` stays in the contract because existing clients already persist
            it, even though newer callers may prefer the system-aware theme values.
          enum:
          - road-sign
          - night
          - system-dark
          - system-light
        temperatureC:
          type: boolean
        windSpeedMs:
          type: boolean
      additionalProperties: false
    NotificationSchedule:
      type: object
      required:
      - timezone
      - allowedDays
      - windowStart
      - windowEnd
      properties:
        timezone:
          type: string
        allowedDays:
          type: array
          items:
            type: string
            enum:
            - mon
            - tue
            - wed
            - thu
            - fri
            - sat
            - sun
        windowStart:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        windowEnd:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
      additionalProperties: false
    NotificationSubscription:
      type: object
      required:
      - id
      - userId
      - lat
      - lon
      - eventType
      - enabled
      - scheduleEnabled
      - createdAt
      - updatedAt
      properties:
        id:
          type: string
        userId:
          type: string
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        stationId:
          $ref: '#/components/schemas/StationId'
        eventType:
          type: string
          enum:
          - wind_speed
          - gust_speed
          - road_temperature
          - air_temperature
          - alert
        thresholdValue:
          type:
          - number
          - 'null'
        quietHoursStart:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        quietHoursEnd:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        scheduleEnabled:
          type: boolean
        scheduleTimezone:
          type: string
        scheduleAllowedDays:
          type: array
          items:
            type: string
            enum:
            - mon
            - tue
            - wed
            - thu
            - fri
            - sat
            - sun
        scheduleWindowStart:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        scheduleWindowEnd:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        schedule:
          $ref: '#/components/schemas/NotificationSchedule'
        enabled:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      additionalProperties: false
    CreateNotificationSubscriptionRequest:
      type: object
      required:
      - lat
      - lon
      - eventType
      - enabled
      properties:
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        stationId:
          anyOf:
          - $ref: '#/components/schemas/StationId'
          - type: 'null'
          description: Optional override. If omitted, the backend may resolve the
            nearest station from lat/lon.
        eventType:
          type: string
          enum:
          - wind_speed
          - gust_speed
          - road_temperature
          - air_temperature
          - alert
        thresholdValue:
          type:
          - number
          - 'null'
        quietHoursStart:
          type:
          - string
          - 'null'
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        quietHoursEnd:
          type:
          - string
          - 'null'
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        scheduleEnabled:
          type:
          - boolean
          - 'null'
        scheduleTimezone:
          type:
          - string
          - 'null'
        scheduleAllowedDays:
          type: array
          items:
            type: string
            enum:
            - mon
            - tue
            - wed
            - thu
            - fri
            - sat
            - sun
        scheduleWindowStart:
          type:
          - string
          - 'null'
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        scheduleWindowEnd:
          type:
          - string
          - 'null'
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        schedule:
          anyOf:
          - $ref: '#/components/schemas/NotificationSchedule'
          - type: 'null'
          description: Preferred explicit allow-schedule. When provided, the backend mirrors it into the flat schedule fields for compatibility while quiet-hours remain temporary bridge fields.
        enabled:
          type: boolean
      additionalProperties: false
    UsageStats:
      type: object
      required:
      - scope
      - totalUsers
      - activeSubscriptions
      - registeredDevices
      - widgetConfigs
      - notificationRules
      - updatedAt
      properties:
        scope:
          type: string
          enum:
          - global
          - user
        userId:
          type: string
        totalUsers:
          type: integer
          minimum: 0
        activeSubscriptions:
          type: integer
          minimum: 0
        registeredDevices:
          type: integer
          minimum: 0
        widgetConfigs:
          type: integer
          minimum: 0
        notificationRules:
          type: integer
          minimum: 0
        updatedAt:
          type: string
          format: date-time
      additionalProperties: false
    SourceHealthSnapshot:
      type: object
      required:
      - source
      - cadenceSeconds
      - operatorState
      - lastAttemptStatus
      - consecutiveFailures
      - overdueSecs
      - dueInSecs
      - lastStartedAt
      - lastCompletedAt
      - lastSuccessAt
      - lastErrorAt
      - lastObservedUpstreamAt
      - lastJobId
      - lastErrorMessage
      - lastProviderUrl
      - lastProviderDetail
      - lastProviderState
      - lastBackoffReason
      - lastRetryAfterSecs
      - nextPollAt
      - updatedAt
      properties:
        source:
          type: string
          minLength: 1
          maxLength: 64
        cadenceSeconds:
          type: integer
          minimum: 1
        operatorState:
          type: string
          enum:
          - waiting
          - due_now
          - running
          - stale_running
          description: Derived operator view of scheduler state. `stale_running` mirrors the takeover threshold so operators can spot hung refreshes before the next scheduler pass writes a timeout row.
        lastAttemptStatus:
          type: string
          minLength: 1
          maxLength: 64
        consecutiveFailures:
          type: integer
          minimum: 0
        overdueSecs:
          type: integer
          minimum: 0
        dueInSecs:
          type: integer
          minimum: 0
        lastStartedAt:
          type:
          - string
          - 'null'
          format: date-time
        lastCompletedAt:
          type:
          - string
          - 'null'
          format: date-time
        lastSuccessAt:
          type:
          - string
          - 'null'
          format: date-time
        lastErrorAt:
          type:
          - string
          - 'null'
          format: date-time
        lastObservedUpstreamAt:
          type:
          - string
          - 'null'
          format: date-time
        lastJobId:
          type:
          - string
          - 'null'
        lastErrorMessage:
          type:
          - string
          - 'null'
        lastProviderUrl:
          type:
          - string
          - 'null'
          format: uri
          maxLength: 2048
        lastProviderDetail:
          type:
          - string
          - 'null'
          maxLength: 4000
        lastProviderState:
          type:
          - string
          - 'null'
          description: JSON-encoded source/provider state captured from the last refresh attempt when the source exposes durable conditional-fetch or similar operator state.
          maxLength: 65535
        lastBackoffReason:
          type:
          - string
          - 'null'
        lastRetryAfterSecs:
          type:
          - integer
          - 'null'
          minimum: 0
        nextPollAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      additionalProperties: false
    SourceHealthListResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SourceHealthSnapshot'
      additionalProperties: false
    ErrorLog:
      type: object
      required:
      - id
      - source
      - category
      - severity
      - operation
      - stationId
      - occurredAt
      - firstOccurredAt
      - lastOccurredAt
      - occurrenceCount
      - statusCode
      - message
      - detail
      - upstreamUrl
      - retryAfterSecs
      - createdAt
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 128
        source:
          type: string
          minLength: 1
          maxLength: 64
        category:
          type: string
          minLength: 1
          maxLength: 64
          description: Stable operator-facing classifier for the upstream issue. Common
            values include `fetch_failure`, `parse_failure`, `bad_response`,
            `rate_limited`, `apply_failure`, and `scheduler_timeout`, but the field
            remains open so new source-specific categories can surface without silently
            drifting from the contract.
        severity:
          type: string
          enum:
          - info
          - warning
          - error
          - critical
          description: Incident severity. `warning` is also used for successful refreshes that intentionally skip malformed upstream rows while still publishing the healthy remainder.
        operation:
          type:
          - string
          - 'null'
          enum:
          - directory_refresh
          - discovery
          - measurement_refresh
          - refresh
          - refresh_recovery
          - road_condition_refresh
          - road_inventory_apply
          - snapshot_apply
          - snapshot_sanitize
          - 'null'
          description: Optional ingest-phase context for operator debugging. Ambient Weather uses `discovery`, `refresh`, `refresh_recovery`, and `snapshot_sanitize`; umferdin.is uses `directory_refresh`, `measurement_refresh`, and `road_condition_refresh`; and apply-time failures now distinguish `snapshot_apply` from `road_inventory_apply` so `/v2/errorlogs` can separate bulk snapshot trouble from persisted road-inventory write failures.
        stationId:
          anyOf:
          - $ref: '#/components/schemas/StationId'
          - type: 'null'
        occurredAt:
          type: string
          format: date-time
          description: Latest occurrence timestamp for this coalesced operator incident row.
        firstOccurredAt:
          type: string
          format: date-time
          description: First time this matching incident was seen.
        lastOccurredAt:
          type: string
          format: date-time
          description: Most recent time this matching incident was seen.
        occurrenceCount:
          type: integer
          minimum: 1
          description: Number of matching upstream incidents coalesced into this row.
        statusCode:
          type:
          - integer
          - 'null'
          minimum: 100
          maximum: 599
        message:
          type: string
          minLength: 1
          maxLength: 500
        detail:
          type:
          - string
          - 'null'
          maxLength: 4000
        upstreamUrl:
          type:
          - string
          - 'null'
          format: uri
          maxLength: 2048
        retryAfterSecs:
          type:
          - integer
          - 'null'
          minimum: 0
        createdAt:
          type: string
          format: date-time
      additionalProperties: false
    AlertStationRef:
      type: object
      additionalProperties: false
      required:
      - stationId
      - name
      properties:
        stationId:
          type: string
        name:
          type: string
        roadNumber:
          type:
          - string
          - 'null'
        roadLabel:
          type:
          - string
          - 'null'
        distanceKm:
          type: number
          minimum: 0
          description: Approximate distance from the requested coordinate pair when `/v2/alerts` uses `sort=nearest` and the linked station coordinates can be resolved.
    Alert:
      type: object
      additionalProperties: false
      required:
      - id
      - source
      - title
      - roadConditionId
      - stations
      - weatherStations
      - updatedAt
      properties:
        id:
          type: string
        source:
          type: string
          enum:
          - vegagerdin
          - umferdin.is
          - vedur.is
        title:
          type: string
        roadConditionId:
          type: string
        roadNumber:
          type:
          - string
          - 'null'
        roadName:
          type:
          - string
          - 'null'
        display:
          type:
          - string
          - 'null'
        conditionCode:
          type:
          - string
          - 'null'
        conditionCategory:
          type:
          - string
          - 'null'
          description: Source-specific alert severity/category. Filters and sort rules normalize aliases such as `closed` and `closure` together.
        conditionDescription:
          type:
          - string
          - 'null'
        observedAt:
          type:
          - string
          - 'null'
          format: date-time
        sentAt:
          type:
          - string
          - 'null'
          format: date-time
          description: Original upstream CAP/alert send time when available.
        onsetAt:
          type:
          - string
          - 'null'
          format: date-time
          description: Upstream warning onset/start time when available.
        expiresAt:
          type:
          - string
          - 'null'
          format: date-time
          description: Upstream warning expiry/end time when available. Expired warnings are excluded from active feeds.
        previousCode:
          type:
          - string
          - 'null'
        previousAt:
          type:
          - string
          - 'null'
          format: date-time
        weightLimitTons:
          type:
          - integer
          - 'null'
        weightDescription:
          type:
          - string
          - 'null'
        matchedSegmentId:
          type:
          - integer
          - 'null'
          description: Best resolved Vegagerðin road segment id for the alert. May come directly from persisted source matching or be backfilled from one unambiguous linked-station segment.
        matchStrategy:
          type:
          - string
          - 'null'
        matchConfidence:
          type:
          - number
          - 'null'
        stations:
          type: array
          items:
            $ref: '#/components/schemas/AlertStationRef'
        weatherStations:
          type: array
          description: Up to five nearby or covered weather stations selected from the alert's linked/matched road context or CAP polygon coverage when the backend can resolve that relation honestly.
          items:
            $ref: '#/components/schemas/RoadReportStation'
        updatedAt:
          type: string
          format: date-time
    ObservationAlert:
      type: object
      additionalProperties: false
      required:
      - id
      - source
      - title
      - roadConditionId
      - stations
      - weatherStations
      - updatedAt
      properties:
        id:
          type: string
        source:
          type: string
          enum:
          - vegagerdin
          - umferdin.is
          - vedur.is
        title:
          type: string
        roadConditionId:
          type: string
        roadNumber:
          type:
          - string
          - 'null'
        roadName:
          type:
          - string
          - 'null'
        display:
          type:
          - string
          - 'null'
        conditionCode:
          type:
          - string
          - 'null'
        conditionCategory:
          type:
          - string
          - 'null'
          description: Source-specific alert severity/category. Filters and sort rules normalize aliases such as `closed` and `closure` together.
        conditionDescription:
          type:
          - string
          - 'null'
        observedAt:
          type:
          - string
          - 'null'
          format: date-time
        sentAt:
          type:
          - string
          - 'null'
          format: date-time
          description: Original upstream CAP/alert send time when available.
        onsetAt:
          type:
          - string
          - 'null'
          format: date-time
          description: Upstream warning onset/start time when available.
        expiresAt:
          type:
          - string
          - 'null'
          format: date-time
          description: Upstream warning expiry/end time when available. Expired warnings are excluded from active feeds.
        previousCode:
          type:
          - string
          - 'null'
        previousAt:
          type:
          - string
          - 'null'
          format: date-time
        weightLimitTons:
          type:
          - integer
          - 'null'
        weightDescription:
          type:
          - string
          - 'null'
        matchedSegmentId:
          type:
          - integer
          - 'null'
          description: Best resolved Vegagerðin road segment id for the alert. May come directly from persisted source matching or be backfilled from one unambiguous linked-station segment.
        matchStrategy:
          type:
          - string
          - 'null'
        matchConfidence:
          type:
          - number
          - 'null'
        stations:
          type: array
          items:
            $ref: '#/components/schemas/AlertStationRef'
        weatherStations:
          type: array
          maxItems: 0
          description: Observation/latest polling surfaces intentionally keep this nested list empty to avoid repeatedly shipping bulky alert weather-station summaries. Use `/v2/alerts` or `/v2/alerts/{alertId}` for the richer weather-station context.
          items:
            $ref: '#/components/schemas/RoadReportStation'
        updatedAt:
          type: string
          format: date-time
    AlertResponse:
      type: object
      additionalProperties: false
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/Alert'
    AlertListResponse:
      type: object
      additionalProperties: false
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Alert'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
    RoadReportResolvedRoad:
      type: object
      additionalProperties: false
      required:
      - matchedSegmentIds
      - matchedSegments
      properties:
        number:
          type:
          - string
          - 'null'
        name:
          type:
          - string
          - 'null'
        section:
          type:
          - string
          - 'null'
        corridorSpan:
          type:
          - string
          - 'null'
          description: Compact human-readable corridor span derived from the bounded matched segment set when the persisted Vegagerðin descriptions form one continuous section.
        corridorSpanEndpoints:
          allOf:
          - $ref: '#/components/schemas/RoadReportMatchedSpanLabel'
          description: Structured endpoints for the compact resolved corridor span when that bounded label names a section like `Víðigerði - Núpsvötn`.
          nullable: true
        matchedSegmentIds:
          type: array
          items:
            type: integer
        matchedSegments:
          type: array
          description: Structured persisted Vegagerðin segment detail for the resolved report corridor. The first entry is the winning matched segment when known; later entries can widen across the already-bounded nearby-station corridor span.
          items:
            $ref: '#/components/schemas/RoadReportMatchedSegment'
    RoadReportMatchedSegment:
      type: object
      additionalProperties: false
      required:
      - id
      - context
      properties:
        id:
          type: integer
        context:
          type: string
          enum:
          - matched_alert_segment
          - linked_alert_station
          - same_road_corridor
          description: Explainability context for why this persisted segment is part of the resolved bounded corridor set.
        roadNumber:
          type:
          - string
          - 'null'
        roadName:
          type:
          - string
          - 'null'
        roadDescription:
          type:
          - string
          - 'null'
        span:
          allOf:
          - $ref: '#/components/schemas/RoadReportMatchedSpanLabel'
          description: Parsed span endpoints from the persisted segment description when that label already names a section like `Víðigerði - Gljúfurá`.
          nullable: true
    RoadReportMatchedSpanLabel:
      type: object
      additionalProperties: false
      required:
      - from
      - to
      properties:
        from:
          type: string
        to:
          type: string
    RoadReportCurrentStatus:
      type: object
      additionalProperties: false
      required:
      - source
      - sourceType
      - matchedRoadConditionId
      properties:
        label:
          type:
          - string
          - 'null'
        severity:
          type:
          - string
          - 'null'
        code:
          type:
          - string
          - 'null'
        description:
          type:
          - string
          - 'null'
        updatedAt:
          type:
          - string
          - 'null'
          format: date-time
        source:
          type: string
          enum:
          - umferdin.is
          - vegagerdin
        sourceType:
          type: string
          enum:
          - matched_road_condition
          - direct_segment_status
        matchedRoadConditionId:
          type: string
        matchStrategy:
          type:
          - string
          - 'null'
        matchConfidence:
          type:
          - number
          - 'null'
        transition:
          anyOf:
          - $ref: '#/components/schemas/RoadReportStatusTransition'
          - type: 'null'
    RoadReportStatusTransition:
      type: object
      additionalProperties: false
      required:
      - changed
      properties:
        previousCode:
          type:
          - string
          - 'null'
        previousAt:
          type:
          - string
          - 'null'
          format: date-time
        changed:
          type: boolean
    RoadReportHighlight:
      type: object
      additionalProperties: false
      required:
      - code
      - severity
      - label
      properties:
        code:
          type: string
          enum:
          - road_closed
          - road_restriction
          - road_warning
          - freezing_road
          - freezing_air
          - strong_wind
          - low_visibility
          - active_precipitation
          - recent_status_change
          - stale_weather
          - no_station_weather
        severity:
          type: string
          enum:
          - info
          - warning
          - critical
        label:
          type: string
    RoadReportAdjacentRoad:
      type: object
      additionalProperties: false
      required:
      - currentStatus
      properties:
        number:
          type:
          - string
          - 'null'
        name:
          type:
          - string
          - 'null'
        section:
          type:
          - string
          - 'null'
        distanceKm:
          type: number
          format: double
          description: Approximate corridor distance from the matched road-report anchor stations when it can be derived from persisted linked station coordinates.
        linkStation:
          anyOf:
          - $ref: '#/components/schemas/RoadReportRelatedStation'
          - type: 'null'
          description: Exact linked station on the adjacent/connected alert that made this nearby road eligible for the bounded corridor view.
        currentStatus:
          $ref: '#/components/schemas/RoadReportCurrentStatus'
    RoadReportRelatedStation:
      type: object
      additionalProperties: false
      required:
      - stationId
      - name
      - lat
      - lon
      properties:
        stationId:
          type: string
        name:
          type: string
        lat:
          type: number
          format: double
        lon:
          type: number
          format: double
    RoadReportAlternativeHit:
      type: object
      additionalProperties: false
      required:
      - score
      - scoreDelta
      - matchedBy
      - currentStatus
      properties:
        number:
          type:
          - string
          - 'null'
        name:
          type:
          - string
          - 'null'
        section:
          type:
          - string
          - 'null'
        score:
          type: number
          format: double
          minimum: 0
          maximum: 1
          description: Internal matcher score for this alternative active road candidate.
        scoreDelta:
          type: number
          format: double
          minimum: 0
          maximum: 1
          description: Gap between the winning match score and this alternative score. Smaller means the alternative nearly won.
        matchedBy:
          type: array
          description: Compact explanation of which persisted road fields made this alternative candidate plausible.
          items:
            type: string
            enum:
            - road_number
            - road_name
            - title
            - matched_segment_description
            - matched_segment_name
            - station_road_label
            - station_name
            - near_context
            - ranked_alert
        matchEvidence:
          type: array
          description: Exact persisted labels or query-context snippets that made this alternative candidate remain plausible.
          items:
            $ref: '#/components/schemas/RoadReportMatchEvidence'
        currentStatus:
          $ref: '#/components/schemas/RoadReportCurrentStatus'
    RoadReportLatestSummary:
      type: object
      additionalProperties: false
      required:
      - observedAt
      - source
      - stale
      properties:
        observedAt:
          type: string
          format: date-time
        source:
          type: string
        stale:
          type: boolean
        signals:
          type: array
          items:
            $ref: '#/components/schemas/RoadReportHighlight'
          description: Backend-generated per-station weather signals using the same bounded thresholds as the corridor weather summary.
        airTemperatureC:
          type:
          - number
          - 'null'
        windSpeedMs:
          type:
          - number
          - 'null'
        gustSpeedMs:
          type:
          - number
          - 'null'
        roadTemperatureC:
          type:
          - number
          - 'null'
        precipitationMm:
          description: Source-native recent precipitation amount in millimeters; Ambient Weather currently maps this from `hourlyrainin` (last hour), not a daily total.
          type:
          - number
          - 'null'
          minimum: 0
        visibilityM:
          type:
          - integer
          - 'null'
          minimum: 0
        conditionCode:
          type:
          - string
          - 'null'
    RoadReportWeatherStationRef:
      type: object
      additionalProperties: false
      required:
      - stationId
      - name
      - lat
      - lon
      - observedAt
      - stale
      properties:
        stationId:
          type: string
          minLength: 1
        name:
          type: string
          minLength: 1
        lat:
          type: number
          format: double
        lon:
          type: number
          format: double
        distanceKm:
          type: number
          minimum: 0
        observedAt:
          type: string
          format: date-time
        stale:
          type: boolean
          description: True when the weather-summary driver station came from the stale fallback set because no fresh corridor reading was available.
    RoadReportWeatherSummary:
      type: object
      additionalProperties: false
      required:
      - stationCount
      - reportingStationCount
      - missingLatestStationCount
      - freshStationCount
      - staleStationCount
      - inactiveStationCount
      - basis
      - freezingStationCount
      - freezingRoadStationCount
      - strongWindStationCount
      - lowVisibilityStationCount
      - precipitationStationCount
      properties:
        stationCount:
          type: integer
        reportingStationCount:
          type: integer
          description: Number of nearby corridor stations that currently contribute any latest-reading payload to the report, whether fresh or stale.
        missingLatestStationCount:
          type: integer
          description: Number of included nearby corridor stations that currently have no latest-reading payload, so clients can see summary coverage without diffing the station list.
        freshStationCount:
          type: integer
        staleStationCount:
          type: integer
        inactiveStationCount:
          type: integer
          description: Number of included nearby corridor stations currently marked inactive or otherwise out of service.
        basis:
          type: string
          enum:
          - none
          - fresh
          - stale_fallback
          description: Whether the compact corridor weather summary is driven by fresh readings, stale-only fallback readings, or no station weather at all.
        freezingStationCount:
          type: integer
          description: Number of corridor stations at or below 0°C, using fresh readings when available and otherwise falling back to stale carried readings.
        freezingRoadStationCount:
          type: integer
          description: Number of corridor stations with road-surface temperature at or below 0°C, using the same fresh-first fallback rule as the rest of the summary.
        strongWindStationCount:
          type: integer
          description: Number of corridor stations with notable wind exposure, counted when sustained wind reaches 15 m/s or gusts reach 20 m/s.
        lowVisibilityStationCount:
          type: integer
          description: Number of corridor stations reporting 5 km visibility or less, using the same fresh-first fallback rule as the rest of the summary.
        precipitationStationCount:
          type: integer
          description: Number of corridor stations reporting at least 0.2 mm precipitation, which filters out trace noise while still surfacing active roadside precipitation.
        freshestObservedAt:
          type: string
          format: date-time
        airTemperatureMinC:
          type: number
        airTemperatureMinStation:
          $ref: '#/components/schemas/RoadReportWeatherStationRef'
        airTemperatureMaxC:
          type: number
        airTemperatureMaxStation:
          $ref: '#/components/schemas/RoadReportWeatherStationRef'
        windSpeedMaxMs:
          type: number
        windSpeedMaxStation:
          $ref: '#/components/schemas/RoadReportWeatherStationRef'
        gustSpeedMaxMs:
          type: number
        gustSpeedMaxStation:
          $ref: '#/components/schemas/RoadReportWeatherStationRef'
        roadTemperatureMinC:
          type: number
        roadTemperatureMinStation:
          $ref: '#/components/schemas/RoadReportWeatherStationRef'
        visibilityMinM:
          type: integer
          minimum: 0
        visibilityMinStation:
          $ref: '#/components/schemas/RoadReportWeatherStationRef'
        precipitationMaxMm:
          type: number
          minimum: 0
        precipitationMaxStation:
          $ref: '#/components/schemas/RoadReportWeatherStationRef'
    RoadReportStation:
      type: object
      additionalProperties: false
      required:
      - stationId
      - name
      - lat
      - lon
      - source
      - context
      - isActive
      properties:
        stationId:
          type: string
        name:
          type: string
        lat:
          type: number
          format: double
        lon:
          type: number
          format: double
        source:
          type: string
        context:
          type: string
          enum:
          - matched_segment
          - linked_alert_station
          - same_road_corridor
        roadNumber:
          type:
          - string
          - 'null'
        roadLabel:
          type:
          - string
          - 'null'
        distanceKm:
          type: number
          format: double
        isActive:
          type: boolean
        statusReason:
          type:
          - string
          - 'null'
        statusObservedAt:
          type: string
          format: date-time
        latest:
          anyOf:
          - $ref: '#/components/schemas/RoadReportLatestSummary'
          - type: 'null'
    RoadReportAliasRewrite:
      type: object
      additionalProperties: false
      required:
      - from
      - to
      properties:
        from:
          type: string
        to:
          type: string
    RoadReportMatchEvidence:
      type: object
      additionalProperties: false
      required:
      - source
      - text
      properties:
        source:
          type: string
          enum:
          - road_number
          - road_name
          - title
          - matched_segment_description
          - matched_segment_name
          - station_road_label
          - station_name
          - near_context
        text:
          type: string
          minLength: 1
          description: Exact persisted label or query-context snippet that made the matcher treat this road candidate as a plausible fit.
    RoadReportSpanQuery:
      type: object
      additionalProperties: false
      required:
      - from
      - to
      properties:
        from:
          type: string
        to:
          type: string
    RoadReportResolution:
      type: object
      additionalProperties: false
      required:
      - normalizedQuery
      - matchedBy
      - usedAlias
      - usedDiacriticFolds
      - usedSpanQuery
      - usedNearQuery
      - alternativeCount
      - ambiguous
      - bestScore
      properties:
        normalizedQuery:
          type: string
        matchedBy:
          type: array
          description: Compact explanation of which persisted road fields helped the query resolve. This is explainability metadata for the existing matcher, not a route-search trace.
          items:
            type: string
            enum:
            - road_number
            - road_name
            - title
            - matched_segment_description
            - matched_segment_name
            - station_road_label
            - station_name
            - near_context
            - ranked_alert
        matchEvidence:
          type: array
          description: Exact persisted labels or parsed local-context snippets that matched the query for the winning road candidate.
          items:
            $ref: '#/components/schemas/RoadReportMatchEvidence'
        usedAlias:
          type: boolean
        aliasRewrites:
          type: array
          description: Exact query alias rewrites applied during normalization before scoring. This stays intentionally compact so clients can explain alias handling without rebuilding matcher rules.
          items:
            $ref: '#/components/schemas/RoadReportAliasRewrite'
        usedDiacriticFolds:
          type: boolean
        usedSpanQuery:
          type: boolean
        usedNearQuery:
          type: boolean
        nearQuery:
          type:
          - string
          - 'null'
          description: Parsed nearby-location phrase from queries such as `road 1 near Núpsvötn` when the matcher recognized explicit local context.
        spanQuery:
          oneOf:
          - $ref: '#/components/schemas/RoadReportSpanQuery'
          - type: 'null'
          description: Parsed span endpoints from queries such as `between X and Y` when the matcher recognized that phrasing.
        alternativeCount:
          type: integer
          minimum: 0
          description: Number of additional active matches that also fit the query after the winning match is selected.
        ambiguous:
          type: boolean
          description: True when the runner-up match score is close enough to the winner that clients should treat the resolution as plausibly ambiguous.
        bestScore:
          type: number
          format: double
          minimum: 0
          maximum: 1
          description: Best internal matcher score for the selected active road report candidate.
        runnerUpScore:
          type:
          - number
          - 'null'
          format: double
          minimum: 0
          maximum: 1
          description: Internal matcher score for the next-best active candidate when another plausible match exists.
    RoadReport:
      type: object
      additionalProperties: false
      required:
      - query
      - resolution
      - resolvedRoad
      - currentStatus
      - adjacentRoads
      - connectedRoads
      - otherMatches
      - weatherSummary
      - nearbyStations
      - updatedAt
      properties:
        query:
          type: string
        resolution:
          $ref: '#/components/schemas/RoadReportResolution'
        resolvedRoad:
          $ref: '#/components/schemas/RoadReportResolvedRoad'
        currentStatus:
          $ref: '#/components/schemas/RoadReportCurrentStatus'
        highlights:
          type: array
          description: Compact backend-generated report headlines derived from the current road status plus bounded corridor weather signals. This is a deterministic summary aid, not a full hazard score.
          items:
            $ref: '#/components/schemas/RoadReportHighlight'
        adjacentRoads:
          type: array
          items:
            $ref: '#/components/schemas/RoadReportAdjacentRoad'
        connectedRoads:
          type: array
          description: Nearby cross-road alerts linked through the matched alert's station corridor. This is a bounded proximity heuristic, not a full road-graph adjacency model.
          items:
            $ref: '#/components/schemas/RoadReportAdjacentRoad'
        otherMatches:
          type: array
          description: Other active road-condition matches that also fit the query after the best match wins. This is a compact disambiguation list, not a full search result set.
          items:
            $ref: '#/components/schemas/RoadReportAlternativeHit'
        weatherSummary:
          $ref: '#/components/schemas/RoadReportWeatherSummary'
        nearbyStations:
          type: array
          items:
            $ref: '#/components/schemas/RoadReportStation'
        updatedAt:
          type: string
          format: date-time
    RoadReportResponse:
      type: object
      additionalProperties: false
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/RoadReport'
    StationResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/Station'
      additionalProperties: false
    StationListResponse:
      type: object
      description: Station-list envelope with zero-based offset paging metadata.
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/StationListItem'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
          description: Includes the requested `limit` and the zero-based `nextCursor` to send on the next request when another page exists.
      additionalProperties: false
    StationStatsResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/StationStats'
      additionalProperties: false
    ObservationResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/Observation'
      additionalProperties: false
    ObservationListResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Observation'
      additionalProperties: false
    NearestStationResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/NearestStationResult'
      additionalProperties: false
    ErrorLogListResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ErrorLog'
      additionalProperties: false
    UserResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/User'
      additionalProperties: false
    UserBootstrapResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: object
          required:
          - user
          - accessToken
          properties:
            user:
              $ref: '#/components/schemas/User'
            accessToken:
              type: string
              minLength: 1
          additionalProperties: false
      additionalProperties: false
    SubscriptionResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/Subscription'
      additionalProperties: false
    SubscriptionListResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Subscription'
      additionalProperties: false
    DeviceTokenResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/DeviceToken'
      additionalProperties: false
    DeviceTokenListResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/DeviceToken'
      additionalProperties: false
    WidgetConfigResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/WidgetConfig'
      additionalProperties: false
    NotificationSubscriptionResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/NotificationSubscription'
      additionalProperties: false
    NotificationSubscriptionListResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/NotificationSubscription'
      additionalProperties: false
    UsageStatsResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/UsageStats'
      additionalProperties: false
    UpdateSubscriptionRequest:
      type: object
      required:
      - plan
      - status
      - startedAt
      properties:
        plan:
          type: string
          enum:
          - free
          - plus
          - pro
          - lifetime
        status:
          type: string
          enum:
          - active
          - trialing
          - paused
          - canceled
          - expired
        startedAt:
          type: string
          format: date-time
        expiresAt:
          type:
          - string
          - 'null'
          format: date-time
      additionalProperties: false
    UpdateDeviceTokenRequest:
      type: object
      required:
      - platform
      - token
      - environment
      properties:
        platform:
          type: string
          enum:
          - ios
          - android
          - web
        token:
          type: string
          minLength: 1
          maxLength: 4096
        environment:
          type: string
          enum:
          - development
          - production
      additionalProperties: false
    UpdateNotificationSubscriptionRequest:
      type: object
      required:
      - lat
      - lon
      - eventType
      - enabled
      properties:
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        stationId:
          anyOf:
          - $ref: '#/components/schemas/StationId'
          - type: 'null'
          description: Optional override. If omitted, the backend may resolve the
            nearest station from lat/lon.
        eventType:
          type: string
          enum:
          - wind_speed
          - gust_speed
          - road_temperature
          - air_temperature
          - alert
        thresholdValue:
          type:
          - number
          - 'null'
        quietHoursStart:
          type:
          - string
          - 'null'
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        quietHoursEnd:
          type:
          - string
          - 'null'
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        scheduleEnabled:
          type:
          - boolean
          - 'null'
        scheduleTimezone:
          type:
          - string
          - 'null'
        scheduleAllowedDays:
          type: array
          items:
            type: string
            enum:
            - mon
            - tue
            - wed
            - thu
            - fri
            - sat
            - sun
        scheduleWindowStart:
          type:
          - string
          - 'null'
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        scheduleWindowEnd:
          type:
          - string
          - 'null'
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        schedule:
          anyOf:
          - $ref: '#/components/schemas/NotificationSchedule'
          - type: 'null'
          description: Preferred explicit allow-schedule. When provided, the backend mirrors it into the flat schedule fields for compatibility while quiet-hours remain temporary bridge fields.
        enabled:
          type: boolean
      additionalProperties: false
    NearestLatestObservationResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: object
          required:
          - nearestStation
          - latestReading
          properties:
            nearestStation:
              $ref: '#/components/schemas/NearestStationResult'
            latestReading:
              $ref: '#/components/schemas/Observation'
          additionalProperties: false
      additionalProperties: false
    DriveContextRequest:
      type: object
      required:
      - lat
      - lon
      properties:
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        headingDeg:
          type:
          - number
          - 'null'
        speedMs:
          type:
          - number
          - 'null'
        accuracyM:
          type:
          - number
          - 'null'
        alertRadiusM:
          type:
          - integer
          - 'null'
        corridorRadiusM:
          type:
          - integer
          - 'null'
        lookaheadSeconds:
          type:
          - integer
          - 'null'
        gustThresholdMs:
          description: Optional wind-risk threshold in m/s. The backend clamps it and applies it to the dedicated `windRisk` semantic, using gust when available and otherwise falling back to sustained wind.
          type:
          - number
          - 'null'
        vehicleProfile:
          description: Optional vehicle profile hint for the dedicated `windRisk` semantic. The backend currently accepts `car` and `truck`; `truck` keeps the same measured wind truth but escalates threshold exceedance severity more conservatively.
          type:
          - string
          - 'null'
          enum:
          - car
          - truck
          - 'null'
        grossWeightTons:
          type:
          - number
          - 'null'
        routePolyline:
          type:
          - string
          - 'null'
        locale:
          type:
          - string
          - 'null'
      additionalProperties: false
    RouteContextPoint:
      type: object
      required:
      - lat
      - lon
      properties:
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        label:
          type:
          - string
          - 'null'
      additionalProperties: false
    RouteContextRequest:
      type: object
      required:
      - origin
      - destination
      properties:
        origin:
          $ref: '#/components/schemas/RouteContextPoint'
        destination:
          $ref: '#/components/schemas/RouteContextPoint'
        waypoints:
          type: array
          items:
            $ref: '#/components/schemas/RouteContextPoint'
        encodedPolyline:
          type:
          - string
          - 'null'
        vehicleProfile:
          type:
          - string
          - 'null'
          enum:
          - car
          - truck
          - 'null'
        locale:
          type:
          - string
          - 'null'
      additionalProperties: false
    DriveContextLocation:
      type: object
      required:
      - lat
      - lon
      properties:
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
        headingDeg:
          type:
          - number
          - 'null'
        speedMs:
          type:
          - number
          - 'null'
        accuracyM:
          type:
          - number
          - 'null'
      additionalProperties: false
    DriveRoadContext:
      type: object
      required:
      - scope
      - provenance
      - localToStation
      properties:
        segmentId:
          type:
          - string
          - 'null'
        matchedSegmentId:
          type:
          - string
          - 'null'
        predefinedLocationId:
          type:
          - string
          - 'null'
        derivedIdbutur:
          type:
          - string
          - 'null'
        roadNumber:
          type:
          - string
          - 'null'
        roadLabel:
          type:
          - string
          - 'null'
        segmentLabel:
          type:
          - string
          - 'null'
        distanceM:
          type:
          - integer
          - 'null'
        matchingMode:
          type:
          - string
          - 'null'
        confidence:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1
        scope:
          type: string
          enum:
          - station_local
        provenance:
          type: string
        localToStation:
          type: boolean
      additionalProperties: false
    DriveRoadStatus:
      type: object
      required:
      - state
      - title
      - message
      - source
      properties:
        state:
          type: string
          enum:
          - normal
          - info
          - warning
          - critical
        title:
          type: string
        message:
          type: string
        source:
          type: string
        observedAt:
          type:
          - string
          - 'null'
          format: date-time
        matchedRoadConditionId:
          type:
          - string
          - 'null'
        matchedSegmentId:
          type:
          - string
          - 'null'
        predefinedLocationId:
          type:
          - string
          - 'null'
        derivedIdbutur:
          type:
          - string
          - 'null'
      additionalProperties: false
    DriveNearestWeatherObservation:
      type: object
      properties:
        windSpeedMs:
          type:
          - number
          - 'null'
        gustSpeedMs:
          type:
          - number
          - 'null'
        temperatureC:
          type:
          - number
          - 'null'
        observedAt:
          type:
          - string
          - 'null'
          format: date-time
      additionalProperties: false
    DriveNearestWeatherStation:
      type: object
      required:
      - stationId
      - stationName
      - onMatchedRoad
      - relation
      - observation
      properties:
        stationId:
          type: string
        stationName:
          type: string
        roadNumber:
          type:
          - string
          - 'null'
        segmentLabel:
          type:
          - string
          - 'null'
        distanceM:
          type:
          - integer
          - 'null'
        onMatchedRoad:
          type: boolean
        relation:
          type: string
          enum:
          - same_station
          - same_road
          - nearby_station
        observation:
          $ref: '#/components/schemas/DriveNearestWeatherObservation'
      additionalProperties: false
    DriveWindRisk:
      type: object
      required:
      - state
      - headline
      - message
      - scope
      - provenance
      - source
      - vehicleProfile
      - thresholdSource
      - gustThresholdMs
      - measurementType
      - measuredValueMs
      - stationId
      - stationName
      - relation
      - onMatchedRoad
      properties:
        state:
          type: string
          enum:
          - normal
          - warning
          - critical
        headline:
          type: string
        message:
          type: string
        scope:
          type: string
          enum:
          - station_local
        provenance:
          type: string
          enum:
          - nearest_weather_station
        source:
          type: string
        vehicleProfile:
          type: string
          enum:
          - car
          - truck
        thresholdSource:
          type: string
          enum:
          - request
          - default
        gustThresholdMs:
          type: number
        measurementType:
          type: string
          enum:
          - gust
          - wind_speed
        measuredValueMs:
          type: number
        windSpeedMs:
          type:
          - number
          - 'null'
        gustSpeedMs:
          type:
          - number
          - 'null'
        stationId:
          type: string
        stationName:
          type: string
        distanceM:
          type:
          - integer
          - 'null'
        relation:
          type: string
          enum:
          - same_station
          - same_road
          - nearby_station
        onMatchedRoad:
          type: boolean
        observedAt:
          type:
          - string
          - 'null'
          format: date-time
      additionalProperties: false
    DriverAlertLocation:
      type: object
      required:
      - lat
      - lon
      properties:
        lat:
          $ref: '#/components/schemas/Latitude'
        lon:
          $ref: '#/components/schemas/Longitude'
      additionalProperties: false
    DriverAlert:
      type: object
      required:
      - id
      - kind
      - subtype
      - severity
      - title
      - message
      - source
      - ahead
      - raw
      properties:
        id:
          type: string
        kind:
          type: string
        subtype:
          type: string
        severity:
          type: string
          enum:
          - info
          - warning
          - critical
        title:
          type: string
        message:
          type: string
        source:
          type: string
        segmentId:
          type:
          - string
          - 'null'
        matchedSegmentId:
          type:
          - string
          - 'null'
        predefinedLocationId:
          type:
          - string
          - 'null'
        derivedIdbutur:
          type:
          - string
          - 'null'
        roadNumber:
          type:
          - string
          - 'null'
        roadLabel:
          type:
          - string
          - 'null'
        segmentLabel:
          type:
          - string
          - 'null'
        stationId:
          type:
          - string
          - 'null'
        stationName:
          type:
          - string
          - 'null'
        distanceM:
          type:
          - integer
          - 'null'
        ahead:
          type: boolean
        observedAt:
          type:
          - string
          - 'null'
          format: date-time
        expiresAt:
          type:
          - string
          - 'null'
          format: date-time
        location:
          oneOf:
          - $ref: '#/components/schemas/DriverAlertLocation'
          - type: 'null'
        raw:
          type: object
          additionalProperties: true
      additionalProperties: false
    DriveNearbyLinkedSegment:
      type: object
      required:
      - relation
      - userFacingTier
      - contextScope
      - contextSource
      - primaryLocal
      properties:
        relation:
          type: string
          enum:
          - same_segment
          - same_road_corridor
          - connected_road
          - nearby_linked_road
        userFacingTier:
          type: string
          enum:
          - same_segment
          - same_road
          - adjacent_road
          - nearby_road
        contextScope:
          type: string
          enum:
          - contextual_nearby
        contextSource:
          type: string
          enum:
          - station_incident
          - public_alert
          - datex
        primaryLocal:
          type: boolean
        segmentId:
          type:
          - string
          - 'null'
        matchedSegmentId:
          type:
          - string
          - 'null'
        predefinedLocationId:
          type:
          - string
          - 'null'
        derivedIdbutur:
          type:
          - string
          - 'null'
        roadNumber:
          type:
          - string
          - 'null'
        segmentLabel:
          type:
          - string
          - 'null'
        distanceM:
          type:
          - integer
          - 'null'
        confidence:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1
        status:
          oneOf:
          - $ref: '#/components/schemas/DriveRoadStatus'
          - type: 'null'
      additionalProperties: false
    DriveExpandedRoadContext:
      type: object
      required:
      - available
      - defaultExpanded
      - nearbyLinkedSegments
      properties:
        available:
          type: boolean
        defaultExpanded:
          type: boolean
        nearbyLinkedSegments:
          type: array
          items:
            $ref: '#/components/schemas/DriveNearbyLinkedSegment'
      additionalProperties: false
    DriveLiveStatus:
      type: object
      required:
      - state
      - headline
      - severity
      - moreAlertsCount
      - updatedAt
      properties:
        state:
          type: string
          enum:
          - normal
          - info
          - warning
          - critical
        headline:
          type: string
        subheadline:
          type:
          - string
          - 'null'
        severity:
          type: string
          enum:
          - info
          - warning
          - critical
        distanceM:
          type:
          - integer
          - 'null'
        primaryAlertId:
          type:
          - string
          - 'null'
        moreAlertsCount:
          type: integer
        updatedAt:
          type: string
          format: date-time
      additionalProperties: false
    LiveTravelSurfaceSummary:
      type: object
      required:
      - surfaceVersion
      - mode
      - state
      - headline
      - severity
      - moreAlertsCount
      - deepLink
      - updatedAt
      - computedAt
      - routeAware
      - derivedFromAnchor
      properties:
        surfaceVersion:
          type: string
        mode:
          type: string
          enum:
          - current_context
          - route_context
        state:
          type: string
          enum:
          - normal
          - info
          - warning
          - critical
        headline:
          type: string
        subheadline:
          type:
          - string
          - 'null'
        severity:
          type: string
          enum:
          - info
          - warning
          - critical
        primaryAlertId:
          type:
          - string
          - 'null'
        moreAlertsCount:
          type: integer
        primaryRoadLabel:
          type:
          - string
          - 'null'
        secondaryRoadLabel:
          type:
          - string
          - 'null'
        nextStationName:
          type:
          - string
          - 'null'
        nextStationRelation:
          type:
          - string
          - 'null'
        deepLink:
          type: string
        updatedAt:
          type: string
          format: date-time
        computedAt:
          type: string
          format: date-time
        routeAware:
          type: boolean
        derivedFromAnchor:
          type: string
      additionalProperties: false
    DriveContextMeta:
      type: object
      required:
      - computedAt
      - guestMode
      - semanticsVersion
      properties:
        computedAt:
          type: string
          format: date-time
        guestMode:
          type: boolean
        semanticsVersion:
          type: string
      additionalProperties: false
    DriveContext:
      type: object
      required:
      - location
      - roadContext
      - roadStatus
      - nearestWeatherStation
      - windRisk
      - expandedRoadContext
      - alerts
      - liveStatus
      - liveSurfaceSummary
      - guestModeAllowed
      - subscriptionTierRequired
      - meta
      properties:
        location:
          $ref: '#/components/schemas/DriveContextLocation'
        roadContext:
          $ref: '#/components/schemas/DriveRoadContext'
        roadStatus:
          oneOf:
          - $ref: '#/components/schemas/DriveRoadStatus'
          - type: 'null'
        nearestWeatherStation:
          oneOf:
          - $ref: '#/components/schemas/DriveNearestWeatherStation'
          - type: 'null'
        windRisk:
          oneOf:
          - $ref: '#/components/schemas/DriveWindRisk'
          - type: 'null'
        expandedRoadContext:
          $ref: '#/components/schemas/DriveExpandedRoadContext'
        alerts:
          type: array
          items:
            $ref: '#/components/schemas/DriverAlert'
        liveStatus:
          $ref: '#/components/schemas/DriveLiveStatus'
        liveSurfaceSummary:
          $ref: '#/components/schemas/LiveTravelSurfaceSummary'
        guestModeAllowed:
          type: boolean
          description: True for the current free guest-first Ferð baseline.
        subscriptionTierRequired:
          type: string
          description: Explicit gate for the current drive-context surface or future route-aware upgrades.
          enum:
          - none
          - subscriber
        meta:
          $ref: '#/components/schemas/DriveContextMeta'
      additionalProperties: false
    RouteContextSegment:
      type: object
      required:
      - matchingMode
      - contextScope
      - provenance
      properties:
        segmentId:
          type:
          - string
          - 'null'
        matchedSegmentId:
          type:
          - string
          - 'null'
        predefinedLocationId:
          type:
          - string
          - 'null'
        derivedIdbutur:
          type:
          - string
          - 'null'
        roadNumber:
          type:
          - string
          - 'null'
        roadLabel:
          type:
          - string
          - 'null'
        segmentLabel:
          type:
          - string
          - 'null'
        distanceM:
          type:
          - integer
          - 'null'
        matchingMode:
          type: string
          enum:
          - route_geometry_nearest
          - origin_anchor
        confidence:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1
        contextScope:
          type: string
          enum:
          - route_corridor
          - origin_local
        provenance:
          type: string
        status:
          oneOf:
          - $ref: '#/components/schemas/DriveRoadStatus'
          - type: 'null'
      additionalProperties: false
    RouteNearestWeatherStation:
      type: object
      required:
      - stationId
      - stationName
      - relation
      - observation
      - derivedFromAnchor
      properties:
        stationId:
          type: string
        stationName:
          type: string
        roadNumber:
          type:
          - string
          - 'null'
        segmentLabel:
          type:
          - string
          - 'null'
        distanceM:
          type:
          - integer
          - 'null'
        relation:
          type: string
          enum:
          - matched_route_segment_station
          - route_corridor_station
          - route_nearby_station
          - origin_anchor_station
        observation:
          $ref: '#/components/schemas/DriveNearestWeatherObservation'
        derivedFromAnchor:
          type: string
          enum:
          - route_geometry
          - origin_local_context
      additionalProperties: false
    RouteCompactLiveSummary:
      type: object
      required:
      - state
      - headline
      - severity
      - moreAlertsCount
      - updatedAt
      - routeAware
      - derivedFromAnchor
      properties:
        state:
          type: string
          enum:
          - normal
          - info
          - warning
          - critical
        headline:
          type: string
        subheadline:
          type:
          - string
          - 'null'
        severity:
          type: string
          enum:
          - info
          - warning
          - critical
        primaryAlertId:
          type:
          - string
          - 'null'
        moreAlertsCount:
          type: integer
        updatedAt:
          type: string
          format: date-time
        routeAware:
          type: boolean
        derivedFromAnchor:
          type: string
          enum:
          - route_geometry
          - origin_local_context
      additionalProperties: false
    RouteProgressAnchor:
      type: object
      required:
      - anchorType
      - origin
      - snapped
      properties:
        anchorType:
          type: string
          enum:
          - route_geometry
          - origin
        origin:
          $ref: '#/components/schemas/RouteContextPoint'
        snapped:
          type: boolean
      additionalProperties: false
    RouteContextConfidence:
      type: object
      required:
      - overall
      - routeMatchImplemented
      - notes
      properties:
        overall:
          type: string
          enum:
          - geometry_matched
          - scaffold_only
        routeMatchImplemented:
          type: boolean
        notes:
          type: array
          items:
            type: string
      additionalProperties: false
    RouteContextProvenance:
      type: object
      required:
      - primarySource
      - sources
      properties:
        primarySource:
          type: string
          enum:
          - vegagerdin_route_geometry
          - origin_drive_context
        sources:
          type: array
          items:
            type: string
      additionalProperties: false
    RouteContextMeta:
      type: object
      required:
      - computedAt
      - semanticsVersion
      - routeAware
      properties:
        computedAt:
          type: string
          format: date-time
        semanticsVersion:
          type: string
        routeAware:
          type: boolean
      additionalProperties: false
    RouteContext:
      type: object
      required:
      - matchedRouteSegments
      - routeAlerts
      - nextStationsOnRoute
      - compactLiveSummary
      - liveSurfaceSummary
      - routeProgressAnchor
      - confidence
      - provenance
      - subscriptionTierRequired
      - meta
      properties:
        matchedRouteSegments:
          type: array
          items:
            $ref: '#/components/schemas/RouteContextSegment'
        routeAlerts:
          type: array
          items:
            $ref: '#/components/schemas/DriverAlert'
        nextStationsOnRoute:
          type: array
          items:
            $ref: '#/components/schemas/RouteNearestWeatherStation'
        compactLiveSummary:
          $ref: '#/components/schemas/RouteCompactLiveSummary'
        liveSurfaceSummary:
          $ref: '#/components/schemas/LiveTravelSurfaceSummary'
        routeProgressAnchor:
          $ref: '#/components/schemas/RouteProgressAnchor'
        confidence:
          $ref: '#/components/schemas/RouteContextConfidence'
        provenance:
          $ref: '#/components/schemas/RouteContextProvenance'
        subscriptionTierRequired:
          type: string
          enum:
          - subscriber
        meta:
          $ref: '#/components/schemas/RouteContextMeta'
      additionalProperties: false
    RouteContextResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/RouteContext'
      additionalProperties: false
    DriveContextResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: '#/components/schemas/DriveContext'
      additionalProperties: false
    StationListItem:
      type: object
      required:
      - id
      - name
      - slug
      - region
      - lat
      - lon
      - source
      - roadNumber
      - road
      - alerts
      - updatedAt
      - isActive
      - statusReason
      - statusObservedAt
      - lastObservationAt
      - observationSource
      properties:
        id: *id002
        name: *id003
        slug: *id004
        region: *id005
        lat: *id006
        lon: *id007
        source: *id008
        elevationM: *id009
        roadNumber: *id010
        road: *id017
        alerts: *id018
        distance:
          $ref: '#/components/schemas/StationDistance'
        isActive: *id011
        statusReason: *id012
        statusObservedAt: *id013
        lastObservationAt: *id014
        observationSource: *id015
        updatedAt: *id016
      additionalProperties: false
