openapi: "3.1.0"
info:
  title: Framedash API
  version: "1.0"
  description: |
    The Framedash API provides programmatic access to game performance telemetry,
    project management, map management, content registry, and analytics queries.

    ## Authentication

    All requests require an API key sent via the `X-API-Key` header.
    API keys can be generated from the Framedash dashboard under
    **Settings > API Keys**.

    ## Response Format

    ### Web API

    All Web API responses use the envelope:
    ```json
    { "success": true, "data": { ... } }
    ```

    Error responses:
    ```json
    { "success": false, "error": "Error message" }
    ```

    ### Ingest API

    The Event Ingestion endpoint (`/v1/events`) uses a different format:
    ```json
    { "status": "accepted" }
    ```

servers:
  - url: https://api.framedash.dev/api
    description: Web API (Projects, Maps, Content, Query)

security:
  - ApiKeyAuth: []

tags:
  - name: Projects
    description: Manage projects and view project status.
  - name: Analytics
    description: Dashboard KPIs, heatmaps, retention, funnels, and insights.
  - name: Alerts
    description: Create, manage, and monitor performance alert rules.
  - name: Maps
    description: Upload, list, and delete game maps.
  - name: Content Registry
    description: Manage game content entries (items, weapons, levels, etc.).
  - name: Query
    description: Execute SQL-like analytics queries against telemetry data.
  - name: Event Ingestion
    description: Ingest telemetry events from game SDKs.

paths:
  /v1/projects:
    get:
      operationId: listProjects
      summary: List projects
      description: Returns all projects belonging to the authenticated tenant.
      tags: [Projects]
      responses:
        "200":
          description: List of projects.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/status:
    get:
      operationId: getProjectStatus
      summary: Get project status
      description: Returns the project details and key performance indicators (KPIs).
      tags: [Projects]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: Project status and KPIs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/dashboard:
    get:
      operationId: getDashboard
      summary: Get dashboard metrics
      description: |
        Returns dashboard KPIs, daily active user time series, and top events
        for the specified project and time period.
      tags: [Analytics]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/Days"
      responses:
        "200":
          description: Dashboard metrics.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/DashboardData"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/heatmap:
    get:
      operationId: getHeatmap
      summary: Get heatmap data
      description: |
        Returns aggregated heatmap cells for a specific map. Each cell contains
        performance metrics (FPS, frame time, memory, GPU time) and event counts.
        Use in Performance mode (no eventName) for QA or Events mode (with eventName)
        for game design analysis.
      tags: [Analytics]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: mapId
          in: query
          required: true
          description: User-specified map identifier to query heatmap data for.
          schema:
            type: string
            maxLength: 255
        - name: cellSize
          in: query
          required: false
          description: Grid cell size in world units.
          schema:
            type: integer
            enum: [5, 10, 25, 50]
            default: 25
        - name: days
          in: query
          required: false
          description: Time period in days.
          schema:
            type: integer
            enum: [1, 7, 14, 30]
            default: 7
        - name: eventName
          in: query
          required: false
          description: Filter by event name (e.g., `player.death`). Omit for performance mode.
          schema:
            type: string
      responses:
        "200":
          description: Array of heatmap cells.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/HeatmapCell"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/retention:
    get:
      operationId: getRetention
      summary: Get retention cohorts
      description: |
        Returns player retention cohort data showing day-1, day-7, and day-30
        retention rates for each cohort date within the specified time period.
      tags: [Analytics]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/Days"
      responses:
        "200":
          description: Array of retention cohorts.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/RetentionCohort"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/funnels:
    get:
      operationId: getFunnels
      summary: Get funnel analysis
      description: |
        Performs funnel analysis on a sequence of events. Returns step-by-step
        conversion rates showing how many unique players completed each step
        within the specified time window.
      tags: [Analytics]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: steps
          in: query
          required: true
          description: Comma-separated list of event names defining funnel steps (2-8 steps).
          schema:
            type: string
          example: "player.spawn,player.death,player.respawn"
        - name: window
          in: query
          required: false
          description: Maximum time in seconds between first and last step for a player to count.
          schema:
            type: integer
            enum: [3600, 21600, 86400, 604800]
            default: 86400
        - $ref: "#/components/parameters/Days"
      responses:
        "200":
          description: Array of funnel steps with conversion rates.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/FunnelStep"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/insights:
    get:
      operationId: getInsights
      summary: Get insights
      description: |
        Returns aggregated analytics data grouped by a specified dimension.
        Useful for exploring event distributions, platform breakdowns, and
        time-series trends.
      tags: [Analytics]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: metric
          in: query
          required: true
          description: Metric to aggregate.
          schema:
            type: string
            enum: [count, unique_players]
        - name: groupBy
          in: query
          required: true
          description: Dimension to group results by.
          schema:
            type: string
            enum: [day, week, month, event_name, map_id, platform, build_id]
        - $ref: "#/components/parameters/Days"
        - name: limit
          in: query
          required: false
          description: Maximum number of groups to return.
          schema:
            type: integer
            enum: [10, 20, 50]
            default: 10
        - name: eventName
          in: query
          required: false
          description: Filter by event name.
          schema:
            type: string
      responses:
        "200":
          description: Aggregated insights data.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/InsightsResult"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/alerts:
    get:
      operationId: listAlertRules
      summary: List alert rules
      description: |
        Returns all alert rules for the specified project, including joined
        map names, threshold profile names, latest evaluation status, and
        notification channel IDs.
      tags: [Alerts]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: Array of alert rules.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/AlertRuleListItem"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      operationId: createAlertRule
      summary: Create an alert rule
      description: |
        Creates a new performance alert rule for the project. The rule monitors
        a specific metric on a map and triggers notifications when the failure
        percentage exceeds the threshold.
      tags: [Alerts]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AlertRuleCreate"
      responses:
        "201":
          description: Alert rule created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/AlertRuleDetail"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Plan limit reached or feature not available.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/alerts/{alertId}:
    get:
      operationId: getAlertRule
      summary: Get an alert rule
      description: Returns a single alert rule with its notification channel IDs.
      tags: [Alerts]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/AlertId"
      responses:
        "200":
          description: Alert rule details.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/AlertRuleDetail"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

    patch:
      operationId: updateAlertRule
      summary: Update an alert rule
      description: |
        Partially updates an alert rule. All fields are optional. To reactivate
        a deactivated rule, set `isActive: true` (subject to plan quota).
      tags: [Alerts]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/AlertId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AlertRuleUpdate"
      responses:
        "200":
          description: Alert rule updated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/AlertRuleDetail"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Plan limit reached (e.g., reactivating when at max rules).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      operationId: deactivateAlertRule
      summary: Deactivate an alert rule
      description: |
        Deactivates an alert rule by setting `isActive` to `false`. The rule
        record is retained and can be reactivated via PATCH. This endpoint
        does **not** permanently delete the rule.
      tags: [Alerts]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/AlertId"
      responses:
        "200":
          description: Alert rule deactivated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/AlertDeactivateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/alerts/history:
    get:
      operationId: listAlertHistory
      summary: List alert evaluation history
      description: |
        Returns the evaluation history for all alert rules in the project,
        ordered by most recent first. Use pagination via `limit` and `offset`.
      tags: [Alerts]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: limit
          in: query
          required: false
          description: Maximum number of events to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of events to skip for pagination.
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Array of alert evaluation events.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/AlertEvent"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/maps:
    get:
      operationId: listMaps
      summary: List maps
      description: Returns all maps for the specified project, ordered by creation date (newest first).
      tags: [Maps]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: List of maps.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Map"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/projects/{id}/maps/{mapId}:
    delete:
      operationId: deleteMap
      summary: Delete a map
      description: |
        Delete a map by its user-specified map identifier (not the internal UUID).
        Only the database record is removed; the map image is retained.
      tags: [Maps]
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: mapId
          in: path
          required: true
          description: The user-specified map identifier.
          schema:
            type: string
      responses:
        "200":
          description: Map deleted successfully.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          mapId:
                            type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/maps/upload:
    post:
      operationId: uploadMap
      summary: Upload a map
      description: |
        Upload a map image with metadata via multipart/form-data.
        Upserts by (projectId, mapId) — if a map with the same ID exists, it is updated.
        Requires admin API key and `X-Project-Id` header.
      tags: [Maps]
      parameters:
        - $ref: "#/components/parameters/XProjectId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [name, mapId, image, worldMinX, worldMinY, worldMaxX, worldMaxY, imageWidth, imageHeight]
              properties:
                name:
                  type: string
                  description: Display name for the map (max 255 characters).
                mapId:
                  type: string
                  description: Unique map identifier within the project (max 255 characters).
                image:
                  type: string
                  format: binary
                  description: Map image file (PNG, JPEG, or WebP, max 10 MB).
                worldMinX:
                  type: number
                  description: Minimum X coordinate in world space.
                worldMinY:
                  type: number
                  description: Minimum Y coordinate in world space.
                worldMaxX:
                  type: number
                  description: Maximum X coordinate in world space.
                worldMaxY:
                  type: number
                  description: Maximum Y coordinate in world space.
                imageWidth:
                  type: integer
                  description: Image width in pixels.
                imageHeight:
                  type: integer
                  description: Image height in pixels.
      responses:
        "200":
          description: Map updated (existing map with same mapId was found).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          mapId:
                            type: string
                          action:
                            type: string
                            enum: [updated]
        "201":
          description: Map created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          mapId:
                            type: string
                          action:
                            type: string
                            enum: [created]
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/content:
    get:
      operationId: listContent
      summary: List content entries
      description: |
        Returns content entries for the project. Optionally filter by content type.
        Requires `X-Project-Id` header.
      tags: [Content Registry]
      parameters:
        - $ref: "#/components/parameters/XProjectId"
        - name: type
          in: query
          required: false
          description: Filter by content type (e.g., `weapon`, `map`, `event_type`).
          schema:
            type: string
      responses:
        "200":
          description: List of content entries.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/ContentEntry"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      operationId: upsertContent
      summary: Create or update content entries
      description: |
        Create or upsert content entries in bulk (up to 500 per request).
        Deduplicates by `(contentType, contentId)` — last entry wins.
        Requires `X-Project-Id` header.
      tags: [Content Registry]
      parameters:
        - $ref: "#/components/parameters/XProjectId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entries]
              properties:
                entries:
                  type: array
                  maxItems: 500
                  items:
                    $ref: "#/components/schemas/ContentEntryPayload"
      responses:
        "201":
          description: Content entries created/updated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          imported:
                            type: integer
                            description: Number of entries imported.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      operationId: deleteContent
      summary: Delete a content entry
      description: |
        Delete a content entry by UUID or by `(contentType, contentId)` pair.
        Use either `?id=UUID` or both `?contentType=X&contentId=Y`, not both methods.
        Requires `X-Project-Id` header.
      tags: [Content Registry]
      parameters:
        - $ref: "#/components/parameters/XProjectId"
        - name: id
          in: query
          required: false
          description: UUID of the content entry to delete.
          schema:
            type: string
            format: uuid
        - name: contentType
          in: query
          required: false
          description: Content type (used with `contentId`).
          schema:
            type: string
        - name: contentId
          in: query
          required: false
          description: Content identifier (used with `contentType`).
          schema:
            type: string
      responses:
        "200":
          description: Content entry deleted.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          deleted:
                            type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/query:
    post:
      operationId: executeQuery
      summary: Execute an analytics query
      description: |
        Execute a SQL-like analytics query against telemetry data stored in ClickHouse.
        Only `SELECT` statements are allowed. Queries are validated, rewritten to enforce
        tenant isolation, and executed against a read-only connection.
      tags: [Query]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, sql]
              properties:
                project_id:
                  type: string
                  format: uuid
                  description: The project ID to query.
                sql:
                  type: string
                  description: SQL query (SELECT only).
                limit:
                  type: integer
                  minimum: 1
                  maximum: 10000
                  default: 1000
                  description: Maximum number of rows to return.
      responses:
        "200":
          description: Query results.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          description: Query execution failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/events:
    post:
      operationId: ingestEvents
      summary: Ingest telemetry events
      description: |
        Ingest a batch of telemetry events from a game SDK.
        The request body must be a Protobuf-encoded `TelemetryBatch`.
        Optional gzip compression is supported via `Content-Encoding: gzip`.

        **Required headers:**
        - `X-API-Key` — write or CI API key
        - `Content-Type: application/x-protobuf`
        - `Content-Length` — must match actual body size
        - `X-SDK-Version` — SDK version identifier

        This endpoint is served by the Ingest Worker, separate from the Web API.
      tags: [Event Ingestion]
      servers:
        - url: https://ingest.framedash.dev
          description: Ingest API
      parameters:
        - name: Content-Encoding
          in: header
          required: false
          description: Set to `gzip` if the body is gzip-compressed.
          schema:
            type: string
            enum: [gzip]
        - name: X-SDK-Version
          in: header
          required: true
          description: SDK version identifier (e.g., `unity-1.2.0`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/x-protobuf:
            schema:
              type: string
              format: binary
              description: Protobuf-encoded TelemetryBatch.
      responses:
        "202":
          description: Events accepted for processing.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [accepted]
        "400":
          description: Invalid payload or missing required headers.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestErrorResponse"
        "401":
          description: Missing API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestErrorResponse"
        "403":
          description: Invalid API key or insufficient permissions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestErrorResponse"
        "411":
          description: Content-Length header is required.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestErrorResponse"
        "413":
          description: Payload exceeds maximum size.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestErrorResponse"
        "415":
          description: Unsupported Content-Type or Content-Encoding.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestErrorResponse"
        "429":
          description: Rate limit or monthly event budget exceeded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestErrorResponse"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        API key for authentication. Generate keys from the Framedash dashboard
        under **Settings > API Keys**.

        Key types:
        - **admin** — Full access to Web API endpoints
        - **write** — Event ingestion only
        - **ci** — Event ingestion (CI builds, bypasses billing)

  parameters:
    ProjectId:
      name: id
      in: path
      required: true
      description: Project UUID.
      schema:
        type: string
        format: uuid

    XProjectId:
      name: X-Project-Id
      in: header
      required: true
      description: Project UUID (sent as a header for endpoints that don't include it in the path).
      schema:
        type: string
        format: uuid

    AlertId:
      name: alertId
      in: path
      required: true
      description: Alert rule UUID.
      schema:
        type: string
        format: uuid

    Days:
      name: days
      in: query
      required: false
      description: Time period in days.
      schema:
        type: integer
        enum: [7, 14, 30, 90]
        default: 30

  headers:
    X-RateLimit-Limit:
      description: Maximum number of requests allowed per hour.
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Number of requests remaining in the current window.
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Unix timestamp when the rate limit window resets.
      schema:
        type: integer

  schemas:
    SuccessEnvelope:
      type: object
      properties:
        success:
          type: boolean
          const: true

    SuccessResponse:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              description: Response payload (varies by endpoint).

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          const: false
        error:
          type: string
          description: Human-readable error message.

    IngestErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message.

    # --- Analytics schemas ---

    DashboardData:
      type: object
      properties:
        kpis:
          $ref: "#/components/schemas/DashboardKpis"
        dailyActiveUsers:
          type: array
          items:
            $ref: "#/components/schemas/TimeSeriesPoint"
        topEvents:
          type: array
          items:
            $ref: "#/components/schemas/TopEvent"

    DashboardKpis:
      type: object
      description: Key performance indicators for the project dashboard.
      properties:
        dau:
          type: integer
          description: Daily active unique players.
        mau:
          type: integer
          description: Monthly active unique players.
        sessions:
          type: integer
          description: Total sessions in the period.
        events:
          type: integer
          description: Total events in the period.
        fetchedAt:
          type: number
          description: Unix timestamp (ms) when the ClickHouse query executed.

    TimeSeriesPoint:
      type: object
      properties:
        date:
          type: string
          description: Date in YYYY-MM-DD format.
          example: "2024-01-15"
        count:
          type: integer

    TopEvent:
      type: object
      properties:
        event_name:
          type: string
        count:
          type: integer

    HeatmapCell:
      type: object
      description: Aggregated performance and event data for a single grid cell.
      properties:
        x:
          type: number
          description: Cell center X coordinate in world space.
        y:
          type: number
          description: Cell center Y coordinate in world space.
        weight:
          type: number
          description: Normalized weight for visualization.
        event_count:
          type: integer
          description: Number of events in this cell.
        avg_fps:
          type: number
          description: Average FPS across all samples in this cell.
        avg_frame_time:
          type: number
          description: Average frame time (ms) across all samples.
        avg_memory:
          type: number
          description: Average memory usage (MB) across all samples.
        avg_gpu_time:
          type: [number, "null"]
          description: Average GPU time (ms), or null if no GPU data available.

    RetentionCohort:
      type: object
      description: Retention data for a single cohort (players who first appeared on cohort_date).
      properties:
        cohort_date:
          type: string
          description: Date the cohort was formed (YYYY-MM-DD).
          example: "2024-01-15"
        cohort_size:
          type: integer
          description: Number of unique players in this cohort.
        d1:
          type: integer
          description: Players who returned on day 1.
        d7:
          type: integer
          description: Players who returned on day 7.
        d30:
          type: integer
          description: Players who returned on day 30.
        d1_pct:
          type: number
          description: Day-1 retention percentage (0-100).
        d7_pct:
          type: number
          description: Day-7 retention percentage (0-100).
        d30_pct:
          type: number
          description: Day-30 retention percentage (0-100).

    FunnelStep:
      type: object
      properties:
        step:
          type: integer
          description: 1-indexed step number in the funnel.
        event_name:
          type: string
          description: Event name for this step.
        count:
          type: integer
          description: Number of unique players who reached this step.
        pct:
          type: number
          description: Percentage of step-1 players who reached this step.
        drop_off_pct:
          type: number
          description: Drop-off percentage from the previous step.

    InsightsResult:
      type: object
      properties:
        rows:
          type: array
          items:
            $ref: "#/components/schemas/InsightsRow"
        total:
          type: integer
          description: Total value across all groups.
        distinctGroups:
          type: integer
          description: Total number of distinct groups (may exceed returned rows if limited).

    InsightsRow:
      type: object
      properties:
        group:
          type: string
          description: Dimension value (date, event name, map ID, etc.).
        value:
          type: integer

    # --- Alert schemas ---

    AlertRuleListItem:
      type: object
      description: Alert rule with joined display names and latest status.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        mapId:
          type: string
          format: uuid
        mapName:
          type: [string, "null"]
          description: Display name of the associated map (joined).
        thresholdProfileId:
          type: string
          format: uuid
        thresholdProfileName:
          type: [string, "null"]
          description: Display name of the threshold profile (joined).
        metric:
          type: string
          enum: [fps, frame_time, memory, gpu_time]
        thresholdLevel:
          type: string
          enum: [warn, good]
        failPercentage:
          type: number
          description: Percentage of cells that must fail to trigger (1-100).
        evaluationDays:
          type: integer
          description: Number of days of data to evaluate.
        cellSize:
          type: integer
          description: Grid cell size used for evaluation.
        cooldownMinutes:
          type: integer
          description: Minimum minutes between notifications.
        isActive:
          type: boolean
        createdAt:
          type: string
          format: date-time
        latestStatus:
          type: [string, "null"]
          description: Status from the most recent evaluation (`triggered` or `resolved`).
        channelIds:
          type: array
          items:
            type: string
            format: uuid
          description: Notification channel UUIDs.

    AlertRuleCreate:
      type: object
      required:
        - name
        - mapId
        - thresholdProfileId
        - metric
        - thresholdLevel
        - failPercentage
        - evaluationDays
        - cellSize
        - cooldownMinutes
      properties:
        name:
          type: string
        mapId:
          type: string
          format: uuid
        thresholdProfileId:
          type: string
          format: uuid
        metric:
          type: string
          enum: [fps, frame_time, memory, gpu_time]
        thresholdLevel:
          type: string
          enum: [warn, good]
        failPercentage:
          type: number
          minimum: 1
          maximum: 100
        evaluationDays:
          type: integer
        cellSize:
          type: integer
          enum: [5, 10, 25, 50]
        cooldownMinutes:
          type: integer
          minimum: 1
        channelIds:
          type: array
          items:
            type: string
            format: uuid

    AlertRuleDetail:
      type: object
      description: Full alert rule including timestamps and channel IDs.
      properties:
        id:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        mapId:
          type: string
          format: uuid
        thresholdProfileId:
          type: string
          format: uuid
        name:
          type: string
        metric:
          type: string
          enum: [fps, frame_time, memory, gpu_time]
        thresholdLevel:
          type: string
          enum: [warn, good]
        failPercentage:
          type: number
        evaluationDays:
          type: integer
        cellSize:
          type: integer
        cooldownMinutes:
          type: integer
        isActive:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        channelIds:
          type: array
          items:
            type: string
            format: uuid

    AlertRuleUpdate:
      type: object
      description: Partial update for an alert rule. All fields are optional.
      properties:
        name:
          type: string
        mapId:
          type: string
          format: uuid
        thresholdProfileId:
          type: string
          format: uuid
        metric:
          type: string
          enum: [fps, frame_time, memory, gpu_time]
        thresholdLevel:
          type: string
          enum: [warn, good]
        failPercentage:
          type: number
          minimum: 1
          maximum: 100
        evaluationDays:
          type: integer
        cellSize:
          type: integer
          enum: [5, 10, 25, 50]
        cooldownMinutes:
          type: integer
          minimum: 1
        channelIds:
          type: array
          items:
            type: string
            format: uuid
        isActive:
          type: boolean
          description: Set to true to reactivate (subject to plan quota) or false to deactivate.

    AlertDeactivateResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        isActive:
          type: boolean
          const: false

    AlertEvent:
      type: object
      description: Record of a single alert rule evaluation.
      properties:
        id:
          type: string
          format: uuid
        alertRuleId:
          type: string
          format: uuid
        ruleName:
          type: string
          description: Name of the alert rule at time of evaluation.
        metric:
          type: string
        status:
          type: string
          enum: [triggered, resolved]
          description: Evaluation result.
        failCount:
          type: integer
          description: Number of cells that failed the threshold.
        totalCount:
          type: integer
          description: Total number of cells evaluated.
        failPercentage:
          type: number
          description: Actual failure percentage.
        notified:
          type: boolean
          description: Whether a notification was sent for this evaluation.
        createdAt:
          type: string
          format: date-time

    # --- Map & Content schemas ---

    Map:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Internal UUID primary key.
        name:
          type: string
          description: Display name of the map.
        mapId:
          type: string
          description: User-specified map identifier.
        imageUrl:
          type: string
          description: URL path to the map image.
        worldMinX:
          type: number
        worldMinY:
          type: number
        worldMaxX:
          type: number
        worldMaxY:
          type: number
        worldMinZ:
          type: [number, "null"]
        worldMaxZ:
          type: [number, "null"]
        imageWidth:
          type: integer
        imageHeight:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ContentEntry:
      type: object
      properties:
        id:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        contentType:
          type: string
          description: Type category (e.g., `weapon`, `map`, `event_type`).
        contentId:
          type: string
          description: Unique identifier within the content type.
        displayName:
          type: string
        description:
          type: [string, "null"]
        category:
          type: [string, "null"]
        metadata:
          type: [object, "null"]
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ContentEntryPayload:
      type: object
      required: [contentType, contentId, displayName]
      properties:
        contentType:
          type: string
          maxLength: 100
          description: Type category (e.g., `weapon`, `map`).
        contentId:
          type: string
          maxLength: 255
          description: Unique identifier within the content type.
        displayName:
          type: string
          maxLength: 255
          description: Human-readable display name.
        description:
          type: [string, "null"]
          maxLength: 2000
        category:
          type: [string, "null"]
          maxLength: 255
        metadata:
          type: [object, "null"]
          additionalProperties: true
          description: Arbitrary metadata (max 10 KB serialized).

  responses:
    BadRequest:
      description: Invalid request parameters or body.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    RateLimited:
      description: Rate limit exceeded.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
      headers:
        X-RateLimit-Limit:
          $ref: "#/components/headers/X-RateLimit-Limit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/X-RateLimit-Remaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/X-RateLimit-Reset"
