openapi: 3.0.3
info:
  title: ConstruProgress Mobile API
  version: "1.1.0"
  description: >
    Offline-first sync API for the mobile app. Auth via Laravel Sanctum bearer
    tokens (ability `mobile-sync`). All protected endpoints require
    `Authorization: Bearer <token>`. See docs/MOBILE_SYNC_PROTOCOL.md.


    ## Changelog (from v1.0)


    - Inspection templates are now **global** and assigned to projects via
      pivot. `bundle.templates` and `/templates` return templates *assigned* to
      the accessible projects (no longer filtered by `project_id`).
      `template.phase_id` was **removed**. `project_id` remains in the schema
      for compatibility but no longer determines visibility.

    - Template `fields[]` items may include `group` (section), `question` (short
      prompt shown at inspection time) and `help` (long help text).

    - New entities in the bundle: `feature_types` (global catalogue),
      `issue_tasks`, `issue_comments`.

    - `feature` gains `feature_type_id`, `is_active`; `issue` gains `type`.

    - New `/sync` ops: `issue.update`, `issue_task.create`, `issue_task.update`,
      `issue_comment.create`. `feature.update` accepts `is_active` and
      `feature_type_id`; `issue.create`/`update` accept `type`.

    - `POST /media` `parent_entity` also accepts `issue_task` and `issue_comment`.

    - `deleted` tombstones include `issue_tasks` and `issue_comments`.
servers:
  - url: /api/v1
security:
  - bearerAuth: []
paths:
  /login:
    post:
      summary: Issue a device token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, device_name]
              properties:
                email: { type: string, format: email }
                password: { type: string }
                device_name: { type: string }
                app_version: { type: string, nullable: true }
      responses:
        "200":
          description: Token issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  user: { $ref: '#/components/schemas/User' }
        "422": { description: Invalid credentials }
  /me:
    get:
      summary: Current user + effective permissions
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }
        "401": { description: Unauthenticated }
  /logout:
    post:
      summary: Revoke the current device token
      responses:
        "200": { description: Logged out }
  /projects:
    get:
      summary: Projects the user can access
      responses:
        "200": { description: OK }
  /projects/{project}/bundle:
    get:
      summary: Offline bundle (full, or delta when `since` is given)
      parameters:
        - name: project
          in: path
          required: true
          schema: { type: integer }
        - name: since
          in: query
          required: false
          description: >
            ISO8601 timestamp. Returns only records changed after it, plus
            `deleted` tombstones. MUST be URL-encoded (the `+` offset).
          schema: { type: string, format: date-time }
      responses:
        "200":
          description: Bundle
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Bundle' }
        "403": { description: Not a member of the project }
  /templates:
    get:
      summary: Global inspection templates ASSIGNED to any accessible project (via pivot)
      description: >
        Templates are a global catalogue. This endpoint returns the templates
        that have been *assigned* (via `inspection_template_project` pivot) to
        any of the user's accessible projects. Each item includes `version`
        (updated_at timestamp) and `hash` for change detection.
      parameters:
        - name: since
          in: query
          required: false
          schema: { type: string, format: date-time }
      responses:
        "200": { description: OK }
  /sync:
    post:
      summary: Push a batch of offline mutations (idempotent by uuid)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [operations]
              properties:
                operations:
                  type: array
                  items: { $ref: '#/components/schemas/Operation' }
      responses:
        "200":
          description: Per-operation results
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/OperationResult' }
  /media:
    post:
      summary: Upload a file (multipart) and attach it to a parent record
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [uuid, parent_entity, parent_id, file]
              properties:
                uuid: { type: string, format: uuid }
                parent_entity: { type: string, enum: [feature, issue, issue_task, issue_comment, project, phase, layer] }
                parent_id: { type: integer }
                file: { type: string, format: binary }
                category: { type: string, enum: [image, document, other] }
                description: { type: string }
      responses:
        "200": { description: applied | duplicate }
        "403": { description: Forbidden }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  schemas:
    User:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        email: { type: string }
        roles: { type: array, items: { type: string } }
        permissions: { type: array, items: { type: string } }
    Operation:
      type: object
      required: [entity, op, uuid, data]
      properties:
        entity: { type: string, enum: [progress_update, inspection, issue, issue_task, issue_comment, feature] }
        op: { type: string, enum: [create, update] }
        uuid: { type: string, format: uuid, description: client-generated idempotency key }
        client_updated_at: { type: string, format: date-time }
        data: { type: object }
      example:
        entity: feature
        op: update
        uuid: 0f8e...-uuid
        client_updated_at: "2026-06-18T12:00:00+00:00"
        data: { id: 5, status: completed, progress: 100 }
    OperationResult:
      type: object
      properties:
        uuid: { type: string, format: uuid }
        status: { type: string, enum: [applied, duplicate, conflict, error] }
        server_id: { type: integer, nullable: true }
        error: { type: string, nullable: true }
        server: { type: object, nullable: true, description: current server value on conflict }
    Bundle:
      type: object
      properties:
        server_time: { type: string, format: date-time }
        project: { type: object }
        phases: { type: array, items: { type: object } }
        layers: { type: array, items: { type: object } }
        features: { type: array, items: { type: object } }
        feature_types:
          type: array
          description: Global catalogue of feature types (id, name, color)
          items: { type: object }
        inspections: { type: array, items: { type: object } }
        issues:
          type: array
          description: >
            Includes `type` (defect|safety|quality|documentation|other),
            `status` (open|in_review|resolved|closed),
            `priority` (low|medium|high|critical).
          items: { type: object }
        issue_tasks:
          type: array
          description: Checklist tasks per issue.
          items: { type: object }
        issue_comments:
          type: array
          description: Comment thread per issue.
          items: { type: object }
        templates:
          type: array
          description: >
            Templates assigned to this project via pivot (no longer filtered by
            project_id). Each item exposes version/hash for change detection and
            `fields[]` where each field may include `group`, `question`, `help`.
          items: { type: object }
        media: { type: array, items: { type: object } }
        deleted:
          type: object
          description: tombstones (ids of soft-deleted records) when `since` is given
          properties:
            phases: { type: array, items: { type: integer } }
            layers: { type: array, items: { type: integer } }
            features: { type: array, items: { type: integer } }
            inspections: { type: array, items: { type: integer } }
            issues: { type: array, items: { type: integer } }
            issue_tasks: { type: array, items: { type: integer } }
            issue_comments: { type: array, items: { type: integer } }
